Ver Fonte

[feat]亲密度分析,报告卡片,增加数值概览和进度条

hezihao há 7 meses atrás
pai
commit
9ac4b946d2

+ 8 - 0
assets/color/business_color.xml

@@ -45,4 +45,12 @@
 
     <!-- 亲密度分析,添加预测方向 -->
     <color name="bg_add_prediction_direction">#B2755BAB</color>
+
+    <!-- 亲密度分析,报告概览区域的背景色 -->
+    <color name="bg_report_overview">#FFFAF9FC</color>
+    <!-- 亲密度分析,报告概览数值描述的文字颜色 -->
+    <color name="text_report_overview_value_desc">#FF758696</color>
+
+    <!-- 亲密度分析,数值的图标的背景颜色 -->
+    <color name="bg_report_value_icon">#FFF6F5FA</color>
 </resources>

+ 24 - 0
assets/color/common_color.xml

@@ -37,6 +37,7 @@
     <color name="black55">#8C000000</color>
     <color name="black50">#80000000</color>
     <color name="black45">#73000000</color>
+    <color name="black46">#75000000</color>
     <color name="black47">#78000000</color>
     <color name="black40">#66000000</color>
     <color name="black41">#69000000</color>
@@ -48,4 +49,27 @@
     <color name="black10">#1A000000</color>
     <color name="black5">#0D000000</color>
 
+    <!-- 蓝色渐变色 -->
+    <color name="blue_gradient1">#FFABA6FF</color>
+    <color name="blue_gradient2">#FF8279FF</color>
+
+    <!-- 绿色渐变色 -->
+    <color name="green_gradient1">#FF79E8D2</color>
+    <color name="green_gradient2">#FF5ACEB8</color>
+
+    <!-- 黄色渐变色 -->
+    <color name="yellow_gradient1">#FFFFDB99</color>
+    <color name="yellow_gradient2">#FFFFC14D</color>
+
+    <!-- 粉色渐变色 -->
+    <color name="pink_gradient1">#FFFFA4B9</color>
+    <color name="pink_gradient2">#FFFF7695</color>
+
+    <!-- 粉色渐变色 -->
+    <color name="pink_gradient1">#FFFFA4B9</color>
+    <color name="pink_gradient2">#FFFF7695</color>
+
+    <!-- 紫色渐变色 -->
+    <color name="purple_gradient1">#FFE1BAFF</color>
+    <color name="purple_gradient2">#FFBE68FF</color>
 </resources>

BIN
assets/images/icon_emoji_chat.webp


BIN
assets/images/icon_emoji_hi.webp


BIN
assets/images/icon_emoji_like.webp


BIN
assets/images/icon_emoji_love.webp


BIN
assets/images/icon_emoji_percent.webp


BIN
assets/images/icon_intimacy_analyse_report_overview_love.webp


BIN
assets/images/icon_intimacy_analyse_report_overview_stage.webp


+ 3 - 0
assets/string/base/string.xml

@@ -253,6 +253,9 @@
     <string name="and">与</string>
 
     <string name="intimacy_relation">亲密关系</string>
+    <string name="intimacy_value">情感亲密度</string>
+    <string name="intimacy_current_stage">目前阶段</string>
+    <string name="intimacy_value_percent">%</string>
 
     <string name="next_step">下一步</string>
     <string name="recently">最近</string>

+ 7 - 4
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart

