Browse Source

[feat]亲密度分析,搭建上传选择图片的九宫格布局UI

hezihao 7 months ago
parent
commit
4240ccdada
30 changed files with 1156 additions and 105 deletions
  1. 19 0
      assets/color/business_color.xml
  2. 1 0
      assets/color/common_color.xml
  3. BIN
      assets/images/bg_intimacy_analyse_upload_card.webp
  4. BIN
      assets/images/icon_intimacy_analyse_upload_top.webp
  5. BIN
      assets/images/icon_upload_add_symbol.webp
  6. BIN
      assets/images/icon_upload_delete.webp
  7. BIN
      assets/images/icon_upload_fail.webp
  8. BIN
      assets/images/icon_uploading.webp
  9. 17 0
      assets/string/base/string.xml
  10. 5 0
      lib/di/get_it.config.dart
  11. 8 44
      lib/module/intimacy_analyse/analyse_report/intimacy_analyse_report_view.dart
  12. 39 29
      lib/module/intimacy_analyse/analyse_report/intimacy_analyse_report_view_controller.dart
  13. 41 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart
  14. 334 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart
  15. 31 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/widget/step_label_widget.dart
  16. 76 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_add_widget.dart
  17. 174 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_item_widget.dart
  18. 90 0
      lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_nine_grid.dart
  19. 91 0
      lib/module/intimacy_analyse/widget/intimacy_user_widget.dart
  20. 31 0
      lib/resource/assets.gen.dart
  21. 30 0
      lib/resource/colors.gen.dart
  22. 24 0
      lib/resource/string.gen.dart
  23. 5 0
      lib/router/app_page_arguments.dart
  24. 8 0
      lib/router/app_pages.dart
  25. 32 31
      lib/utils/image_picker_util.dart
  26. 9 0
      lib/utils/string_format_util.dart
  27. 36 0
      lib/widget/gradient_text.dart
  28. 51 0
      lib/widget/rotate_image.dart
  29. 1 1
      pubspec.lock
  30. 3 0
      pubspec.yaml

+ 19 - 0
assets/color/business_color.xml

@@ -8,4 +8,23 @@
     <color name="msg_input_bar">#FFD3D5E1</color>
     <!-- 聊天,输入框的光标 -->
     <color name="input_cursor">#FF996DFF</color>
+
+    <!-- 上传,添加按钮的虚线颜色 -->
+    <color name="upload_add_dotted_border">#FF9F8DC5</color>
+    <!-- 上传,添加按钮中的文字颜色 -->
+    <color name="upload_add_text">#B2755BAB</color>
+    <!-- 步骤标签的背景色 -->
+    <color name="bg_step">#FF7B7DFF</color>
+
+    <!-- 步骤标签的标题渐变色 -->
+    <color name="step_title_color1">#FF202020</color>
+    <color name="step_title_color2">#FF523A68</color>
+
+    <!-- 亲密关系布局的渐变色 -->
+    <color name="bg_intimacy_relation_color1">#FFE2DBFF</color>
+    <color name="bg_intimacy_relation_color2">#FFEBF0FF</color>
+
+    <!-- 亲密关系文字的渐变色 -->
+    <color name="intimacy_relation_color1">#FF5521F6</color>
+    <color name="intimacy_relation_color2">#FFC456F5</color>
 </resources>

+ 1 - 0
assets/color/common_color.xml

@@ -38,6 +38,7 @@
     <color name="black50">#80000000</color>
     <color name="black45">#73000000</color>
     <color name="black40">#66000000</color>
+    <color name="black41">#69000000</color>
     <color name="black35">#59000000</color>
     <color name="black30">#4D000000</color>
     <color name="black25">#40000000</color>

BIN
assets/images/bg_intimacy_analyse_upload_card.webp


BIN
assets/images/icon_intimacy_analyse_upload_top.webp


BIN
assets/images/icon_upload_add_symbol.webp


BIN
assets/images/icon_upload_delete.webp


BIN
assets/images/icon_upload_fail.webp


BIN
assets/images/icon_uploading.webp


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

@@ -221,6 +221,23 @@
     <string name="intimacy_analyse_tab_report">亲密分析报告</string>
     <string name="intimacy_analyse_tab_screenshot_reply">截图回复</string>
 
+    <string name="intimacy_analyse">亲密度分析</string>
+    <string name="intimacy_analyse_upload_card_tip">上传与对方的聊天记录,揭秘文字后的真实想法</string>
+    <string name="intimacy_analyse_switch_ta_test">换TA测测</string>
+    <string name="intimacy_analyse_upload_limit_tip">还可上传%s张</string>
+    <string name="intimacy_analyse_upload_fail">上传失败</string>
+    <string name="intimacy_analyse_uploading">上传中</string>
+
+    <string name="intimacy_analyse_step">STEP%s</string>
+
+    <string name="intimacy_analyse_step_title_select_image">选择你的图片</string>
+    <string name="intimacy_analyse_step_title_select_prediction_direction">选择预测方向</string>
+
+    <string name="my_self">自己</string>
+    <string name="and">与</string>
+
+    <string name="intimacy_relation">亲密关系</string>
+
     <string name="next_step">下一步</string>
     <string name="recently">最近</string>
 

+ 5 - 0
lib/di/get_it.config.dart

@@ -41,6 +41,8 @@ import '../module/feedback/feedback_controller.dart' as _i876;
 import '../module/intimacy_analyse/analyse_report/intimacy_analyse_report_view_controller.dart'
     as _i987;
 import '../module/intimacy_analyse/intimacy_analyse_controller.dart' as _i977;
+import '../module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart'
+    as _i666;
 import '../module/intimacy_analyse/screenshot_reply/intimacy_analyse_screenshot_reply_controller.dart'
     as _i279;
 import '../module/keyboard/keyboard_controller.dart' as _i161;
@@ -86,6 +88,9 @@ extension GetItInjectableX on _i174.GetIt {
     gh.factory<_i415.KeyboardMethodHandler>(
       () => _i415.KeyboardMethodHandler(),
     );
+    gh.factory<_i666.IntimacyAnalyseUploadController>(
+      () => _i666.IntimacyAnalyseUploadController(),
+    );
     gh.singleton<_i361.Dio>(
       () => networkModule.createStreamDio(),
       instanceName: 'streamDio',

+ 8 - 44
lib/module/intimacy_analyse/analyse_report/intimacy_analyse_report_view.dart

@@ -9,6 +9,7 @@ import '../../../data/model/intimacy_analyse_report.dart';
 import '../../../resource/assets.gen.dart';
 import '../../../resource/colors.gen.dart';
 import '../../../widget/avatar/avatar_image_widget.dart';
+import '../widget/intimacy_user_widget.dart';
 import 'intimacy_analyse_report_view_controller.dart';
 
 /// 亲密度分析-分析报告Tab页
@@ -54,39 +55,14 @@ class IntimacyAnalyseReportView
 
   /// 亲密档案
   Widget _buildIntimacyArchives() {
-    return SizedBox(
-      height: 100.h,
+    return IntimacyUserWidget(
       width: 200.w,
-      child: Stack(
-        // 默认内容都居中
-        alignment: Alignment.center,
-        children: [
-          // 用户1
-          Positioned(
-            left: 0,
-            child: _buildAvatar(
-              "",
-              defaultImage: Assets.images.iconDefaultAvatarMale.image().image,
-            ),
-          ),
-          // 用户2
-          Positioned(
-            left: 92.w,
-            top: 0,
-            child: _buildAvatar(
-              "",
-              defaultImage: Assets.images.iconDefaultAvatarFemale.image().image,
-            ),
-          ),
-          // 爱心
-          Positioned(
-            child: Assets.images.iconIntimacyAnalyseLove.image(
-              width: 42.w,
-              height: 42.2.w,
-            ),
-          ),
-        ],
-      ),
+      height: 100.h,
+      avatarSize: 96.0,
+      avatarUrl1: '',
+      avatarUrl2: '',
+      avatarBorderWidth: 3.0,
+      loveSize: 42.w,
     );
   }
 
@@ -302,16 +278,4 @@ class IntimacyAnalyseReportView
       ),
     );
   }
-
-  /// 圆形头像
-  Widget _buildAvatar(String? imageUrl, {ImageProvider? defaultImage}) {
-    return CircleAvatarWidget(
-      imageUrl: imageUrl,
-      placeholderImage: defaultImage,
-      borderColor: ColorName.white,
-      borderWidth: 3.0,
-      backgroundColor: ColorName.white,
-      size: 96.0,
-    );
-  }
 }

+ 39 - 29
lib/module/intimacy_analyse/analyse_report/intimacy_analyse_report_view_controller.dart

@@ -2,49 +2,59 @@ import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
 
 import '../../../data/model/intimacy_analyse_report.dart';
 import '../../../utils/image_picker_util.dart';
+import '../intimacy_analyse_upload/intimacy_analyse_upload_page.dart';
 
 /// 亲密度分析-分析报告Tab-Controller
 @injectable
 class IntimacyAnalyseReportController extends BaseController {
   /// 报告预览数据
-  Rx<IntimacyAnalyseReport> reportPreviewData = IntimacyAnalyseReport(list: [
-    AnalyseItem(
-      title: "性格匹配度",
-      sections: [
-        "互补型:一方外向活泼,另一方沉稳内敛,形成动态平衡。",
-        "相似型:三观一致,兴趣重叠,减少摩擦但需警惕新鲜感流失。",
-        "关键结论:差异是火花的来源,但核心价值观需一致(如家庭观、金钱观)。"
-      ],
-    ),
-    AnalyseItem(
-      title: "沟通模式分析",
-      sections: [
-        "语言风格:幽默调侃型 vs 理性分析型 → 需找到共同表达方式。",
-        "冲突解决:回避型 vs 直面型 → 建议建立“冷静-沟通”机制。",
-        "情感需求:一方需要高频互动,另一方偏好独立空间 → 需协商平衡点。"
-      ],
-    ),
-    AnalyseItem(
-      title: "爱情语言测试",
-      sections: [
-        "根据盖瑞·查普曼的“五种爱之语”理论,分析双方的情感表达偏好:",
-        "你的主要爱语:肯定的言辞(如情话、鼓励)",
-      ],
-    ),
-  ]).obs;
+  Rx<IntimacyAnalyseReport> reportPreviewData =
+      IntimacyAnalyseReport(
+        list: [
+          AnalyseItem(
+            title: "性格匹配度",
+            sections: [
+              "互补型:一方外向活泼,另一方沉稳内敛,形成动态平衡。",
+              "相似型:三观一致,兴趣重叠,减少摩擦但需警惕新鲜感流失。",
+              "关键结论:差异是火花的来源,但核心价值观需一致(如家庭观、金钱观)。",
+            ],
+          ),
+          AnalyseItem(
+            title: "沟通模式分析",
+            sections: [
+              "语言风格:幽默调侃型 vs 理性分析型 → 需找到共同表达方式。",
+              "冲突解决:回避型 vs 直面型 → 建议建立“冷静-沟通”机制。",
+              "情感需求:一方需要高频互动,另一方偏好独立空间 → 需协商平衡点。",
+            ],
+          ),
+          AnalyseItem(
+            title: "爱情语言测试",
+            sections: [
+              "根据盖瑞·查普曼的“五种爱之语”理论,分析双方的情感表达偏好:",
+              "你的主要爱语:肯定的言辞(如情话、鼓励)",
+            ],
+          ),
+        ],
+      ).obs;
 
   /// 报告预览-气泡文本
   RxString reportPreviewBubbleText = '报告还有2565字未显示,点击按钮查看完整报告'.obs;
 
   /// 切换亲密档案
-  void clickIntimacyArchivesSwitch() {
-  }
+  void clickIntimacyArchivesSwitch() {}
 
   /// 解锁
-  void clickUnlockBtn(BuildContext context) {
-    ImagePickerUtil.pickImage(context, maxAssetsCount: 9);
+  void clickUnlockBtn(BuildContext context) async {
+    // 跳转到图片选择,并返回选择的图片列表
+    List<AssetEntity> selectedAssetList = await ImagePickerUtil.pickImage(
+      context,
+      maxAssetsCount: 9,
+    );
+    // 跳转到亲密度分析上传页
+    IntimacyAnalyseUploadPage.start(selectedAssetList);
   }
 }

+ 41 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart

@@ -0,0 +1,41 @@
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+import '../../../router/app_page_arguments.dart';
+import '../../../utils/atmob_log.dart';
+
+/// 亲密度分析上传页Controller
+@injectable
+class IntimacyAnalyseUploadController extends BaseController {
+  final String tag = "IntimacyAnalyseUploadController";
+
+  /// 已选择的图片列表
+  RxList<AssetEntity> selectedAssetList = <AssetEntity>[].obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initArgs();
+  }
+
+  /// 初始化参数
+  void _initArgs() {
+    final arguments = Get.arguments as Map<String, dynamic>?;
+
+    if (arguments?[AppPageArguments.selectedAssetList] == null) {
+      AtmobLog.i(tag, '没有传递 selectedAssetList 参数');
+    } else {
+      final List<AssetEntity>? argumentList = arguments?[AppPageArguments.selectedAssetList] as List<AssetEntity>?;
+      if (argumentList != null) {
+        selectedAssetList.assignAll(argumentList);
+        AtmobLog.i(tag, "selectedAssetList: $selectedAssetList");
+      }
+    }
+  }
+
+  /// 返回上一页
+  void clickBack() {
+    Get.back();
+  }
+}