@@ -36,19 +36,22 @@ class IntimacyAnalyseUploadController extends BaseController {
   ● 互补型:一方外向活泼,另一方沉稳内敛,形成动态平衡。
   ● 相似型:三观一致,兴趣重叠,减少摩擦但需警惕新鲜感流失。
   ● 关键结论:差异是火花的来源,但核心价值观需一致(如家庭观、金钱观)。
-  
+
   **沟通模式分析**
-  
+
   ● 语言风格:幽默调侃型 vs 理性分析型 → 需找到共同表达方式。
   ● 冲突解决:回避型 vs 直面型 → 建议建立“冷静-沟通”机制。
   ● 情感需求:一方需要高频互动,另一方偏好独立空间 → 需协商平衡点。
-  
+
   **爱情语言测试**
-  
+
   ● 根据盖瑞·查普曼的“五种爱之语”理论,分析双方的情感表达偏好:
   ● 你的主要爱语:肯定的言辞(如情话、鼓励)
   '''.obs;
 
+  // TODO hezihao,测试报告生成中
+  // RxString reportMarkdownData = "".obs;
+
   @override
   void onInit() {
     super.onInit();

+ 349 - 50
lib/module/intimacy_analyse/widget/intimacy_analyse_report_widget.dart

@@ -4,6 +4,7 @@ import 'package:keyboard/resource/string.gen.dart';
 
 import '../../../resource/assets.gen.dart';
 import '../../../resource/colors.gen.dart';
+import '../../../widget/animated_progress_bar.dart';
 import '../../../widget/markdown/markdown_viewer.dart';
 
 /// 亲密度报告组件
@@ -15,9 +16,352 @@ class IntimacyAnalyseReportWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    // 卡片背景
+    return reportContent.isEmpty
+        // 报告生成中
+        ? CreatingReportCardWidget()
+        // 已出报告
+        : ExistReportCardWidget(reportContent: reportContent);
+  }
+}
+
+/// 已有报告组件
+class ExistReportCardWidget extends StatelessWidget {
+  /// 报告内容,Markdown格式
+  final String reportContent;
+
+  const ExistReportCardWidget({super.key, required this.reportContent});
+
+  @override
+  Widget build(BuildContext context) {
+    return ReportCardFrameWidget(
+      height: 825.h,
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          _buildReportOverview(),
+          SizedBox(height: 10.h),
+          _buildReportDetail(),
+        ],
+      ),
+    );
+  }
+
+  /// 报告概览,包含概览数值和图表
+  Widget _buildReportOverview() {
+    // 圆角背景
+    return Container(
+      padding: EdgeInsets.only(left: 12.w, right: 12.w, top: 12.h),
+      decoration: BoxDecoration(
+        color: ColorName.white,
+        borderRadius: BorderRadius.circular(20.r),
+      ),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          // 概览数值
+          Container(
+            decoration: BoxDecoration(
+              color: ColorName.bgReportOverview,
+              borderRadius: BorderRadius.all(Radius.circular(10.r)),
+            ),
+            child: Row(
+              children: [
+                _buildIntimacyOverviewItem("30"),
+                SizedBox(width: 29.w),
+                _buildOverviewDivider(),
+                SizedBox(width: 29.w),
+                _buildCurrentStageOverviewItem("相互了解"),
+              ],
+            ),
+          ),
+          SizedBox(height: 12.h),
+          // 图表
+          _buildChart(),
+        ],
+      ),
+    );
+  }
+
+  /// 概览数值的分割线
+  Widget _buildOverviewDivider() {
+    return Container(
+      height: 44.h,
+      width: 1.w,
+      decoration: BoxDecoration(color: ColorName.black5),
+      margin: EdgeInsets.symmetric(vertical: 10.h),
+    );
+  }
+
+  /// 概览亲密度的数值项
+  Widget _buildIntimacyOverviewItem(String value) {
+    return Expanded(
+      child: Row(
+        children: [
+          // 图表
+          Assets.images.iconIntimacyAnalyseReportOverviewLove.image(
+            height: 22.w,
+            width: 22.w,
+          ),
+          SizedBox(width: 14.w),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              // 数值区域
+              Row(
+                children: [
+                  // 数值
+                  Text(
+                    value,
+                    style: TextStyle(
+                      color: ColorName.black80,
+                      fontSize: 20.sp,
+                      fontWeight: FontWeight.w700,
+                    ),
+                  ),
+                  SizedBox(width: 2.w),
+                  // 百分比符号
+                  Text(
+                    StringName.intimacyValuePercent,
+                    style: TextStyle(
+                      color: ColorName.black46,
+                      fontSize: 13.sp,
+                      fontWeight: FontWeight.w700,
+                    ),
+                  ),
+                ],
+              ),
+              // 描述
+              Text(
+                StringName.intimacyValue,
+                style: TextStyle(
+                  color: ColorName.textReportOverviewValueDesc,
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 概览目前阶段的数值项
+  Widget _buildCurrentStageOverviewItem(String value) {
+    return Expanded(
+      child: Row(
+        children: [
+          // 图标
+          Assets.images.iconIntimacyAnalyseReportOverviewStage.image(
+            height: 22.w,
+            width: 22.w,
+          ),
+          SizedBox(width: 14.w),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              // 数值
+              Text(
+                value,
+                style: TextStyle(
+                  color: ColorName.black80,
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w700,
+                ),
+              ),
+              // 描述
+              Text(
+                StringName.intimacyCurrentStage,
+                style: TextStyle(
+                  color: ColorName.textReportOverviewValueDesc,
+                  fontSize: 12.sp,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 报告图表
+  Widget _buildChart() {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        _buildValueItem(
+          iconProvider: Assets.images.iconEmojiLike.provider(),
+          title: "互动好感度",
+          value: 0.5,
+          progressColors: [ColorName.blueGradient1, ColorName.blueGradient2],
+        ),
+        SizedBox(height: 18.h),
+        _buildValueItem(
+          iconProvider: Assets.images.iconEmojiChat.provider(),
+          title: "话题好感度",
+          value: 0.35,
+          progressColors: [ColorName.greenGradient1, ColorName.greenGradient2],
+        ),
+        SizedBox(height: 18.h),
+        _buildValueItem(
+          iconProvider: Assets.images.iconEmojiLike.provider(),
+          title: "情绪回应",
+          value: 1.0,
+          progressColors: [
+            ColorName.yellowGradient1,
+            ColorName.yellowGradient2,
+          ],
+        ),
+        SizedBox(height: 18.h),
+        _buildValueItem(
+          iconProvider: Assets.images.iconEmojiPercent.provider(),
+          title: "亲密词占比",
+          value: 0.4,
+          progressColors: [ColorName.pinkGradient1, ColorName.pinkGradient2],
+        ),
+        SizedBox(height: 18.h),
+        _buildValueItem(
+          iconProvider: Assets.images.iconEmojiLove.provider(),
+          title: "缘分指数",
+          value: 0.15,
+          progressColors: [
+            ColorName.purpleGradient1,
+            ColorName.purpleGradient2,
+          ],
+        ),
+        SizedBox(height: 12, width: double.infinity),
+      ],
+    );
+  }
+
+  /// 构建数值Item
+  /// [iconProvider] icon图标的ImageProvider
+  /// [title] 标题
+  /// [progressColors] 进度条渐变色
+  /// [value] 进度条的数值,从0到1.0
+  Widget _buildValueItem({
+    required ImageProvider iconProvider,
+    required String title,
+    required List<Color> progressColors,
+    required double value,
+  }) {
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        // 图标
+        Container(
+          padding: EdgeInsets.all(6.w),
+          decoration: BoxDecoration(
+            // 圆形背景
+            shape: BoxShape.circle,
+            color: ColorName.bgReportValueIcon,
+          ),
+          child: ClipOval(
+            child: Image(
+              image: iconProvider,
+              width: 15.w,
+              height: 15.w,
+              fit: BoxFit.fill,
+            ),
+          ),
+        ),
+        SizedBox(width: 5.w),
+        // 文字
+        Text(title),
+        SizedBox(width: 10.w),
+        // 进度条
+        Expanded(
+          child: AnimatedGradientProgressBar(
+            targetValue: value,
+            // 渐变色
+            gradient: LinearGradient(colors: progressColors),
+            duration: const Duration(seconds: 1),
+          ),
+        ),
+      ],
+    );
+  }
+
+  /// 报告详情
+  Widget _buildReportDetail() {
     return Container(
       height: 424.h,
+      decoration: ShapeDecoration(
+        color: ColorName.white,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(30.r),
+        ),
+      ),
+      child: MarkdownViewer(content: reportContent),
+    );
+  }
+}
+
+/// 报告生成中的卡片
+class CreatingReportCardWidget extends StatelessWidget {
+  const CreatingReportCardWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return ReportCardFrameWidget(
+      height: 300.h,
+      child: Container(
+        decoration: ShapeDecoration(
+          color: ColorName.white,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(30.r),
+          ),
+        ),
+        child: Center(
+          child: Column(
+            // 垂直水平都居中
+            mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              // 图标
+              Assets.images.iconIntimacyAnalysisReportCreating.image(
+                width: 82,
+                height: 82,
+              ),
+              SizedBox(height: 3.h),
+              // 文字
+              Text(
+                StringName.intimacyAnalyseReportCreating,
+                style: TextStyle(
+                  fontSize: 14.sp,
+                  color: ColorName.black60,
+                  fontWeight: FontWeight.w400,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// 报告卡片框架组件,只定义报告卡片的基本框架,例如卡片背景和顶部的标题,内容由子组件来实现
+class ReportCardFrameWidget extends StatelessWidget {
+  // 卡片高度
+  final double height;
+
+  /// 内容子组件
+  final Widget child;
+
+  const ReportCardFrameWidget({
+    super.key,
+    required this.height,
+    required this.child,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    // 卡片背景
+    return Container(
+      height: height,
       margin: EdgeInsets.only(left: 12, right: 12, bottom: 0),
       padding: EdgeInsets.only(bottom: 12),
       decoration: BoxDecoration(
@@ -28,22 +372,10 @@ class IntimacyAnalyseReportWidget extends StatelessWidget {
       ),
       child: Stack(
         children: [
-          // 图标和标题
-          _buildReportTopLayout(),
-          // 内容
-          Container(
-            margin: EdgeInsets.only(top: 51, left: 12, right: 12, bottom: 5),
-            decoration: ShapeDecoration(
-              color: ColorName.white,
-              shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(30.r),
-              ),
-            ),
-            child:
-                reportContent.isEmpty
-                    ? _buildCreatingLayout()
-                    : _buildReportContentLayout(),
-          ),
+          // 顶部的图标和标题
+          Positioned(left: 0, right: 0, top: 0, child: _buildReportTopLayout()),
+          // 内容区域
+          Positioned(top: 51, left: 12, right: 12, bottom: 5, child: child),
         ],
       ),
     );
@@ -64,37 +396,4 @@ class IntimacyAnalyseReportWidget extends StatelessWidget {
       ],
     );
   }
-
-  /// 报告内容布局
-  Widget _buildReportContentLayout() {
-    return MarkdownViewer(content: reportContent);
-  }
-
-  /// 生成中布局
-  Widget _buildCreatingLayout() {
-    return Center(
-      child: Column(
-        // 垂直水平都居中
-        mainAxisAlignment: MainAxisAlignment.center,
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          // 图标
-          Assets.images.iconIntimacyAnalysisReportCreating.image(
-            width: 82,
-            height: 82,
-          ),
-          SizedBox(height: 3.h),
-          // 文字
-          Text(
-            StringName.intimacyAnalyseReportCreating,
-            style: TextStyle(
-              fontSize: 14.sp,
-              color: ColorName.black60,
-              fontWeight: FontWeight.w400,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
 }

+ 39 - 0
lib/resource/assets.gen.dart

@@ -434,6 +434,26 @@ class $AssetsImagesGen {
   AssetGenImage get iconDiscountTutorial =>
       const AssetGenImage('assets/images/icon_discount_tutorial.webp');
 
+  /// File path: assets/images/icon_emoji_chat.webp
+  AssetGenImage get iconEmojiChat =>
+      const AssetGenImage('assets/images/icon_emoji_chat.webp');
+
+  /// File path: assets/images/icon_emoji_hi.webp
+  AssetGenImage get iconEmojiHi =>
+      const AssetGenImage('assets/images/icon_emoji_hi.webp');
+
+  /// File path: assets/images/icon_emoji_like.webp
+  AssetGenImage get iconEmojiLike =>
+      const AssetGenImage('assets/images/icon_emoji_like.webp');
+
+  /// File path: assets/images/icon_emoji_love.webp
+  AssetGenImage get iconEmojiLove =>
+      const AssetGenImage('assets/images/icon_emoji_love.webp');
+
+  /// File path: assets/images/icon_emoji_percent.webp
+  AssetGenImage get iconEmojiPercent =>
+      const AssetGenImage('assets/images/icon_emoji_percent.webp');
+
   /// File path: assets/images/icon_goods_info_title.webp
   AssetGenImage get iconGoodsInfoTitle =>
       const AssetGenImage('assets/images/icon_goods_info_title.webp');
@@ -446,6 +466,18 @@ class $AssetsImagesGen {
   AssetGenImage get iconIntimacyAnalyseLove =>
       const AssetGenImage('assets/images/icon_intimacy_analyse_love.webp');
 
+  /// File path: assets/images/icon_intimacy_analyse_report_overview_love.webp
+  AssetGenImage get iconIntimacyAnalyseReportOverviewLove =>
+      const AssetGenImage(
+        'assets/images/icon_intimacy_analyse_report_overview_love.webp',
+      );
+
+  /// File path: assets/images/icon_intimacy_analyse_report_overview_stage.webp
+  AssetGenImage get iconIntimacyAnalyseReportOverviewStage =>
+      const AssetGenImage(
+        'assets/images/icon_intimacy_analyse_report_overview_stage.webp',
+      );
+
   /// File path: assets/images/icon_intimacy_analyse_report_preview_bubble_symbol.webp
   AssetGenImage get iconIntimacyAnalyseReportPreviewBubbleSymbol =>
       const AssetGenImage(
@@ -901,9 +933,16 @@ class $AssetsImagesGen {
     iconDiscountSubhead,
     iconDiscountTitle,
     iconDiscountTutorial,
+    iconEmojiChat,
+    iconEmojiHi,
+    iconEmojiLike,
+    iconEmojiLove,
+    iconEmojiPercent,
     iconGoodsInfoTitle,
     iconIntimacyAnalyseArrow,
     iconIntimacyAnalyseLove,
+    iconIntimacyAnalyseReportOverviewLove,
+    iconIntimacyAnalyseReportOverviewStage,
     iconIntimacyAnalyseReportPreviewBubbleSymbol,
     iconIntimacyAnalyseReportPreviewLove,
     iconIntimacyAnalyseReportPreviewTitle,

+ 42 - 0
lib/resource/colors.gen.dart

@@ -46,6 +46,12 @@ class ColorName {
   /// Color: #FFBC87FF
   static const Color bgOptionSelectSelected2 = Color(0xFFBC87FF);
 
+  /// Color: #FFFAF9FC
+  static const Color bgReportOverview = Color(0xFFFAF9FC);
+
+  /// Color: #FFF6F5FA
+  static const Color bgReportValueIcon = Color(0xFFF6F5FA);
+
   /// Color: #FF7B7DFF
   static const Color bgStep = Color(0xFF7B7DFF);
 
@@ -79,6 +85,9 @@ class ColorName {
   /// Color: #73000000
   static const Color black45 = Color(0x73000000);
 
+  /// Color: #75000000
+  static const Color black46 = Color(0x75000000);
+
   /// Color: #78000000
   static const Color black47 = Color(0x78000000);
 
@@ -118,6 +127,12 @@ class ColorName {
   /// Color: #F2000000
   static const Color black95 = Color(0xF2000000);
 
+  /// Color: #FFABA6FF
+  static const Color blueGradient1 = Color(0xFFABA6FF);
+
+  /// Color: #FF8279FF
+  static const Color blueGradient2 = Color(0xFF8279FF);
+
   /// Color: #6399FF
   static const Color colorAccentPrimary = Color(0xFF6399FF);
 
@@ -136,6 +151,12 @@ class ColorName {
   /// Color: #BDBDBD
   static const Color disabledTextColor = Color(0xFFBDBDBD);
 
+  /// Color: #FF79E8D2
+  static const Color greenGradient1 = Color(0xFF79E8D2);
+
+  /// Color: #FF5ACEB8
+  static const Color greenGradient2 = Color(0xFF5ACEB8);
+
   /// Color: #FF996DFF
   static const Color inputCursor = Color(0xFF996DFF);
 
@@ -157,12 +178,24 @@ class ColorName {
   /// Color: #FFD3D5E1
   static const Color msgInputBar = Color(0xFFD3D5E1);
 
+  /// Color: #FFFFA4B9
+  static const Color pinkGradient1 = Color(0xFFFFA4B9);
+
+  /// Color: #FFFF7695
+  static const Color pinkGradient2 = Color(0xFFFF7695);
+
   /// Color: #999999
   static const Color placeholderTextColor = Color(0xFF999999);
 
   /// Color: #333333
   static const Color primaryTextColor = Color(0xFF333333);
 
+  /// Color: #FFE1BAFF
+  static const Color purpleGradient1 = Color(0xFFE1BAFF);
+
+  /// Color: #FFBE68FF
+  static const Color purpleGradient2 = Color(0xFFBE68FF);
+
   /// Color: #666666
   static const Color secondaryTextColor = Color(0xFF666666);
 
@@ -172,6 +205,9 @@ class ColorName {
   /// Color: #FF523A68
   static const Color stepTitleColor2 = Color(0xFF523A68);
 
+  /// Color: #FF758696
+  static const Color textReportOverviewValueDesc = Color(0xFF758696);
+
   /// Color: #00FFFFFF
   static const Color transparent = Color(0x00FFFFFF);
 
@@ -243,4 +279,10 @@ class ColorName {
 
   /// Color: #F2FFFFFF
   static const Color white95 = Color(0xF2FFFFFF);
+
+  /// Color: #FFFFDB99
+  static const Color yellowGradient1 = Color(0xFFFFDB99);
+
+  /// Color: #FFFFC14D
+  static const Color yellowGradient2 = Color(0xFFFFC14D);
 }

+ 6 - 0
lib/resource/string.gen.dart

@@ -184,6 +184,9 @@ class StringName {
   static final String mySelf = 'my_self'.tr; // 自己
   static final String and = 'and'.tr; // 与
   static final String intimacyRelation = 'intimacy_relation'.tr; // 亲密关系
+  static final String intimacyValue = 'intimacy_value'.tr; // 情感亲密度
+  static final String intimacyCurrentStage = 'intimacy_current_stage'.tr; // 目前阶段
+  static final String intimacyValuePercent = 'intimacy_value_percent'.tr; // %
   static final String nextStep = 'next_step'.tr; // 下一步
   static final String recently = 'recently'.tr; // 最近
 }
@@ -373,6 +376,9 @@ class StringMultiSource {
       'my_self': '自己',
       'and': '与',
       'intimacy_relation': '亲密关系',
+      'intimacy_value': '情感亲密度',
+      'intimacy_current_stage': '目前阶段',
+      'intimacy_value_percent': '%',
       'next_step': '下一步',
       'recently': '最近',
     },

+ 152 - 0
lib/widget/animated_progress_bar.dart

@@ -0,0 +1,152 @@
+import 'package:flutter/material.dart';
+
+/// 支持动画过渡和渐变色的进度条
+// 使用示例
+// AnimatedGradientProgressBar(
+//   targetValue: 0.75,
+//   gradient: LinearGradient(colors: [Colors.orange, Colors.red]),
+//   height: 10,
+//   borderRadius: 5,
+// )
+class AnimatedGradientProgressBar extends StatefulWidget {
+  // 目标进度值(0~1)
+  final double targetValue;
+
+  // 动画时长
+  final Duration duration;
+
+  // 渐变色
+  final Gradient gradient;
+
+  // 进度条高度
+  final double height;
+
+  // 圆角半径
+  final double borderRadius;
+
+  const AnimatedGradientProgressBar({
+    super.key,
+    required this.targetValue,
+    required this.duration,
+    required this.gradient,
+    this.height = 10.0,
+    this.borderRadius = 7.0,
+  });
+
+  @override
+  AnimatedGradientProgressBarState createState() =>
+      AnimatedGradientProgressBarState();
+}
+
+class AnimatedGradientProgressBarState
+    extends State<AnimatedGradientProgressBar>
+    with TickerProviderStateMixin {
+  late AnimationController _controller;
+  late Animation<double> _animation;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = AnimationController(duration: widget.duration, vsync: this);
+    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
+    _startAnimation();
+  }
+
+  @override
+  void didUpdateWidget(AnimatedGradientProgressBar oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.targetValue != widget.targetValue) {
+      _resetAnimation();
+      _startAnimation();
+    }
+  }
+
+  @override
+  void dispose() {
+    // 移除监听
+    _animation.removeListener(() {});
+    _controller.dispose();
+    super.dispose();
+  }
+
+  void _startAnimation() {
+    print('进度条 => 初始动画值: ${_animation.value}'); // 0.0
+
+    // 先移除旧监听
+    _animation.removeListener(() {});
+
+    // 创建新的 Tween 动画
+    _animation = Tween<double>(
+      begin: 0,
+      end: widget.targetValue,
+    ).animate(_animation);
+
+    // 添加监听器并启动动画
+    _animation.addListener(() => setState(() {}));
+    _controller.forward();
+
+    Future.delayed(widget.duration, () {
+      print('进度条 => 最终动画值: ${_animation.value}');
+    });
+  }
+
+  void _resetAnimation() => _controller.reset();
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      // 允许子组件溢出自己本身的大小,默认是裁切的
+      clipBehavior: Clip.none,
+      children: [
+        // 百分比文本
+        Positioned(
+          right: 0,
+          // 文字位于进度条上方
+          bottom: widget.height + 4,
+          child: AnimatedBuilder(
+            animation: _animation,
+            builder: (context, _) {
+              return Text(
+                '${(_animation.value * 100).toStringAsFixed(0)}%',
+                style: const TextStyle(fontSize: 12, color: Colors.black54),
+              );
+            },
+          ),
+        ),
+        // 进度条
+        ClipRRect(
+          borderRadius: BorderRadius.circular(widget.borderRadius),
+          child: Container(
+            width: double.infinity,
+            height: widget.height,
+            decoration: BoxDecoration(
+              color: Colors.grey[200],
+              borderRadius: BorderRadius.circular(widget.borderRadius),
+            ),
+            child: Stack(
+              children: [
+                AnimatedBuilder(
+                  animation: _animation,
+                  builder: (context, _) {
+                    return FractionallySizedBox(
+                      widthFactor: _animation.value,
+                      alignment: Alignment.centerLeft,
+                      child: Container(
+                        decoration: BoxDecoration(
+                          gradient: widget.gradient,
+                          borderRadius: BorderRadius.circular(
+                            widget.borderRadius,
+                          ),
+                        ),
+                      ),
+                    );
+                  },
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}