+ 334 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart

@@ -0,0 +1,334 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/base/base_page.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/step_label_widget.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_add_widget.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_item_widget.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_nine_grid.dart';
+import 'package:keyboard/resource/colors.gen.dart';
+import 'package:keyboard/resource/string.gen.dart';
+import 'package:sprintf/sprintf.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../../resource/assets.gen.dart';
+import '../../../router/app_page_arguments.dart';
+import '../../../router/app_pages.dart';
+import '../../../utils/string_format_util.dart';
+import '../../../widget/gradient_text.dart';
+import '../widget/intimacy_user_widget.dart';
+import 'intimacy_analyse_upload_controller.dart';
+
+/// 亲密度分析上传页
+class IntimacyAnalyseUploadPage
+    extends BasePage<IntimacyAnalyseUploadController> {
+  const IntimacyAnalyseUploadPage({super.key});
+
+  @override
+  bool immersive() {
+    // 开启沉浸式
+    return true;
+  }
+
+  @override
+  backgroundColor() {
+    return Colors.transparent;
+  }
+
+  /// 页面跳转
+  static start(List<AssetEntity> selectedAssetList) {
+    Get.toNamed(
+      RoutePath.intimacyAnalyseUpload,
+      arguments: {AppPageArguments.selectedAssetList: selectedAssetList},
+    );
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Scaffold(
+      backgroundColor: backgroundColor(),
+      body: Container(
+        decoration: BoxDecoration(
+          image: DecorationImage(
+            image: Assets.images.bgIntimacyAnalyse.provider(),
+            fit: BoxFit.fill,
+          ),
+        ),
+        child: Column(
+          children: [_buildStatusBar(), _buildTopBar(), _buildContent()],
+        ),
+      ),
+    );
+  }
+
+  /// 导航栏占位
+  Widget _buildStatusBar() {
+    double statusBarHeight = MediaQuery.of(Get.context!).padding.top;
+    return Container(
+      // 导航栏高度
+      height: statusBarHeight,
+      color: backgroundColor(),
+    );
+  }
+
+  /// 顶部栏
+  Widget _buildTopBar() {
+    return Container(
+      // 宽度撑满父组件
+      width: double.infinity,
+      // 高度为标准导航栏高度
+      height: kToolbarHeight,
+      // 背景颜色
+      color: Colors.transparent,
+      child: ConstrainedBox(
+        // 设置宽度为无限大,撑满父组件,否则Stack获取不到高度,会报错
+        constraints: BoxConstraints(minWidth: double.infinity),
+        child: Stack(
+          alignment: Alignment.center,
+          children: [
+            // 返回按钮
+            Positioned(
+              left: 16.0,
+              child: GestureDetector(
+                onTap: controller.clickBack,
+                child: Assets.images.iconWhiteBackArrow.image(
+                  width: 24.w,
+                  height: 24.h,
+                ),
+              ),
+            ),
+            // TabBar
+            Positioned(
+              child: Text(
+                StringName.intimacyAnalyse,
+                style: TextStyle(
+                  color: ColorName.white,
+                  fontSize: 17.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 步骤标题
+  Widget _buildStepTitle(int step, String title) {
+    return Row(
+      children: [
+        // 步骤标签
+        StepLabelWidget(
+          label: StringFormatUtil.formatStr(
+            StringName.intimacyAnalyseStep,
+            step.toString(),
+          ),
+        ),
+        SizedBox(width: 10.w),
+        // 标题
+        GradientText(
+          // 渐变颜色
+          colors: [ColorName.stepTitleColor1, ColorName.stepTitleColor2],
+          child: Text(
+            title,
+            style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.w700),
+          ),
+        ),
+      ],
+    );
+  }
+
+  /// 构建上传卡片
+  Widget _buildUploadCard() {
+    return Container(
+      margin: EdgeInsets.only(left: 12.w, top: 10.h, right: 12.w),
+      child: Stack(
+        // 不裁切超出区域的子组件,例如下面的顶部的图标
+        clipBehavior: Clip.none,
+        children: [
+          // 顶部的图标
+          Positioned(
+            top: -11.h,
+            right: 0,
+            child: Assets.images.iconIntimacyAnalyseUploadTop.image(
+              height: 63.h,
+              width: 103.w,
+            ),
+          ),
+          // 卡片背景
+          Container(
+            decoration: BoxDecoration(
+              image: DecorationImage(
+                image: Assets.images.bgIntimacyAnalyseUploadCard.provider(),
+                fit: BoxFit.fill,
+              ),
+            ),
+            child: Column(
+              // 左对齐
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                // 步骤1
+                Container(
+                  margin: EdgeInsets.only(left: 12.w, top: 16.h),
+                  child: _buildStepTitle(
+                    1,
+                    StringName.intimacyAnalyseStepTitleSelectImage,
+                  ),
+                ),
+                SizedBox(height: 4.h),
+                // 提示文字
+                Container(
+                  margin: EdgeInsets.only(left: 12.w),
+                  child: Text(
+                    StringName.intimacyAnalyseUploadCardTip,
+                    style: TextStyle(
+                      fontSize: 12.sp,
+                      fontWeight: FontWeight.w400,
+                      color: ColorName.black60,
+                    ),
+                  ),
+                ),
+                SizedBox(height: 16.h),
+                // 九宫格
+                Container(
+                  margin: EdgeInsets.only(left: 12.w, right: 12.w),
+                  padding: EdgeInsets.only(
+                    left: 12.w,
+                    top: 12.h,
+                    right: 12.w,
+                    bottom: 12.h,
+                  ),
+                  decoration: BoxDecoration(
+                    color: ColorName.white,
+                    borderRadius: BorderRadius.circular(16.r),
+                  ),
+                  child: UploadNineGrid(
+                    imageUrls: ["", "", "", "", "", "", ""],
+                    maxCount: 9,
+                    spacing: 8.0,
+                  ),
+                ),
+                SizedBox(height: 10.h),
+                // 当前的亲密关系
+                _buildRecommendIntimacy(),
+                SizedBox(height: 18.h),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 推荐的亲密关系
+  Widget _buildRecommendIntimacy() {
+    return Container(
+      width: double.maxFinite,
+      margin: EdgeInsets.only(left: 14.w, right: 14.w),
+      // 圆角背景
+      decoration: BoxDecoration(
+        // 渐变背景
+        gradient: LinearGradient(
+          colors: [
+            ColorName.bgIntimacyRelationColor1,
+            ColorName.bgIntimacyRelationColor2,
+          ],
+          begin: Alignment.centerLeft,
+          end: Alignment.centerRight,
+        ),
+        shape: BoxShape.rectangle,
+        border: Border.all(color: ColorName.white80, width: 1.w),
+        borderRadius: BorderRadius.all(Radius.circular(23.r)),
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Row(
+            children: [
+              SizedBox(width: 6.w),
+              // 用户亲密头像
+              IntimacyUserWidget(
+                width: 68.w,
+                height: 34.h,
+                avatarSize: 34.0,
+                avatarUrl1: '',
+                avatarUrl2: '',
+                avatarBorderWidth: 1.w,
+                loveSize: 19.w,
+              ),
+              SizedBox(width: 4.w),
+              Text(
+                StringName.mySelf,
+                style: TextStyle(
+                  color: ColorName.black80,
+                  fontSize: 11.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              SizedBox(width: 4.w),
+              Text(
+                StringName.and,
+                style: TextStyle(
+                  color: ColorName.black41,
+                  fontSize: 11.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              SizedBox(width: 4.w),
+              Text(
+                "小瑞",
+                style: TextStyle(
+                  color: ColorName.black80,
+                  fontSize: 11.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              SizedBox(width: 5.w),
+              GradientText(
+                // 渐变颜色
+                colors: [
+                  ColorName.intimacyRelationColor1,
+                  ColorName.intimacyRelationColor2,
+                ],
+                child: Text(
+                  StringName.intimacyRelation,
+                  style: TextStyle(
+                    fontSize: 11.sp,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ],
+          ),
+          // 切换按钮
+          Container(
+            padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 6.h),
+            margin: EdgeInsets.only(right: 8.w, top: 8.h, bottom: 8.h),
+            decoration: BoxDecoration(
+              color: ColorName.white,
+              borderRadius: BorderRadius.circular(22.r),
+            ),
+            child: Text(
+              StringName.intimacyAnalyseSwitchTaTest,
+              style: TextStyle(
+                color: ColorName.black80,
+                fontSize: 12.sp,
+                fontWeight: FontWeight.w400,
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 内容
+  Widget _buildContent() {
+    return Expanded(
+      child: SingleChildScrollView(
+        child: Column(children: [_buildUploadCard()]),
+      ),
+    );
+  }
+}

+ 31 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/widget/step_label_widget.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../../../resource/colors.gen.dart';
+
+/// 步骤标签组件
+class StepLabelWidget extends StatelessWidget {
+  /// 文字
+  final String label;
+
+  const StepLabelWidget({super.key, required this.label});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      // 不规则的圆角背景
+      decoration: BoxDecoration(
+        color: ColorName.bgStep,
+        borderRadius: BorderRadius.only(
+          topLeft: Radius.circular(6.r),
+          topRight: Radius.circular(10.r),
+          bottomLeft: Radius.circular(17.r),
+          bottomRight: Radius.circular(6.r),
+        ),
+      ),
+      padding: EdgeInsets.symmetric(horizontal: 7.w, vertical: 5.w),
+      // 文字
+      child: Text(label, style: TextStyle(color: ColorName.white)),
+    );
+  }
+}

+ 76 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_add_widget.dart

@@ -0,0 +1,76 @@
+import 'package:dotted_border/dotted_border.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/resource/colors.gen.dart';
+
+import '../../../../resource/assets.gen.dart';
+import '../../../../resource/string.gen.dart';
+import '../../../../utils/string_format_util.dart';
+
+/// 点击添加回调
+typedef OnClickAddCallback = void Function();
+
+/// 添加上传项组件
+class UploadAddWidget extends StatelessWidget {
+  /// 还可以上传多少个文件
+  final int residueCount;
+
+  /// 回调
+  final OnClickAddCallback? onClickAddCallback;
+
+  const UploadAddWidget({
+    super.key,
+    required this.residueCount,
+    this.onClickAddCallback,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      alignment: Alignment.center,
+      child: GestureDetector(
+        onTap: onClickAddCallback,
+        child: DottedBorder(
+          // 虚线颜色
+          color: ColorName.uploadAddDottedBorder,
+          // 线条宽度
+          strokeWidth: 1.0.w,
+          // 圆角矩形,要使用 RRect 类型,才能有效
+          borderType: BorderType.RRect,
+          // 圆角半径
+          radius: Radius.circular(12.r),
+          child: SizedBox(
+            // 宽度和高度,都撑满父组件
+            width: double.maxFinite,
+            height: double.maxFinite,
+            child: Column(
+              // 文字和图标都居中
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                // 上传图标
+                Assets.images.iconUploadAddSymbol.image(
+                  width: 23.w,
+                  height: 23.w,
+                ),
+                // 还可以上传多少个文件
+                Text(
+                  StringFormatUtil.formatStr(
+                    StringName.intimacyAnalyseUploadLimitTip,
+                    residueCount.toString(),
+                  ),
+                  style: TextStyle(
+                    color: ColorName.uploadAddText,
+                    fontSize: 12.sp,
+                    fontWeight: FontWeight.w400,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 174 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_item_widget.dart

@@ -0,0 +1,174 @@
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/resource/assets.gen.dart';
+
+import '../../../../resource/colors.gen.dart';
+import '../../../../resource/string.gen.dart';
+import '../../../../widget/rotate_image.dart';
+
+/// 上传状态
+enum UploadState {
+  /// 上传成功
+  success,
+
+  /// 上传中
+  uploading,
+
+  /// 上传失败
+  fail,
+}
+
+/// 点击了条目时回调
+typedef OnClickItemCallback = void Function();
+
+/// 点击了删除按钮时回调
+typedef OnClickDeleteCallback = void Function();
+
+/// 上传项组件,有3种状态,上传成功、上传中、上传失败
+class UploadItemWidget extends StatelessWidget {
+  /// 本地文件路径
+  final String filePath;
+
+  /// 上传状态
+  final UploadState uploadState;
+
+  /// 点击条目时回调
+  final OnClickItemCallback onClickItemCallback;
+
+  /// 点击删除时回调
+  final OnClickDeleteCallback onClickDeleteCallback;
+
+  const UploadItemWidget({
+    super.key,
+    required this.filePath,
+    required this.uploadState,
+    required this.onClickItemCallback,
+    required this.onClickDeleteCallback,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: onClickItemCallback,
+      child: Container(
+        decoration: BoxDecoration(borderRadius: BorderRadius.circular(12.5.r)),
+        child: Stack(
+          alignment: Alignment.center,
+          children: [
+            // 本地图片
+            Visibility(
+              visible: filePath.isNotEmpty,
+              child: Image.file(File(filePath), fit: BoxFit.cover),
+            ),
+            // 上传状态的遮罩层
+            _buildMaskByUploadStatus(uploadState),
+            // 删除按钮
+            Positioned(right: 3.w, top: 3.w, child: _buildDeleteBtn()),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 根据状态,构建遮罩
+  Widget _buildMaskByUploadStatus(UploadState uploadState) {
+    switch (uploadState) {
+      case UploadState.success:
+        // 上传成功,不显示遮罩
+        return SizedBox();
+      case UploadState.uploading:
+        // 上传中
+        return _buildUploadingMask();
+      case UploadState.fail:
+        // 上传失败
+        return _buildUploadFailMask();
+    }
+  }
+
+  /// 上传中状态的遮罩
+  Widget _buildUploadingMask() {
+    return Container(
+      // 宽度和高度,都撑满父组件
+      width: double.maxFinite,
+      height: double.maxFinite,
+      // 圆角背景
+      decoration: BoxDecoration(
+        color: ColorName.black50,
+        borderRadius: BorderRadius.circular(12.5.r),
+      ),
+      child: Column(
+        // 图标和文字都居中
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          // 图标
+          RotateImage(
+            image: Assets.images.iconUploading.image(width: 26.w, height: 26.w),
+          ),
+          // 间距
+          SizedBox(height: 6.h),
+          // 文字
+          Text(
+            StringName.intimacyAnalyseUploading,
+            style: TextStyle(
+              color: ColorName.white,
+              fontSize: 12.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 上传失败状态的遮罩
+  Widget _buildUploadFailMask() {
+    return Container(
+      // 宽度和高度,都撑满父组件
+      width: double.maxFinite,
+      height: double.maxFinite,
+      // 圆角背景
+      decoration: BoxDecoration(
+        color: ColorName.black50,
+        borderRadius: BorderRadius.circular(12.5.r),
+      ),
+      child: Column(
+        // 图标和文字都居中
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          // 图标
+          Assets.images.iconUploadFail.image(width: 26.w, height: 26.w),
+          // 间距
+          SizedBox(height: 6.h),
+          // 文字
+          Text(
+            StringName.intimacyAnalyseUploadFail,
+            style: TextStyle(
+              color: ColorName.white,
+              fontSize: 12.sp,
+              fontWeight: FontWeight.w700,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 删除按钮
+  Widget _buildDeleteBtn() {
+    return GestureDetector(
+      onTap: () {
+        // 执行删除
+        onClickDeleteCallback();
+      },
+      child: Container(
+        padding: EdgeInsets.all(3.w),
+        child: Assets.images.iconUploadDelete.image(height: 20.w, width: 20.w),
+      ),
+    );
+  }
+}

+ 90 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_nine_grid.dart

@@ -0,0 +1,90 @@
+import 'package:flutter/cupertino.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_add_widget.dart';
+import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_item_widget.dart';
+
+/// 上传九宫格
+class UploadNineGrid extends StatelessWidget {
+  /// 图片Url列表
+  final List<String> imageUrls;
+
+  /// 最大显示数量
+  final int maxCount;
+
+  /// 图片间距
+  final double spacing;
+
+  const UploadNineGrid({
+    super.key,
+    required this.imageUrls,
+    this.maxCount = 9,
+    this.spacing = 2.0,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GridView.builder(
+      // 去掉默认的Padding,默认会有一个默认的顶部padding大小
+      padding: EdgeInsets.zero,
+      // 包裹内容
+      shrinkWrap: true,
+      // 禁止滚动
+      physics: const NeverScrollableScrollPhysics(),
+      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+        // 宫格的列数
+        crossAxisCount: 3,
+        // 垂直方向的间距
+        crossAxisSpacing: spacing,
+        // 水平方向的间距
+        mainAxisSpacing: spacing,
+      ),
+      itemCount: _getItemCount(),
+      itemBuilder: (context, index) {
+        // 添加图片的条目
+        if (index >= imageUrls.length) {
+          return UploadAddWidget(
+            residueCount: maxCount - imageUrls.length,
+            onClickAddCallback: () {
+              // 上传图片
+            },
+          );
+        } else {
+          // 上传项
+          String url = imageUrls[index];
+          return _buildUploadItem(url);
+        }
+      },
+    );
+  }
+
+  /// 如果图片数量小于最大数量,则总数量,要添加1个添加图片的条目
+  bool _isNeedAddItem() {
+    return imageUrls.length < maxCount;
+  }
+
+  /// 获取宫格列表项的数量
+  int _getItemCount() {
+    final int itemCount;
+    // 如果图片数量小于最大数量,则总数量,要添加1个添加图片的条目
+    if (_isNeedAddItem()) {
+      itemCount = imageUrls.length + 1;
+    } else {
+      // 满了最大数量,则直接显示所有图片
+      itemCount = imageUrls.length;
+    }
+    return itemCount;
+  }
+
+  /// 上传项
+  Widget _buildUploadItem(String url) {
+    return UploadItemWidget(
+      filePath: url,
+      uploadState: UploadState.uploading,
+      onClickDeleteCallback: () {
+        // 删除图片
+      },
+      onClickItemCallback: () {
+        // 预览图片
+      },
+    );
+  }
+}

+ 91 - 0
lib/module/intimacy_analyse/widget/intimacy_user_widget.dart

@@ -0,0 +1,91 @@
+import 'package:flutter/cupertino.dart';
+
+import '../../../resource/assets.gen.dart';
+import '../../../resource/colors.gen.dart';
+import '../../../widget/avatar/avatar_image_widget.dart';
+
+/// 亲密用户组件,包含2个头像和中间一个心形图标
+class IntimacyUserWidget extends StatelessWidget {
+  /// 组件的宽度
+  final double width;
+
+  /// 组件的高度
+  final double height;
+
+  /// 头像的大小
+  final double avatarSize;
+
+  /// 头像的边框宽度
+  final double avatarBorderWidth;
+
+  /// 爱心的大小
+  final double loveSize;
+
+  /// 用户1的头像
+  final String avatarUrl1;
+
+  /// 用户2的头像
+  final String avatarUrl2;
+
+  const IntimacyUserWidget({
+    super.key,
+    required this.width,
+    required this.height,
+    required this.avatarSize,
+    required this.avatarBorderWidth,
+    required this.loveSize,
+    required this.avatarUrl1,
+    required this.avatarUrl2,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: width,
+      height: height,
+      child: Stack(
+        // 默认内容都居中
+        alignment: Alignment.center,
+        children: [
+          // 用户1
+          Positioned(
+            left: 0,
+            child: _buildAvatar(
+              avatarUrl1,
+              defaultImage: Assets.images.iconDefaultAvatarMale.image().image,
+            ),
+          ),
+          // 用户2
+          Positioned(
+            // 距离左侧的距离
+            left: avatarSize - (avatarBorderWidth * 2),
+            top: 0,
+            child: _buildAvatar(
+              avatarUrl2,
+              defaultImage: Assets.images.iconDefaultAvatarFemale.image().image,
+            ),
+          ),
+          // 爱心
+          Positioned(
+            child: Assets.images.iconIntimacyAnalyseLove.image(
+              width: loveSize,
+              height: loveSize,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 圆形头像
+  Widget _buildAvatar(String? imageUrl, {ImageProvider? defaultImage}) {
+    return CircleAvatarWidget(
+      imageUrl: imageUrl,
+      placeholderImage: defaultImage,
+      borderColor: ColorName.white,
+      borderWidth: avatarBorderWidth,
+      backgroundColor: ColorName.white,
+      size: avatarSize,
+    );
+  }
+}

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

@@ -120,6 +120,10 @@ class $AssetsImagesGen {
     'assets/images/bg_intimacy_analyse_report_preview_mask.webp',
   );
 
+  /// File path: assets/images/bg_intimacy_analyse_upload_card.webp
+  AssetGenImage get bgIntimacyAnalyseUploadCard =>
+      const AssetGenImage('assets/images/bg_intimacy_analyse_upload_card.webp');
+
   /// File path: assets/images/bg_keyboard.webp
   AssetGenImage get bgKeyboard =>
       const AssetGenImage('assets/images/bg_keyboard.webp');
@@ -420,6 +424,11 @@ class $AssetsImagesGen {
   AssetGenImage get iconIntimacyAnalyseUnlock =>
       const AssetGenImage('assets/images/icon_intimacy_analyse_unlock.webp');
 
+  /// File path: assets/images/icon_intimacy_analyse_upload_top.webp
+  AssetGenImage get iconIntimacyAnalyseUploadTop => const AssetGenImage(
+    'assets/images/icon_intimacy_analyse_upload_top.webp',
+  );
+
   /// File path: assets/images/icon_keyboard_banner.webp
   AssetGenImage get iconKeyboardBanner =>
       const AssetGenImage('assets/images/icon_keyboard_banner.webp');
@@ -699,6 +708,22 @@ class $AssetsImagesGen {
   AssetGenImage get iconTicketDialogButton =>
       const AssetGenImage('assets/images/icon_ticket_dialog_button.webp');
 
+  /// File path: assets/images/icon_upload_add_symbol.webp
+  AssetGenImage get iconUploadAddSymbol =>
+      const AssetGenImage('assets/images/icon_upload_add_symbol.webp');
+
+  /// File path: assets/images/icon_upload_delete.webp
+  AssetGenImage get iconUploadDelete =>
+      const AssetGenImage('assets/images/icon_upload_delete.webp');
+
+  /// File path: assets/images/icon_upload_fail.webp
+  AssetGenImage get iconUploadFail =>
+      const AssetGenImage('assets/images/icon_upload_fail.webp');
+
+  /// File path: assets/images/icon_uploading.webp
+  AssetGenImage get iconUploading =>
+      const AssetGenImage('assets/images/icon_uploading.webp');
+
   /// File path: assets/images/icon_wechat.webp
   AssetGenImage get iconWechat =>
       const AssetGenImage('assets/images/icon_wechat.webp');
@@ -735,6 +760,7 @@ class $AssetsImagesGen {
     bgIntimacyAnalyseReportPreview,
     bgIntimacyAnalyseReportPreviewBubble,
     bgIntimacyAnalyseReportPreviewMask,
+    bgIntimacyAnalyseUploadCard,
     bgKeyboard,
     bgKeyboardEasyReply,
     bgKeyboardIntimacyAnalyze,
@@ -807,6 +833,7 @@ class $AssetsImagesGen {
     iconIntimacyAnalyseReportPreviewLove,
     iconIntimacyAnalyseReportPreviewTitle,
     iconIntimacyAnalyseUnlock,
+    iconIntimacyAnalyseUploadTop,
     iconKeyboardBanner,
     iconKeyboardBannerClose,
     iconKeyboardCurrentCharacterTitle,
@@ -876,6 +903,10 @@ class $AssetsImagesGen {
     iconTabMineSelected,
     iconTabMineUnselect,
     iconTicketDialogButton,
+    iconUploadAddSymbol,
+    iconUploadDelete,
+    iconUploadFail,
+    iconUploading,
     iconWechat,
     iconWechatPayment,
     iconWechatScanPayment,

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

@@ -16,6 +16,15 @@ class ColorName {
   /// Color: #FFF5F6F8
   static const Color bgColorPrimary = Color(0xFFF5F6F8);
 
+  /// Color: #FFE2DBFF
+  static const Color bgIntimacyRelationColor1 = Color(0xFFE2DBFF);
+
+  /// Color: #FFEBF0FF
+  static const Color bgIntimacyRelationColor2 = Color(0xFFEBF0FF);
+
+  /// Color: #FF7B7DFF
+  static const Color bgStep = Color(0xFF7B7DFF);
+
   /// Color: #FF000000
   static const Color black = Color(0xFF000000);
 
@@ -40,6 +49,9 @@ class ColorName {
   /// Color: #66000000
   static const Color black40 = Color(0x66000000);
 
+  /// Color: #69000000
+  static const Color black41 = Color(0x69000000);
+
   /// Color: #73000000
   static const Color black45 = Color(0x73000000);
 
@@ -100,6 +112,12 @@ class ColorName {
   /// Color: #FF996DFF
   static const Color inputCursor = Color(0xFF996DFF);
 
+  /// Color: #FF5521F6
+  static const Color intimacyRelationColor1 = Color(0xFF5521F6);
+
+  /// Color: #FFC456F5
+  static const Color intimacyRelationColor2 = Color(0xFFC456F5);
+
   /// Color: #FFFFFFFF
   static const Color inverseTextColor = Color(0xFFFFFFFF);
 
@@ -121,9 +139,21 @@ class ColorName {
   /// Color: #666666
   static const Color secondaryTextColor = Color(0xFF666666);
 
+  /// Color: #FF202020
+  static const Color stepTitleColor1 = Color(0xFF202020);
+
+  /// Color: #FF523A68
+  static const Color stepTitleColor2 = Color(0xFF523A68);
+
   /// Color: #00FFFFFF
   static const Color transparent = Color(0x00FFFFFF);
 
+  /// Color: #FF9F8DC5
+  static const Color uploadAddDottedBorder = Color(0xFF9F8DC5);
+
+  /// Color: #B2755BAB
+  static const Color uploadAddText = Color(0xB2755BAB);
+
   /// Color: #FFFFFFFF
   static const Color white = Color(0xFFFFFFFF);
 

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

@@ -157,6 +157,18 @@ class StringName {
   static final String keyboardGuideTaReply3 = 'keyboard_guide_ta_reply3'.tr; // 我先去吃饭了,一会聊
   static final String intimacyAnalyseTabReport = 'intimacy_analyse_tab_report'.tr; // 亲密分析报告
   static final String intimacyAnalyseTabScreenshotReply = 'intimacy_analyse_tab_screenshot_reply'.tr; // 截图回复
+  static final String intimacyAnalyse = 'intimacy_analyse'.tr; // 亲密度分析
+  static final String intimacyAnalyseUploadCardTip = 'intimacy_analyse_upload_card_tip'.tr; // 上传与对方的聊天记录,揭秘文字后的真实想法
+  static final String intimacyAnalyseSwitchTaTest = 'intimacy_analyse_switch_ta_test'.tr; // 换TA测测
+  static final String intimacyAnalyseUploadLimitTip = 'intimacy_analyse_upload_limit_tip'.tr; // 还可上传%s张
+  static final String intimacyAnalyseUploadFail = 'intimacy_analyse_upload_fail'.tr; // 上传失败
+  static final String intimacyAnalyseUploading = 'intimacy_analyse_uploading'.tr; // 上传中
+  static final String intimacyAnalyseStep = 'intimacy_analyse_step'.tr; // STEP%s
+  static final String intimacyAnalyseStepTitleSelectImage = 'intimacy_analyse_step_title_select_image'.tr; // 选择你的图片
+  static final String intimacyAnalyseStepTitleSelectPredictionDirection = 'intimacy_analyse_step_title_select_prediction_direction'.tr; // 选择预测方向
+  static final String mySelf = 'my_self'.tr; // 自己
+  static final String and = 'and'.tr; // 与
+  static final String intimacyRelation = 'intimacy_relation'.tr; // 亲密关系
   static final String nextStep = 'next_step'.tr; // 下一步
   static final String recently = 'recently'.tr; // 最近
 }
@@ -319,6 +331,18 @@ class StringMultiSource {
       'keyboard_guide_ta_reply3': '我先去吃饭了,一会聊',
       'intimacy_analyse_tab_report': '亲密分析报告',
       'intimacy_analyse_tab_screenshot_reply': '截图回复',
+      'intimacy_analyse': '亲密度分析',
+      'intimacy_analyse_upload_card_tip': '上传与对方的聊天记录,揭秘文字后的真实想法',
+      'intimacy_analyse_switch_ta_test': '换TA测测',
+      'intimacy_analyse_upload_limit_tip': '还可上传%s张',
+      'intimacy_analyse_upload_fail': '上传失败',
+      'intimacy_analyse_uploading': '上传中',
+      'intimacy_analyse_step': 'STEP%s',
+      'intimacy_analyse_step_title_select_image': '选择你的图片',
+      'intimacy_analyse_step_title_select_prediction_direction': '选择预测方向',
+      'my_self': '自己',
+      'and': '与',
+      'intimacy_relation': '亲密关系',
       'next_step': '下一步',
       'recently': '最近',
     },

+ 5 - 0
lib/router/app_page_arguments.dart

@@ -0,0 +1,5 @@
+/// App页面跳转参数
+abstract class AppPageArguments {
+  /// 选择的图片资源列表
+  static const selectedAssetList = "selectedAssetList";
+}

+ 8 - 0
lib/router/app_pages.dart

@@ -27,6 +27,8 @@ import '../module/character_custom/detail/character_custom_detail_controller.dar
 import '../module/character_custom/list/character_custom_list_page.dart';
 import '../module/feedback/feedback_page.dart';
 import '../module/intimacy_analyse/analyse_report/intimacy_analyse_report_view_controller.dart';
+import '../module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart';
+import '../module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart';
 import '../module/intimacy_analyse/screenshot_reply/intimacy_analyse_screenshot_reply_controller.dart';
 import '../module/keyboard_guide/keyboard_guide_controller.dart';
 import '../module/keyboard_guide/keyboard_guide_page.dart';
@@ -63,6 +65,9 @@ abstract class RoutePath {
 
   // 亲密度分析页
   static const intimacyAnalyse = '/intimacyAnalyse';
+
+  // 亲密度分析上传页
+  static const intimacyAnalyseUpload = '/intimacyAnalyseUpload';
 }
 
 class AppBinding extends Bindings {
@@ -90,6 +95,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<IntimacyAnalyseController>());
     lazyPut(() => getIt.get<IntimacyAnalyseReportController>());
     lazyPut(() => getIt.get<IntimacyAnalyseScreenshotReplyController>());
+    lazyPut(() => getIt.get<IntimacyAnalyseUploadController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -120,4 +126,6 @@ final generalPages = [
   GetPage(name: RoutePath.keyboardGuide, page: () => KeyboardGuidePage()),
   // 亲密度报告页
   GetPage(name: RoutePath.intimacyAnalyse, page: () => IntimacyAnalysePage()),
+  // 亲密度分析上传页
+  GetPage(name: RoutePath.intimacyAnalyseUpload, page: () => IntimacyAnalyseUploadPage()),
 ];

+ 32 - 31
lib/utils/image_picker_util.dart

@@ -9,41 +9,42 @@ class ImagePickerUtil {
   static final Color _themeColor = ColorName.colorBrand;
 
   /// 选择图片
-  static Future<void> pickImage(
+  static Future<List<AssetEntity>> pickImage(
     BuildContext context, {
     required int maxAssetsCount,
     List<AssetEntity> selectedAssets = const [],
   }) async {
-    await AssetPicker.pickAssets(
-      context,
-      pickerConfig: AssetPickerConfig(
-        // 最大选择数量
-        maxAssets: maxAssetsCount,
-        // 已选择的图片列表
-        selectedAssets: selectedAssets,
-        // 自定义按钮文字
-        textDelegate: const CustomChineseDelegate(),
-        // 主题
-        pickerTheme: AssetPicker.themeData(
-          // 主题色
-          _themeColor,
-          // 深色默认
-          light: false,
-        ),
-        // 设置为不能预览的模式
-        specialPickerType: SpecialPickerType.noPreview,
-        // 只能选取图片类型
-        requestType: RequestType.image,
-        // 关闭拽托选择
-        dragToSelect: false,
-        // 实现最近相册的名字显示
-        pathNameBuilder:
-            (AssetPathEntity path) => switch (path) {
-              final p when p.isAll => StringName.recently,
-              _ => path.name,
-            },
-      ),
-    );
+    return await AssetPicker.pickAssets(
+          context,
+          pickerConfig: AssetPickerConfig(
+            // 最大选择数量
+            maxAssets: maxAssetsCount,
+            // 已选择的图片列表
+            selectedAssets: selectedAssets,
+            // 自定义按钮文字
+            textDelegate: const CustomChineseDelegate(),
+            // 主题
+            pickerTheme: AssetPicker.themeData(
+              // 主题色
+              _themeColor,
+              // 深色默认
+              light: false,
+            ),
+            // 设置为不能预览的模式
+            specialPickerType: SpecialPickerType.noPreview,
+            // 只能选取图片类型
+            requestType: RequestType.image,
+            // 关闭拽托选择
+            dragToSelect: false,
+            // 实现最近相册的名字显示
+            pathNameBuilder:
+                (AssetPathEntity path) => switch (path) {
+                  final p when p.isAll => StringName.recently,
+                  _ => path.name,
+                },
+          ),
+        ) ??
+        [];
   }
 
   /// 跳转到图片预览

+ 9 - 0
lib/utils/string_format_util.dart

@@ -0,0 +1,9 @@
+import 'package:sprintf/sprintf.dart';
+
+/// 字符串格式化工具类
+class StringFormatUtil {
+  /// 格式化字符串
+  static String formatStr(String str, String format) {
+    return sprintf(str, [format]);
+  }
+}

+ 36 - 0
lib/widget/gradient_text.dart

@@ -0,0 +1,36 @@
+import 'package:flutter/cupertino.dart';
+
+/// 渐变色文字
+class GradientText extends StatelessWidget {
+  /// 渐变颜色参数
+  final List<Color> colors;
+
+  final AlignmentGeometry begin;
+  final AlignmentGeometry end;
+
+  final Widget child;
+
+  const GradientText({
+    super.key,
+    required this.colors,
+    this.begin = Alignment.centerLeft,
+    this.end = Alignment.centerRight,
+    required this.child,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return ShaderMask(
+      // 线性渐变
+      shaderCallback:
+          (bounds) => LinearGradient(
+            colors: colors,
+            begin: begin,
+            end: end,
+          ).createShader(bounds),
+      blendMode: BlendMode.srcIn,
+      // 内容
+      child: child,
+    );
+  }
+}

+ 51 - 0
lib/widget/rotate_image.dart

@@ -0,0 +1,51 @@
+import 'package:flutter/cupertino.dart';
+
+/// 旋转图片组件
+class RotateImage extends StatefulWidget {
+  /// 图片组件
+  final Image image;
+
+  /// 动画时长
+  final Duration duration;
+
+  const RotateImage({
+    super.key,
+    required this.image,
+    this.duration = const Duration(milliseconds: 1000),
+  });
+
+  @override
+  State<StatefulWidget> createState() {
+    return _RotateImageState();
+  }
+}
+
+class _RotateImageState extends State<RotateImage>
+    with SingleTickerProviderStateMixin {
+  /// 动画控制器
+  late AnimationController _animationController;
+
+  @override
+  void initState() {
+    super.initState();
+    _animationController = AnimationController(duration: widget.duration, vsync: this)
+      // 无限循环
+      ..repeat();
+  }
+
+  @override
+  void dispose() {
+    // 释放资源
+    _animationController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // 动画组件
+    return RotationTransition(
+      turns: _animationController,
+      child: widget.image,
+    );
+  }
+}

+ 1 - 1
pubspec.lock

@@ -1240,7 +1240,7 @@ packages:
     source: hosted
     version: "1.10.1"
   sprintf:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: sprintf
       sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"

+ 3 - 0
pubspec.yaml

@@ -82,6 +82,9 @@ dependencies:
   # url跳转
   url_launcher: ^6.3.1
 
+  # 字符串替换
+  sprintf: ^7.0.0
+
   #android日志打印
   atmob_logging:
     version: ^0.0.5