瀏覽代碼

Merge remote-tracking branch 'origin/v1.0.0' into v1.0.0

云天逵 7 月之前
父節點
當前提交
5434acc55c
共有 31 個文件被更改,包括 711 次插入358 次删除
  1. 16 0
      assets/string/base/string.xml
  2. 20 10
      lib/data/api/response/intimacy_analyze_response.dart
  3. 4 4
      lib/data/api/response/intimacy_analyze_response.g.dart
  4. 6 2
      lib/data/bean/option_select_item.dart
  5. 6 1
      lib/data/bean/option_select_item.g.dart
  6. 0 10
      lib/data/repository/file_upload_repository.dart
  7. 8 28
      lib/module/intimacy_analyse/intimacy_analyse_upload/dialog/direction/custom_direction_edit_view.dart
  8. 65 33
      lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart
  9. 61 52
      lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart
  10. 3 2
      lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_nine_grid.dart
  11. 12 3
      lib/module/intimacy_analyse/screenshot_reply/conversation_analysis/conversation_analysis_controller.dart
  12. 6 31
      lib/module/intimacy_analyse/screenshot_reply/conversation_analysis/conversation_analysis_view.dart
  13. 21 6
      lib/module/intimacy_analyse/screenshot_reply/scan_image_reply/scan_image_reply_controller.dart
  14. 22 66
      lib/module/intimacy_analyse/screenshot_reply/scan_image_reply/scan_image_reply_view.dart
  15. 133 25
      lib/module/intimacy_analyse/widget/intimacy_analyse_report_widget.dart
  16. 24 0
      lib/resource/string.gen.dart
  17. 5 0
      lib/utils/string_format_util.dart
  18. 15 4
      lib/utils/upload/upload_file_manager.dart
  19. 22 6
      lib/widget/animated_progress_bar.dart
  20. 121 0
      lib/widget/gradient_btn.dart
  21. 1 0
      plugins/keyboard_android/android/src/main/assets/lottie/anim_intimacy_value_love.json
  22. 1 0
      plugins/keyboard_android/android/src/main/assets/lottie/anim_join_vip_btn.json
  23. 41 22
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/AiKeyboardCommonPanelComponent.kt
  24. 32 21
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/AiKeyboardProloguePanelComponent.kt
  25. 1 1
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/QuickSwitchComponent.kt
  26. 7 1
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/ToolBarComponent.kt
  27. 1 1
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/KeyboardSelectViewBinder.kt
  28. 5 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/constant/Constants.kt
  29. 5 5
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/KeyboardSelectModel.kt
  30. 17 9
      plugins/keyboard_android/android/src/main/res/layout/component_tool_bar.xml
  31. 30 15
      plugins/keyboard_android/android/src/main/res/layout/component_vip_page.xml

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

@@ -264,6 +264,8 @@
     <string name="intimacy_analyse_upload_fail">上传失败</string>
     <string name="intimacy_analyse_uploading">上传中</string>
 
+    <string name="intimacy_analyse_upload_fail_please_retry">上传失败,请重试</string>
+
     <string name="intimacy_analyse_step">STEP%s</string>
 
     <string name="intimacy_analyse_step_title_select_image">选择你的图片</string>
@@ -322,9 +324,23 @@
 
     <string name="no_choose_mode_tip">请选择模式</string>
     <string name="no_upload_screenshot_tip">请上传截图</string>
+    <string name="no_choose_intimacy_ta_tip">请选择亲密对象</string>
+    <string name="no_choose_prediction_direction_tip">请选择预测方向</string>
+    <string name="no_choose_ai_model_tip">请选择AI模型</string>
 
     <string name="no_upload_conversation_image_tip">请上传聊天记录图片</string>
 
+    <string name="intimacy_analyse_ing">分析中</string>
+
+    <string name="intimacy_interaction">互动好感度</string>
+    <string name="intimacy_topic">话题好感度</string>
+    <string name="intimacy_respond">情绪回应</string>
+    <string name="intimacy_intimacy_ratio">亲密词占比</string>
+
+    <string name="intimacy_intimacy_emotion_need">情感需求</string>
+    <string name="intimacy_intimacy_chat_strategy">聊天策略</string>
+    <string name="intimacy_intimacy_summary">总结</string>
+
     <string name="preview">预览</string>
 
     <string name="retry">再试试</string>

+ 20 - 10
lib/data/api/response/intimacy_analyze_response.dart

@@ -37,21 +37,31 @@ class IntimacyAnalyzeResponse {
   @JsonKey(name: "need")
   String? need;
 
-  /// 短期聊天策略
-  @JsonKey(name: "shortChat")
-  String? shortChat;
-
-  /// 长期聊天策略
-  @JsonKey(name: "longChat")
-  String? longChat;
+  /// 聊天策略
+  @JsonKey(name: "chatStrategy")
+  String? chatStrategy;
 
   /// 总结
   @JsonKey(name: "summary")
   String? summary;
 
-  IntimacyAnalyzeResponse(this.emotion, this.intimacyPhase, this.interaction,
-      this.topic, this.respond, this.intimacyRatio, this.direction, this.need,
-      this.shortChat, this.longChat, this.summary);
+  /// 预测方向的名称(服务端没返回,客户端手动取用户选择的来赋值)
+  @JsonKey(name: "directionName")
+  String? directionName;
+
+  IntimacyAnalyzeResponse(
+    this.emotion,
+    this.intimacyPhase,
+    this.interaction,
+    this.topic,
+    this.respond,
+    this.intimacyRatio,
+    this.direction,
+    this.need,
+    this.chatStrategy,
+    this.summary,
+    this.directionName,
+  );
 
   factory IntimacyAnalyzeResponse.fromJson(Map<String, dynamic> json) =>
       _$IntimacyAnalyzeResponseFromJson(json);

+ 4 - 4
lib/data/api/response/intimacy_analyze_response.g.dart

@@ -17,9 +17,9 @@ IntimacyAnalyzeResponse _$IntimacyAnalyzeResponseFromJson(
   json['intimacyRatio'] as String?,
   json['direction'] as String?,
   json['need'] as String?,
-  json['shortChat'] as String?,
-  json['longChat'] as String?,
+  json['chatStrategy'] as String?,
   json['summary'] as String?,
+  json['directionName'] as String?,
 );
 
 Map<String, dynamic> _$IntimacyAnalyzeResponseToJson(
@@ -33,7 +33,7 @@ Map<String, dynamic> _$IntimacyAnalyzeResponseToJson(
   'intimacyRatio': instance.intimacyRatio,
   'direction': instance.direction,
   'need': instance.need,
-  'shortChat': instance.shortChat,
-  'longChat': instance.longChat,
+  'chatStrategy': instance.chatStrategy,
   'summary': instance.summary,
+  'directionName': instance.directionName,
 };

+ 6 - 2
lib/data/bean/option_select_item.dart

@@ -5,15 +5,19 @@ part 'option_select_item.g.dart';
 /// 选项选择实体类
 @JsonSerializable()
 class OptionSelectItem {
-  // 名称
+  // 名称,只用来渲染
   @JsonKey(name: 'name')
   String name;
 
+  // 数据,用来提交给服务端的数据
+  @JsonKey(name: 'value')
+  String value;
+
   // 是否选中
   @JsonKey(name: 'selected')
   bool selected;
 
-  OptionSelectItem(this.name, {this.selected = false});
+  OptionSelectItem(this.name, this.value, {this.selected = false});
 
   factory OptionSelectItem.fromJson(Map<String, dynamic> json) =>
       _$OptionSelectItemFromJson(json);

+ 6 - 1
lib/data/bean/option_select_item.g.dart

@@ -9,8 +9,13 @@ part of 'option_select_item.dart';
 OptionSelectItem _$OptionSelectItemFromJson(Map<String, dynamic> json) =>
     OptionSelectItem(
       json['name'] as String,
+      json['value'] as String,
       selected: json['selected'] as bool? ?? false,
     );
 
 Map<String, dynamic> _$OptionSelectItemToJson(OptionSelectItem instance) =>
-    <String, dynamic>{'name': instance.name, 'selected': instance.selected};
+    <String, dynamic>{
+      'name': instance.name,
+      'value': instance.value,
+      'selected': instance.selected,
+    };

+ 0 - 10
lib/data/repository/file_upload_repository.dart

@@ -1,9 +1,6 @@
 import 'dart:io';
 
-import 'package:dio/dio.dart';
 import 'package:injectable/injectable.dart';
-import 'package:keyboard/utils/file_util.dart';
-
 import '../../utils/http_handler.dart';
 import '../api/atmob_file_api.dart';
 import '../api/request/upload_request.dart';
@@ -21,13 +18,6 @@ class FileUploadRepository {
 
   /// 上传图片
   Future<UploadResultBean> uploadImage({required File file}) async {
-    // 包装为 MultipartFile
-    // MultipartFile multipartFile = await MultipartFile.fromFile(
-    //   // 文件路径
-    //   file.path,
-    //   // 文件名
-    //   filename: FileUtil.getFileName(file)
-    // );
     return atmobFileApi
         .uploadImage(UploadRequest(file: file))
         .then(HttpHandler.handle(false));

+ 8 - 28
lib/module/intimacy_analyse/intimacy_analyse_upload/dialog/direction/custom_direction_edit_view.dart

@@ -5,6 +5,7 @@ import 'package:keyboard/base/base_view.dart';
 import 'package:keyboard/module/intimacy_analyse/intimacy_analyse_upload/dialog/direction/custom_direction_edit_controller.dart';
 import 'package:keyboard/resource/assets.gen.dart';
 import 'package:keyboard/resource/colors.gen.dart';
+import 'package:keyboard/widget/gradient_btn.dart';
 
 import '../../../../../resource/string.gen.dart';
 
@@ -151,34 +152,13 @@ class CustomDirectionEditView extends BaseView<CustomDirectionEditController> {
 
   /// 保存按钮
   Widget _buildSaveBtn() {
-    return GestureDetector(
-      onTap: () {
-        controller.doOnSave(onSaveCallback);
-      },
-      child: Container(
-        padding: EdgeInsets.symmetric(vertical: 14.h),
-        width: double.maxFinite,
-        decoration: ShapeDecoration(
-          // 渐变背景
-          gradient: LinearGradient(
-            colors: [ColorName.purpleGradient3, ColorName.purpleGradient4],
-            begin: Alignment.centerLeft,
-            end: Alignment.centerRight,
-          ),
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(50.r),
-          ),
-        ),
-        child: Center(
-          child: Text(
-            StringName.save,
-            style: TextStyle(
-              color: ColorName.white,
-              fontSize: 16.sp,
-              fontWeight: FontWeight.w500,
-            ),
-          ),
-        ),
+    return SizedBox(
+      width: double.infinity,
+      child: GradientTextBtn(
+        StringName.save,
+        onPressed: () {
+          controller.doOnSave(onSaveCallback);
+        },
       ),
     );
   }

+ 65 - 33
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart

@@ -8,6 +8,7 @@ import 'package:keyboard/data/api/request/intimacy_analyze_request.dart';
 import 'package:keyboard/data/bean/upload_info.dart';
 import 'package:keyboard/data/repository/account_repository.dart';
 import 'package:keyboard/resource/string.gen.dart';
+import 'package:keyboard/utils/error_handler.dart';
 import 'package:keyboard/utils/toast_util.dart';
 import 'package:keyboard/utils/upload/upload_scene_type.dart';
 import 'package:wechat_assets_picker/wechat_assets_picker.dart';
@@ -18,8 +19,10 @@ import '../../../data/bean/keyboard_info.dart';
 import '../../../data/bean/option_select_config.dart';
 import '../../../data/bean/option_select_item.dart';
 import '../../../data/repository/intimacy_analyze_repository.dart';
+import '../../../dialog/loading_dialog.dart';
 import '../../../router/app_page_arguments.dart';
 import '../../../utils/atmob_log.dart';
+import '../../../utils/http_handler.dart';
 import '../../../utils/image_picker_util.dart';
 import '../../../utils/intimacy_analyze_config_helper.dart';
 import '../../../utils/upload/upload_file_manager.dart';
@@ -70,39 +73,21 @@ class IntimacyAnalyseUploadController extends BaseController {
   /// 是否可以自定义方向
   RxBool isCanCustomDirection = false.obs;
 
-  /// 报告的markdown数据
-  RxString reportMarkdownData =
-      '''
-  **性格匹配度**
-
-  ● 互补型:一方外向活泼,另一方沉稳内敛,形成动态平衡。
-  ● 相似型:三观一致,兴趣重叠,减少摩擦但需警惕新鲜感流失。
-  ● 关键结论:差异是火花的来源,但核心价值观需一致(如家庭观、金钱观)。
-
-  **沟通模式分析**
-
-  ● 语言风格:幽默调侃型 vs 理性分析型 → 需找到共同表达方式。
-  ● 冲突解决:回避型 vs 直面型 → 建议建立“冷静-沟通”机制。
-  ● 情感需求:一方需要高频互动,另一方偏好独立空间 → 需协商平衡点。
-
-  **爱情语言测试**
-
-  ● 根据盖瑞·查普曼的“五种爱之语”理论,分析双方的情感表达偏好:
-  ● 你的主要爱语:肯定的言辞(如情话、鼓励)
-  '''.obs;
-
-  // TODO hezihao,测试报告生成中
-  // RxString reportMarkdownData = "".obs;
-
   /// 报告是否已解锁
   Rx<bool> isReportUnlock = true.obs;
 
+  /// 报告是否生成中
+  Rx<bool> isReportCreating = false.obs;
+
   /// 当前键盘信息
   Rxn<KeyboardInfo> currentKeyboardInfo = Rxn();
 
   /// 当前选择的预测方向
   Rx<String> currentDirectionOption = "".obs;
 
+  /// 亲密分析结果
+  Rxn<IntimacyAnalyzeResponse> intimacyAnalyzeResult = Rxn();
+
   IntimacyAnalyseUploadController(
     this.intimacyAnalyzeRepository,
     this.intimacyAnalyzeConfigHelper,
@@ -139,7 +124,7 @@ class IntimacyAnalyseUploadController extends BaseController {
           var iconUrl = item.iconUrl ?? "";
           var optionList =
               item.options?.map((value) {
-                return OptionSelectItem(value);
+                return OptionSelectItem(value, value);
               }).toList() ??
               [];
           return OptionSelectConfig(title, iconUrl, optionList);
@@ -198,6 +183,9 @@ class IntimacyAnalyseUploadController extends BaseController {
             // 上传信息更新时回调,更新UI
             _updateUploadInfo(uploadInfo);
           },
+          onUploadFailCallback: (error) {
+            ErrorHandler.toastError(error);
+          },
         ),
       );
       uploadInfoList.refresh();
@@ -265,12 +253,14 @@ class IntimacyAnalyseUploadController extends BaseController {
       if (directionOptionSelectConfigList.isEmpty || !firstRowConfigIsCustom) {
         // 没有自定义配置,添加一个
         directionOptionSelectConfigList[0] = OptionSelectConfig("", "", [
-          OptionSelectItem(customDirection),
+          OptionSelectItem(customDirection, customDirection),
         ], isCustom: true);
       } else {
         // 已经有一个,在原来的基础上,添加1个
         OptionSelectConfig oldConfig = directionOptionSelectConfigList[0];
-        oldConfig.options.add(OptionSelectItem(customDirection));
+        oldConfig.options.add(
+          OptionSelectItem(customDirection, customDirection),
+        );
       }
       directionOptionSelectConfigList.refresh();
     });
@@ -320,13 +310,55 @@ class IntimacyAnalyseUploadController extends BaseController {
     // 当前选择的Ai模型
     String aiModel = currentAiModel.value?.value ?? "";
 
-    // 分析亲密度
-    IntimacyAnalyzeResponse response = await intimacyAnalyzeRepository
-        .getIntimacyAnalyze(
-          IntimacyAnalyzeRequest(imageList, keyboardId, direction, aiModel),
-        );
-    AtmobLog.d(tag, "分析亲密度 => ${response.toJson()}");
+    if (imageList.isEmpty) {
+      ToastUtil.show(StringName.noUploadScreenshotTip);
+      return;
+    }
+    if (keyboardId.isEmpty) {
+      ToastUtil.show(StringName.noChooseIntimacyTaTip);
+      return;
+    }
+    if (direction.isEmpty) {
+      ToastUtil.show(StringName.noChoosePredictionDirectionTip);
+      return;
+    }
+    if (aiModel.isEmpty) {
+      ToastUtil.show(StringName.noChooseAiModelTip);
+      return;
+    }
+
+    // 切换到报告生成中
     isUploadPage.value = false;
+    isReportCreating.value = true;
+
+    try {
+      // 分析亲密度
+      IntimacyAnalyzeResponse response = await intimacyAnalyzeRepository
+          .getIntimacyAnalyze(
+            IntimacyAnalyzeRequest(imageList, keyboardId, direction, aiModel),
+          );
+      // 设置预测方向的名称
+      response.directionName = currentDirectionOption.value;
+
+      AtmobLog.d(tag, "分析亲密度 => ${response.toJson()}");
+
+      isReportCreating.value = false;
+      intimacyAnalyzeResult.value = response;
+    } catch (error) {
+      isReportCreating.value = false;
+      AtmobLog.e(tag, error.toString());
+      if (error is ServerErrorException) {
+        // 需要Vip权限
+        if (error.code == 1005) {
+          ToastUtil.show(error.message);
+          StorePage.start();
+        } else {
+          ToastUtil.show(error.message);
+        }
+      } else {
+        ErrorHandler.toastError(error);
+      }
+    }
   }
 
   /// 换Ta测测

+ 61 - 52
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart

@@ -278,8 +278,10 @@ class IntimacyAnalyseUploadPage
         children: [
           SizedBox(height: 12.h),
           IntimacyAnalyseReportWidget(
-            reportContent: controller.reportMarkdownData.value,
+            // 分析结果
+            intimacyAnalyzeResult: controller.intimacyAnalyzeResult.value,
             unlock: controller.isReportUnlock.value,
+            isReportCreating: controller.isReportCreating.value,
           ),
         ],
       );
@@ -321,7 +323,7 @@ class IntimacyAnalyseUploadPage
                     SizedBox(width: 2.w),
                     // 结果文字
                     Text(
-                      "未来",
+                      controller.currentDirectionOption.value,
                       style: TextStyle(
                         color: ColorName.black80,
                         fontSize: 13.sp,
@@ -351,46 +353,48 @@ class IntimacyAnalyseUploadPage
 
   /// Ai模型结果
   Widget _buildAiModelResult() {
-    return Expanded(
-      child: Container(
-        padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 10.w),
-        decoration: BoxDecoration(
-          color: ColorName.white,
-          borderRadius: BorderRadius.circular(14.r),
-        ),
-        child: Row(
-          children: [
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                // 标题
-                Text(
-                  StringName.intimacyAnalyseModel,
-                  style: TextStyle(
-                    color: ColorName.black47,
-                    fontSize: 12.sp,
-                    fontWeight: FontWeight.w400,
+    return Obx(() {
+      return Expanded(
+        child: Container(
+          padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 10.w),
+          decoration: BoxDecoration(
+            color: ColorName.white,
+            borderRadius: BorderRadius.circular(14.r),
+          ),
+          child: Row(
+            children: [
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  // 标题
+                  Text(
+                    StringName.intimacyAnalyseModel,
+                    style: TextStyle(
+                      color: ColorName.black47,
+                      fontSize: 12.sp,
+                      fontWeight: FontWeight.w400,
+                    ),
                   ),
-                ),
-                SizedBox(height: 4.h),
-                // 结果文字
-                Text(
-                  "DeepSeek R1",
-                  style: TextStyle(
-                    color: ColorName.black80,
-                    fontSize: 13.sp,
-                    fontWeight: FontWeight.w500,
+                  SizedBox(height: 4.h),
+                  // 结果文字
+                  Text(
+                    controller.currentAiModel.value?.name ?? "",
+                    style: TextStyle(
+                      color: ColorName.black80,
+                      fontSize: 13.sp,
+                      fontWeight: FontWeight.w500,
+                    ),
                   ),
-                ),
-              ],
-            ),
-            Expanded(child: SizedBox()),
-            // Ai模型图片
-            Assets.images.iconAiModel.image(width: 38, height: 30),
-          ],
+                ],
+              ),
+              Expanded(child: SizedBox()),
+              // Ai模型图片
+              Assets.images.iconAiModel.image(width: 38, height: 30),
+            ],
+          ),
         ),
-      ),
-    );
+      );
+    });
   }
 
   /// 添加预测方向按钮
@@ -467,7 +471,7 @@ class IntimacyAnalyseUploadPage
   /// 推荐的亲密关系
   Widget _buildRecommendIntimacy() {
     return Container(
-      width: double.maxFinite,
+      width: double.infinity,
       margin: EdgeInsets.only(left: 14.w, right: 14.w),
       // 圆角背景
       decoration: BoxDecoration(
@@ -573,8 +577,8 @@ class IntimacyAnalyseUploadPage
 
   /// 底部操作按钮
   Widget _buildBottomActionBtn() {
+    // 上传页,显示Ai模型切换按钮和下一步按钮
     if (controller.isUploadPage.value) {
-      // 上传页
       return Container(
         width: double.maxFinite,
         margin: EdgeInsets.only(
@@ -590,17 +594,22 @@ class IntimacyAnalyseUploadPage
         ),
       );
     } else {
-      // 结果页
-      return Container(
-        width: double.maxFinite,
-        margin: EdgeInsets.only(
-          left: 13.w,
-          top: 8.h,
-          right: 13.w,
-          bottom: 20.h,
-        ),
-        child: _buildAnalyseBtn(),
-      );
+      // 结果页,显示立即分析按钮
+      if (controller.intimacyAnalyzeResult.value == null) {
+        return Container(
+          width: double.maxFinite,
+          margin: EdgeInsets.only(
+            left: 13.w,
+            top: 8.h,
+            right: 13.w,
+            bottom: 20.h,
+          ),
+          child: _buildAnalyseBtn(),
+        );
+      } else {
+        // 生成定制人设按钮
+        return SizedBox();
+      }
     }
   }
 

+ 3 - 2
lib/module/intimacy_analyse/intimacy_analyse_upload/widget/upload_nine_grid.dart

@@ -109,9 +109,10 @@ class UploadNineGrid extends StatelessWidget {
   Widget _buildUploadItem(UploadInfo info, int index) {
     return UploadItemWidget(
       imageUploadInfo: info,
-      hasDeleteBtn: true,
+      // 编辑状态时,才有删除按钮
+      hasDeleteBtn: mode == Mode.edit,
+      // 点击删除按钮时回调,删除图片
       onClickDeleteCallback: (UploadInfo uploadInfo) {
-        // 删除图片
         if (onClickDeleteCallback != null) {
           onClickDeleteCallback!(uploadInfo);
         }

+ 12 - 3
lib/module/intimacy_analyse/screenshot_reply/conversation_analysis/conversation_analysis_controller.dart

@@ -63,6 +63,9 @@ class ConversationAnalysisController extends BaseController {
   /// 报告数据
   RxString reportData = ''.obs;
 
+  /// 是否生成中
+  RxBool isResultGenerating = false.obs;
+
   /// 上传图片列表
   RxList<UploadInfo> uploadInfoList = <UploadInfo>[].obs;
 
@@ -101,7 +104,8 @@ class ConversationAnalysisController extends BaseController {
     if (forTaConfig != null && forTaConfig.isNotEmpty) {
       var itemList =
           forTaConfig.map((item) {
-            return OptionSelectItem(item.title);
+            var title = item.title;
+            return OptionSelectItem(title, title);
           }).toList();
       optionSelectConfigList.add(
         OptionSelectConfig(StringName.forTa, "", itemList),
@@ -113,7 +117,8 @@ class ConversationAnalysisController extends BaseController {
     if (forMeConfig != null) {
       var itemList =
           forMeConfig.map((item) {
-            return OptionSelectItem(item.title);
+            var title = item.title;
+            return OptionSelectItem(title, title);
           }).toList();
       optionSelectConfigList.add(
         OptionSelectConfig(StringName.forMe, "", itemList),
@@ -161,7 +166,7 @@ class ConversationAnalysisController extends BaseController {
           return item.fileBackendPath ?? "";
         }).toList();
     // 选择的标题
-    String title = currentSelectOption.value?.name ?? "";
+    String title = currentSelectOption.value?.value ?? "";
 
     if (imageList.isEmpty) {
       ToastUtil.show(StringName.noUploadConversationImageTip);
@@ -174,6 +179,7 @@ class ConversationAnalysisController extends BaseController {
 
     // 切换为生成报告
     isUploadPage.value = false;
+    isResultGenerating.value = true;
     reportData.value = "";
 
     Stream<Message> stream = await intimacyAnalyzeRepository
@@ -256,6 +262,9 @@ class ConversationAnalysisController extends BaseController {
             // 上传信息更新时回调,更新UI
             _updateUploadInfo(uploadInfo);
           },
+          onUploadFailCallback: (error) {
+            ErrorHandler.toastError(error);
+          },
         ),
       );
       uploadInfoList.refresh();

+ 6 - 31
lib/module/intimacy_analyse/screenshot_reply/conversation_analysis/conversation_analysis_view.dart

@@ -9,6 +9,7 @@ import '../../../../data/bean/option_select_item.dart';
 import '../../../../resource/assets.gen.dart';
 import '../../../../resource/colors.gen.dart';
 import '../../../../resource/string.gen.dart';
+import '../../../../widget/gradient_btn.dart';
 import '../../intimacy_analyse_upload/widget/upload_nine_grid.dart';
 import '../../widget/option_select_widget.dart';
 import '../../widget/step/upload_step_card.dart';
@@ -61,7 +62,7 @@ class ConversationAnalysisView
             SizedBox(height: 90.h),
           ],
         );
-      } else if (controller.reportData.value.isNotEmpty) {
+      } else if (controller.isResultGenerating.value) {
         // 已出结果
         contentWidget = Column(
           children: [
@@ -433,36 +434,10 @@ class ConversationAnalysisView
     required String btnText,
     required VoidCallback onPressed,
   }) {
-    return GestureDetector(
-      onTap: () {
-        onPressed();
-      },
-      child: Container(
-        margin: EdgeInsets.symmetric(horizontal: 16.w),
-        padding: EdgeInsets.symmetric(vertical: 14.h),
-        width: double.maxFinite,
-        decoration: ShapeDecoration(
-          // 渐变背景
-          gradient: LinearGradient(
-            colors: [ColorName.purpleGradient3, ColorName.purpleGradient4],
-            begin: Alignment.centerLeft,
-            end: Alignment.centerRight,
-          ),
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(50.r),
-          ),
-        ),
-        child: Center(
-          child: Text(
-            btnText,
-            style: TextStyle(
-              color: ColorName.white,
-              fontSize: 16.sp,
-              fontWeight: FontWeight.w500,
-            ),
-          ),
-        ),
-      ),
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 16.w),
+      width: double.infinity,
+      child: GradientTextBtn(btnText, onPressed: onPressed),
     );
   }
 

+ 21 - 6
lib/module/intimacy_analyse/screenshot_reply/scan_image_reply/scan_image_reply_controller.dart

@@ -57,6 +57,9 @@ class ScanImageReplyController extends BaseController {
   /// 是否上传页
   RxBool isUploadPage = true.obs;
 
+  /// 是否生成结果中
+  RxBool isResultGenerating = false.obs;
+
   /// 上传图片列表
   Rxn<UploadInfo> uploadInfo = Rxn();
 
@@ -102,7 +105,11 @@ class ScanImageReplyController extends BaseController {
     if (toneList.isNotEmpty) {
       var newList =
           toneList.map((item) {
-            return OptionSelectItem("${item.emoji} ${item.title}");
+            return OptionSelectItem(
+              "${item.emoji} ${item.title}",
+              // 提交给服务端的数据,不能包含表情字符
+              item.title ?? "",
+            );
           }).toList();
 
       replyToneOptionSelectConfigList.clear();
@@ -184,8 +191,7 @@ class ScanImageReplyController extends BaseController {
     // 上传的图片后端地址
     List<String> imageList = [uploadInfo.value?.fileBackendPath ?? ""];
     // 选择的回复语气
-    // String title = currentSelectReplyToneOption.value?.name ?? "";
-    String title = "高冷";
+    String title = currentSelectReplyToneOption.value?.value ?? "";
     // 当前选择的回复模式
     String mode = currentReplyMode.value?.value ?? "";
 
@@ -204,7 +210,11 @@ class ScanImageReplyController extends BaseController {
 
     StringBuffer buffer = StringBuffer();
 
+    // 切换为生成中
+    isResultGenerating.value = true;
     replyToneList.clear();
+    // 添加一条,正在回复的条目
+    replyToneList.add("");
     replyToneList.refresh();
 
     // 请求分析截图
@@ -220,9 +230,11 @@ class ScanImageReplyController extends BaseController {
         IntimacyReplyAnalyzeResponse response =
             IntimacyReplyAnalyzeResponse.fromJson(jsonDecode(json));
 
-        List<String> contentList = response.choices?.map((item) {
-          return item.delta?.content ?? "";
-        }).toList() ?? [];
+        List<String> contentList =
+            response.choices?.map((item) {
+              return item.delta?.content ?? "";
+            }).toList() ??
+            [];
         // 数组转字符串
         String content = contentList.join("");
 
@@ -277,6 +289,9 @@ class ScanImageReplyController extends BaseController {
           // 上传信息更新时回调,更新UI
           _updateUploadInfo(uploadInfo);
         },
+        onUploadFailCallback: (error) {
+          ErrorHandler.toastError(error);
+        },
       );
     }
   }

+ 22 - 66
lib/module/intimacy_analyse/screenshot_reply/scan_image_reply/scan_image_reply_view.dart

@@ -13,6 +13,7 @@ import '../../../../resource/string.gen.dart';
 import '../../../../utils/prefix_util.dart';
 import '../../../../utils/upload/file_data_source_util.dart';
 import '../../../../widget/actionbtn/action_btn.dart';
+import '../../../../widget/gradient_btn.dart';
 import '../../../../widget/gradient_text.dart';
 import '../../image_viewer/image_viewer_page.dart';
 import '../../intimacy_analyse_upload/popup/reply_mode_select_popup.dart';
@@ -270,7 +271,8 @@ class ScanImageReplyView extends BaseView<ScanImageReplyController> {
   /// 回复语气列表卡片
   Widget _buildReplyToneListCard() {
     return Obx(() {
-      if (controller.replyToneList.isEmpty) {
+      // 为生成
+      if (!controller.isResultGenerating.value) {
         return SizedBox();
       }
       return Container(
@@ -423,9 +425,9 @@ class ScanImageReplyView extends BaseView<ScanImageReplyController> {
               margin: EdgeInsets.only(left: 21.w, top: 12.h, bottom: 12.h),
               child: Text(
                 replyTone,
-                maxLines: 1,
+                // maxLines: 1,
                 style: TextStyle(
-                  overflow: TextOverflow.ellipsis,
+                  // overflow: TextOverflow.ellipsis,
                   color: ColorName.black80,
                   fontSize: 14.sp,
                   fontWeight: FontWeight.w500,
@@ -467,69 +469,28 @@ class ScanImageReplyView extends BaseView<ScanImageReplyController> {
 
   /// 上传截图按钮
   Widget _buildUploadScreenshotBtn(BuildContext context) {
-    return GestureDetector(
-      onTap: () {
-        controller.clickUploadScreenshotBtn(context);
-      },
-      child: Container(
-        margin: EdgeInsets.symmetric(horizontal: 16.w),
-        padding: EdgeInsets.symmetric(vertical: 14.h),
-        width: double.maxFinite,
-        decoration: ShapeDecoration(
-          // 渐变背景
-          gradient: LinearGradient(
-            colors: [ColorName.purpleGradient3, ColorName.purpleGradient4],
-            begin: Alignment.centerLeft,
-            end: Alignment.centerRight,
-          ),
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(50.r),
-          ),
-        ),
-        child: Center(
-          child: Text(
-            StringName.intimacyUploadScreenshot,
-            style: TextStyle(
-              color: ColorName.white,
-              fontSize: 16.sp,
-              fontWeight: FontWeight.w500,
-            ),
-          ),
-        ),
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 16.w),
+      width: double.maxFinite,
+      child: GradientTextBtn(
+        StringName.intimacyUploadScreenshot,
+        onPressed: () {
+          controller.clickUploadScreenshotBtn(context);
+        },
       ),
     );
   }
 
   /// 获取回复按钮
   Widget _buildGetReplayBtn() {
-    return GestureDetector(
-      onTap: () {
-        controller.clickGetReplyBtn();
-      },
-      child: Container(
-        padding: EdgeInsets.symmetric(vertical: 14.h),
-        width: double.maxFinite,
-        decoration: ShapeDecoration(
-          // 渐变背景
-          gradient: LinearGradient(
-            colors: [ColorName.purpleGradient3, ColorName.purpleGradient4],
-            begin: Alignment.centerLeft,
-            end: Alignment.centerRight,
-          ),
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.circular(50.r),
-          ),
-        ),
-        child: Center(
-          child: Text(
-            StringName.intimacyAnalyseGetReply,
-            style: TextStyle(
-              color: ColorName.white,
-              fontSize: 16.sp,
-              fontWeight: FontWeight.w500,
-            ),
-          ),
-        ),
+    return Container(
+      padding: EdgeInsets.symmetric(vertical: 14.h),
+      width: double.maxFinite,
+      child: GradientTextBtn(
+        StringName.intimacyAnalyseGetReply,
+        onPressed: () {
+          controller.clickGetReplyBtn();
+        },
       ),
     );
   }
@@ -608,12 +569,7 @@ class ScanImageReplyView extends BaseView<ScanImageReplyController> {
       // 上传页,显示上传截图按钮
       return Container(
         width: double.maxFinite,
-        margin: EdgeInsets.only(
-          left: 16.w,
-          top: 8.h,
-          right: 16.w,
-          bottom: 20.h,
-        ),
+        margin: EdgeInsets.only(top: 8.h, bottom: 20.h),
         child: _buildUploadScreenshotBtn(context),
       );
     } else {

+ 133 - 25
lib/module/intimacy_analyse/widget/intimacy_analyse_report_widget.dart

@@ -3,27 +3,39 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:keyboard/resource/string.gen.dart';
 import 'package:lottie/lottie.dart';
 
+import '../../../data/api/response/intimacy_analyze_response.dart';
+import '../../../data/bean/intimacy_analyse_report.dart';
 import '../../../resource/assets.gen.dart';
 import '../../../resource/colors.gen.dart';
+import '../../../utils/string_format_util.dart';
 import '../../../widget/animated_progress_bar.dart';
 import '../../../widget/markdown/markdown_viewer.dart';
+import '../analyse_report/widget/report_item_widget.dart';
 
 /// 亲密度报告组件
 class IntimacyAnalyseReportWidget extends StatelessWidget {
-  /// 报告内容
+  /// 报告内容,Markdown格式
   final String reportContent;
 
+  /// 分析结果
+  final IntimacyAnalyzeResponse? intimacyAnalyzeResult;
+
   /// 是否已解锁
   final bool unlock;
 
   /// 是否是预览
   final bool isPreview;
 
+  /// 是否报告生成中
+  final bool isReportCreating;
+
   const IntimacyAnalyseReportWidget({
     super.key,
-    required this.reportContent,
+    this.reportContent = '',
+    this.intimacyAnalyzeResult,
     this.unlock = false,
     this.isPreview = false,
+    this.isReportCreating = false,
   });
 
   @override
@@ -35,11 +47,15 @@ class IntimacyAnalyseReportWidget extends StatelessWidget {
     if (!unlock) {
       return UnlockReportCardWidget();
     }
-    return reportContent.isEmpty
-        // 报告生成中
-        ? CreatingReportCardWidget()
-        // 已出报告
-        : ExistReportCardWidget(reportContent: reportContent);
+    // 报告生成中
+    if (isReportCreating) {
+      return CreatingReportCardWidget();
+    }
+    // 已出报告
+    return ExistReportCardWidget(
+      reportContent: reportContent,
+      intimacyAnalyzeResult: intimacyAnalyzeResult,
+    );
   }
 }
 
@@ -86,7 +102,14 @@ class ExistReportCardWidget extends StatelessWidget {
   /// 报告内容,Markdown格式
   final String reportContent;
 
-  const ExistReportCardWidget({super.key, required this.reportContent});
+  /// 分析结果
+  final IntimacyAnalyzeResponse? intimacyAnalyzeResult;
+
+  const ExistReportCardWidget({
+    super.key,
+    required this.reportContent,
+    this.intimacyAnalyzeResult,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -96,7 +119,9 @@ class ExistReportCardWidget extends StatelessWidget {
         children: [
           _buildReportOverview(),
           SizedBox(height: 10.h),
-          _buildReportDetail(),
+          reportContent.isNotEmpty
+              ? _buildReportDetailByMarkdown()
+              : _buildReportDetail(),
         ],
       ),
     );
@@ -104,6 +129,12 @@ class ExistReportCardWidget extends StatelessWidget {
 
   /// 报告概览,包含概览数值和图表
   Widget _buildReportOverview() {
+    if (intimacyAnalyzeResult == null) {
+      return SizedBox();
+    }
+
+    var analyzeResult = intimacyAnalyzeResult!;
+
     // 圆角背景
     return Container(
       padding: EdgeInsets.only(left: 12.w, right: 12.w, top: 12.h),
@@ -123,15 +154,21 @@ class ExistReportCardWidget extends StatelessWidget {
             ),
             child: Row(
               children: [
-                _buildIntimacyOverviewItem("30"),
+                _buildIntimacyOverviewItem(
+                  StringFormatUtil.removePercentSymbol(
+                    analyzeResult.emotion ?? "",
+                  ),
+                ),
                 _buildOverviewDivider(),
-                _buildCurrentStageOverviewItem("相互了解"),
+                _buildCurrentStageOverviewItem(
+                  analyzeResult.intimacyPhase ?? "",
+                ),
               ],
             ),
           ),
           SizedBox(height: 36.h),
           // 图表
-          _buildChart(),
+          _buildChart(analyzeResult),
         ],
       ),
     );
@@ -244,29 +281,48 @@ class ExistReportCardWidget extends StatelessWidget {
     );
   }
 
+  /// 将百分比转换为进度条需要的数值
+  double _convertPercentValue2ProgressBar(String percentValue) {
+    if (percentValue.isEmpty) {
+      return 0;
+    }
+    // 去掉%号后的数值
+    String valueStr = StringFormatUtil.removePercentSymbol(percentValue);
+    double noPercentValue = double.tryParse(valueStr) ?? 0;
+    if (noPercentValue == 0) {
+      return 0;
+    } else {
+      // 除以100,转换为0-1之间的数值,才是进度条需要的数值
+      double progressValue = noPercentValue / 100;
+      return progressValue;
+    }
+  }
+
   /// 报告图表
-  Widget _buildChart() {
+  Widget _buildChart(IntimacyAnalyzeResponse analyzeResult) {
     return Column(
       mainAxisSize: MainAxisSize.min,
       children: [
         _buildValueItem(
           iconProvider: Assets.images.iconEmojiLike.provider(),
-          title: "互动好感度",
-          value: 0.5,
+          title: StringName.intimacyInteraction,
+          value: _convertPercentValue2ProgressBar(
+            analyzeResult.interaction ?? "",
+          ),
           progressColors: [ColorName.blueGradient1, ColorName.blueGradient2],
         ),
         SizedBox(height: 18.h),
         _buildValueItem(
           iconProvider: Assets.images.iconEmojiChat.provider(),
-          title: "话题好感度",
-          value: 0.35,
+          title: StringName.intimacyTopic,
+          value: _convertPercentValue2ProgressBar(analyzeResult.topic ?? ""),
           progressColors: [ColorName.greenGradient1, ColorName.greenGradient2],
         ),
         SizedBox(height: 18.h),
         _buildValueItem(
           iconProvider: Assets.images.iconEmojiLike.provider(),
-          title: "情绪回应",
-          value: 1.0,
+          title: StringName.intimacyRespond,
+          value: _convertPercentValue2ProgressBar(analyzeResult.respond ?? ""),
           progressColors: [
             ColorName.yellowGradient1,
             ColorName.yellowGradient2,
@@ -275,15 +331,19 @@ class ExistReportCardWidget extends StatelessWidget {
         SizedBox(height: 18.h),
         _buildValueItem(
           iconProvider: Assets.images.iconEmojiPercent.provider(),
-          title: "亲密词占比",
-          value: 0.4,
+          title: StringName.intimacyintimacyRatio,
+          value: _convertPercentValue2ProgressBar(
+            analyzeResult.intimacyRatio ?? "",
+          ),
           progressColors: [ColorName.pinkGradient1, ColorName.pinkGradient2],
         ),
         SizedBox(height: 18.h),
         _buildValueItem(
           iconProvider: Assets.images.iconEmojiLove.provider(),
-          title: "缘分指数",
-          value: 0.15,
+          title: analyzeResult.directionName ?? "",
+          value: _convertPercentValue2ProgressBar(
+            analyzeResult.direction ?? "",
+          ),
           progressColors: [
             ColorName.purpleGradient1,
             ColorName.purpleGradient2,
@@ -345,8 +405,8 @@ class ExistReportCardWidget extends StatelessWidget {
     );
   }
 
-  /// 报告详情
-  Widget _buildReportDetail() {
+  /// 报告详情,使用Markdown
+  Widget _buildReportDetailByMarkdown() {
     return Container(
       decoration: ShapeDecoration(
         color: ColorName.white,
@@ -362,6 +422,54 @@ class ExistReportCardWidget extends StatelessWidget {
       ),
     );
   }
+
+  /// 报告详情,使用组件渲染
+  Widget _buildReportDetail() {
+    if (intimacyAnalyzeResult == null) {
+      return SizedBox();
+    }
+
+    // 组装数据
+    IntimacyAnalyseReport reportPreviewData = IntimacyAnalyseReport(
+      list: [
+        AnalyseItem(
+          title: StringName.intimacyintimacyEmotionNeed,
+          sections: [intimacyAnalyzeResult?.need ?? ""],
+        ),
+        AnalyseItem(
+          title: StringName.intimacyintimacyChatStrategy,
+          sections: [intimacyAnalyzeResult?.chatStrategy ?? ""],
+        ),
+        AnalyseItem(
+          title: StringName.intimacyintimacySummary,
+          sections: [intimacyAnalyzeResult?.summary ?? ""],
+        ),
+      ],
+    );
+
+    return _buildReportContent(reportPreviewData);
+  }
+
+  /// 构建报告内容
+  Widget _buildReportContent(IntimacyAnalyseReport report) {
+    return Container(
+      padding: EdgeInsets.all(14.w),
+      decoration: ShapeDecoration(
+        color: ColorName.white,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(30.r),
+        ),
+      ),
+      child: Column(
+        children: [
+          // 报告列表
+          ...report.list.map((ele) {
+            return ReportItemWidget(item: ele);
+          }),
+        ],
+      ),
+    );
+  }
 }
 
 /// 报告生成中的卡片

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

@@ -192,6 +192,7 @@ class StringName {
   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 intimacyAnalyseUploadFailPleaseRetry = 'intimacy_analyse_upload_fail_please_retry'.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; // 选择预测方向
@@ -231,7 +232,18 @@ class StringName {
   static final String noChooseOptionTip = 'no_choose_option_tip'.tr; // 请选择一项需求
   static final String noChooseModeTip = 'no_choose_mode_tip'.tr; // 请选择模式
   static final String noUploadScreenshotTip = 'no_upload_screenshot_tip'.tr; // 请上传截图
+  static final String noChooseIntimacyTaTip = 'no_choose_intimacy_ta_tip'.tr; // 请选择亲密对象
+  static final String noChoosePredictionDirectionTip = 'no_choose_prediction_direction_tip'.tr; // 请选择预测方向
+  static final String noChooseAiModelTip = 'no_choose_ai_model_tip'.tr; // 请选择AI模型
   static final String noUploadConversationImageTip = 'no_upload_conversation_image_tip'.tr; // 请上传聊天记录图片
+  static final String intimacyAnalyseIng = 'intimacy_analyse_ing'.tr; // 分析中
+  static final String intimacyInteraction = 'intimacy_interaction'.tr; // 互动好感度
+  static final String intimacyTopic = 'intimacy_topic'.tr; // 话题好感度
+  static final String intimacyRespond = 'intimacy_respond'.tr; // 情绪回应
+  static final String intimacyintimacyRatio = 'intimacy_intimacy_ratio'.tr; // 亲密词占比
+  static final String intimacyintimacyEmotionNeed = 'intimacy_intimacy_emotion_need'.tr; // 情感需求
+  static final String intimacyintimacyChatStrategy = 'intimacy_intimacy_chat_strategy'.tr; // 聊天策略
+  static final String intimacyintimacySummary = 'intimacy_intimacy_summary'.tr; // 总结
   static final String preview = 'preview'.tr; // 预览
   static final String retry = 'retry'.tr; // 再试试
   static final String nextStep = 'next_step'.tr; // 下一步
@@ -466,6 +478,7 @@ class StringMultiSource {
       'intimacy_analyse_upload_limit_tip': '还可上传%s张',
       'intimacy_analyse_upload_fail': '上传失败',
       'intimacy_analyse_uploading': '上传中',
+      'intimacy_analyse_upload_fail_please_retry': '上传失败,请重试',
       'intimacy_analyse_step': 'STEP%s',
       'intimacy_analyse_step_title_select_image': '选择你的图片',
       'intimacy_analyse_step_title_select_prediction_direction': '选择预测方向',
@@ -505,7 +518,18 @@ class StringMultiSource {
       'no_choose_option_tip': '请选择一项需求',
       'no_choose_mode_tip': '请选择模式',
       'no_upload_screenshot_tip': '请上传截图',
+      'no_choose_intimacy_ta_tip': '请选择亲密对象',
+      'no_choose_prediction_direction_tip': '请选择预测方向',
+      'no_choose_ai_model_tip': '请选择AI模型',
       'no_upload_conversation_image_tip': '请上传聊天记录图片',
+      'intimacy_analyse_ing': '分析中',
+      'intimacy_interaction': '互动好感度',
+      'intimacy_topic': '话题好感度',
+      'intimacy_respond': '情绪回应',
+      'intimacy_intimacy_ratio': '亲密词占比',
+      'intimacy_intimacy_emotion_need': '情感需求',
+      'intimacy_intimacy_chat_strategy': '聊天策略',
+      'intimacy_intimacy_summary': '总结',
       'preview': '预览',
       'retry': '再试试',
       'next_step': '下一步',

+ 5 - 0
lib/utils/string_format_util.dart

@@ -6,4 +6,9 @@ class StringFormatUtil {
   static String formatStr(String str, String format) {
     return sprintf(str, [format]);
   }
+
+  /// 清除百分比符号
+  static String removePercentSymbol(String str) {
+    return str.replaceAll("%", "");
+  }
 }

+ 15 - 4
lib/utils/upload/upload_file_manager.dart

@@ -1,5 +1,4 @@
 import 'dart:io';
-
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 import 'package:keyboard/utils/upload/upload_scene_type.dart';
@@ -14,6 +13,9 @@ import '../file_util.dart';
 /// 上传信息更新回调
 typedef OnUploadInfoUpdateCallback = void Function(UploadInfo uploadInfo);
 
+/// 上传失败时回调
+typedef OnUploadFailCallback = void Function(dynamic error);
+
 /// 文件上传管理器
 @lazySingleton
 class UploadFileManager {
@@ -32,6 +34,7 @@ class UploadFileManager {
     required UploadSceneType sceneType,
     required File file,
     OnUploadInfoUpdateCallback? onUploadInfoUpdateCallback,
+    OnUploadFailCallback? onUploadFailCallback,
   }) {
     // 创建一个上传记录,添加到队列中
     UploadInfo uploadInfo = UploadInfo(
@@ -49,7 +52,11 @@ class UploadFileManager {
     _uploadInfoQueue.add(uploadInfo);
 
     // 执行上传任务
-    _doUploadFile(uploadInfo, onUploadInfoUpdateCallback);
+    _doUploadFile(
+      uploadInfo,
+      onUploadInfoUpdateCallback: onUploadInfoUpdateCallback,
+      onUploadFailCallback: onUploadFailCallback,
+    );
 
     return uploadInfo;
   }
@@ -85,9 +92,10 @@ class UploadFileManager {
 
   /// 执行,上传文件
   void _doUploadFile(
-    UploadInfo uploadInfo,
+    UploadInfo uploadInfo, {
     OnUploadInfoUpdateCallback? onUploadInfoUpdateCallback,
-  ) {
+    OnUploadFailCallback? onUploadFailCallback,
+  }) {
     File file = File(uploadInfo.filePath ?? "");
     fileUploadRepository
         .uploadImage(file: file)
@@ -114,6 +122,9 @@ class UploadFileManager {
           if (onUploadInfoUpdateCallback != null) {
             onUploadInfoUpdateCallback(uploadInfo);
           }
+          if (onUploadFailCallback != null) {
+            onUploadFailCallback(error);
+          }
         });
   }
 }

+ 22 - 6
lib/widget/animated_progress_bar.dart

@@ -100,14 +100,23 @@ class AnimatedGradientProgressBarState
       builder: (context, constraints) {
         // 进度条的实际宽度
         final progressBarWidth = constraints.maxWidth;
+        // 计算气泡距离左边的偏移量
+        double leftOffset = 0;
+        if (_animation.value == 0) {
+          leftOffset = 0;
+        } else {
+          leftOffset =
+              _animation.value * progressBarWidth -
+              (widget.targetValue > 0.5 ? 32 : 24);
+        }
         return Stack(
           // 允许子组件溢出自己本身的大小,默认是裁切的
           clipBehavior: Clip.none,
           children: [
             // 百分比文本
             Positioned(
-              // 左侧偏移量,跟随当前进度的末尾,公式:当前比例值 * 进度条的宽度 - 气泡宽度
-              left:  widget.targetValue * progressBarWidth - (widget.targetValue > 0.5 ? 32 : 24),
+              // 左侧偏移量,气泡跟随当前进度的末尾,公式:当前比例值 * 进度条的宽度 - 气泡宽度
+              left: leftOffset,
               // 进度气泡,位于进度条上方
               bottom: widget.height + 0.85,
               child: AnimatedBuilder(
@@ -116,18 +125,25 @@ class AnimatedGradientProgressBarState
                   return BubbleWidget(
                     // 箭头方向
                     arrowDirection: AxisDirection.down,
-                    arrowOffset: 22,
+                    // 箭头距离左边的偏移量
+                    arrowOffset: leftOffset == 0 ? 10 : 22,
                     arrowLength: 8,
                     arrowRadius: 2,
                     arrowWidth: 5,
-                    padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 7),
+                    padding: const EdgeInsets.symmetric(
+                      vertical: 2,
+                      horizontal: 7,
+                    ),
                     borderRadius: BorderRadius.circular(9),
                     // 气泡的背景颜色,取渐变色的第2个颜色
                     backgroundColor: widget.gradient.colors.last,
                     contentBuilder: (context) {
                       return Text(
                         '${(_animation.value * 100).toStringAsFixed(0)}%',
-                        style: const TextStyle(fontSize: 10, color: Colors.white),
+                        style: const TextStyle(
+                          fontSize: 10,
+                          color: Colors.white,
+                        ),
                       );
                     },
                   );
@@ -169,7 +185,7 @@ class AnimatedGradientProgressBarState
             ),
           ],
         );
-      }
+      },
     );
   }
 }

+ 121 - 0
lib/widget/gradient_btn.dart

@@ -0,0 +1,121 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../resource/colors.gen.dart';
+
+/// 渐变色按钮
+class GradientBtn extends StatelessWidget {
+  /// 纯色颜色
+  final Color? color;
+
+  /// 渐变颜色参数
+  final List<Color> colors;
+
+  /// 渐变方向
+  final AlignmentGeometry begin;
+  final AlignmentGeometry end;
+
+  /// 按钮圆角半径
+  final double radius;
+
+  /// 子组件
+  final Widget child;
+
+  /// 点击回调
+  final VoidCallback onPressed;
+
+  const GradientBtn({
+    super.key,
+    required this.colors,
+    this.color,
+    this.begin = Alignment.centerLeft,
+    this.end = Alignment.centerRight,
+    this.radius = 50,
+    required this.child,
+    required this.onPressed,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    Decoration decoration;
+    // 优先使用纯色,如果没有纯色,则使用渐变色
+    if (color != null) {
+      decoration = BoxDecoration(
+        color: color,
+        borderRadius: BorderRadius.all(Radius.circular(radius)),
+      );
+    } else {
+      // 渐变色
+      decoration = ShapeDecoration(
+        gradient: LinearGradient(colors: colors, begin: begin, end: end),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(radius),
+        ),
+      );
+    }
+    return GestureDetector(
+      onTap: () {
+        onPressed();
+      },
+      child: Container(
+        padding: EdgeInsets.symmetric(vertical: 14.h),
+        width: double.maxFinite,
+        // 渐变背景
+        decoration: decoration,
+        child: Center(child: child),
+      ),
+    );
+  }
+}
+
+/// 渐变按钮,中间为文字
+class GradientTextBtn extends StatelessWidget {
+  /// 按钮文字
+  final String text;
+
+  /// 纯色颜色
+  final Color? color;
+
+  /// 渐变颜色参数
+  final List<Color> colors;
+
+  /// 渐变方向
+  final AlignmentGeometry begin;
+  final AlignmentGeometry end;
+
+  /// 按钮圆角半径
+  final double radius;
+
+  /// 点击回调
+  final VoidCallback onPressed;
+
+  const GradientTextBtn(
+    this.text, {
+    super.key,
+    this.color,
+    this.colors = const [ColorName.purpleGradient3, ColorName.purpleGradient4],
+    this.begin = Alignment.centerLeft,
+    this.end = Alignment.centerRight,
+    this.radius = 50,
+    required this.onPressed,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GradientBtn(
+      color: color,
+      colors: colors,
+      radius: radius,
+      onPressed: onPressed,
+      child: Text(
+        text,
+        style: TextStyle(
+          color: ColorName.white,
+          fontSize: 16.sp,
+          fontWeight: FontWeight.bold,
+        ),
+      ),
+    );
+  }
+}

File diff suppressed because it is too large
+ 1 - 0
plugins/keyboard_android/android/src/main/assets/lottie/anim_intimacy_value_love.json


File diff suppressed because it is too large
+ 1 - 0
plugins/keyboard_android/android/src/main/assets/lottie/anim_join_vip_btn.json


+ 41 - 22
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/AiKeyboardCommonPanelComponent.kt

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.util.AttributeSet
 import android.view.View
+import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.atmob.keyboard_android.R
 import com.atmob.keyboard_android.component.base.BaseUIComponent
@@ -98,10 +99,18 @@ class AiKeyboardCommonPanelComponent @JvmOverloads constructor(
             mKeyListAdapter = MultiTypeAdapter(mKeyListItems).apply {
                 // 键盘按键条目
                 register(AiKeyboardKeyModel::class.java, AiKeyboardKeyViewBinder {
-                    // 正在加载中,忽略点击
+                    // 当前点击的条目,正在加载中,忽略点击
                     if (it.isLoading) {
                         return@AiKeyboardKeyViewBinder
                     }
+                    // 其他条目正在加载中,忽略点击
+                    val hasItemLoading =
+                        mKeyListItems.filterIsInstance<AiKeyboardKeyModel>().filter {
+                            it.isLoading
+                        }.toList().isNotEmpty()
+                    if (hasItemLoading) {
+                        return@AiKeyboardKeyViewBinder
+                    }
                     // 点击键盘按键,打开AI生成内容面板
                     if (it.isVip) {
                         // 检查是否VIP
@@ -120,29 +129,39 @@ class AiKeyboardCommonPanelComponent @JvmOverloads constructor(
                     FlutterBridgeManager.jump2CharacterMarketPage()
                 })
             }
-            // 水平分页布局管理器
-            layoutManager = PagerGridLayoutManager(
-                Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
-                Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
-                PagerGridLayoutManager.HORIZONTAL
-            ).apply {
-                // 设置滚动监听
-                setPageListener(object : PageListener {
-                    override fun onPageSizeChanged(pageSize: Int) {
-                        // 当总页数确定时的回调
-                    }
+            if (Constants.AI_KEYBOARD_KEY_LIST_PAGE_ENABLE) {
+                // 分页布局管理器
+                layoutManager = PagerGridLayoutManager(
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    PagerGridLayoutManager.HORIZONTAL
+                ).apply {
+                    // 设置滚动监听
+                    setPageListener(object : PageListener {
+                        override fun onPageSizeChanged(pageSize: Int) {
+                            // 当总页数确定时的回调
+                        }
 
-                    override fun onPageSelect(pageIndex: Int) {
-                        // 当页面被选中时的回调(从 0 开始)
-                    }
-                })
-            }
-            // 设置滚动辅助工具
-            val pageSnapHelper = PagerGridSnapHelper().apply {
-                // 设置滚动阀值
-                setFlingThreshold(minFlingVelocity)
+                        override fun onPageSelect(pageIndex: Int) {
+                            // 当页面被选中时的回调(从 0 开始)
+                        }
+                    })
+                }
+                // 设置滚动辅助工具
+                val pageSnapHelper = PagerGridSnapHelper().apply {
+                    // 设置滚动阀值
+                    setFlingThreshold(minFlingVelocity)
+                }
+                pageSnapHelper.attachToRecyclerView(this)
+            } else {
+                // 不分页,只网格
+                layoutManager = GridLayoutManager(
+                    context,
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    GridLayoutManager.VERTICAL,
+                    false
+                )
             }
-            pageSnapHelper.attachToRecyclerView(this)
             // 设置适配器
             adapter = mKeyListAdapter
             // 添加分割线

+ 32 - 21
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/AiKeyboardProloguePanelComponent.kt

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.util.AttributeSet
 import android.view.View
+import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.atmob.keyboard_android.R
 import com.atmob.keyboard_android.component.base.BaseUIComponent
@@ -189,29 +190,39 @@ class AiKeyboardProloguePanelComponent @JvmOverloads constructor(
                     }
                 })
             }
-            // 水平分页布局管理器
-            layoutManager = PagerGridLayoutManager(
-                Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
-                Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
-                PagerGridLayoutManager.HORIZONTAL
-            ).apply {
-                // 设置滚动监听
-                setPageListener(object : PageListener {
-                    override fun onPageSizeChanged(pageSize: Int) {
-                        // 当总页数确定时的回调
-                    }
+            if (Constants.AI_KEYBOARD_KEY_LIST_PAGE_ENABLE) {
+                // 分页布局管理器
+                layoutManager = PagerGridLayoutManager(
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    PagerGridLayoutManager.HORIZONTAL
+                ).apply {
+                    // 设置滚动监听
+                    setPageListener(object : PageListener {
+                        override fun onPageSizeChanged(pageSize: Int) {
+                            // 当总页数确定时的回调
+                        }
 
-                    override fun onPageSelect(pageIndex: Int) {
-                        // 当页面被选中时的回调(从 0 开始)
-                    }
-                })
-            }
-            // 设置滚动辅助工具
-            val pageSnapHelper = PagerGridSnapHelper().apply {
-                // 设置滚动阀值
-                setFlingThreshold(minFlingVelocity)
+                        override fun onPageSelect(pageIndex: Int) {
+                            // 当页面被选中时的回调(从 0 开始)
+                        }
+                    })
+                }
+                // 设置滚动辅助工具
+                val pageSnapHelper = PagerGridSnapHelper().apply {
+                    // 设置滚动阀值
+                    setFlingThreshold(minFlingVelocity)
+                }
+                pageSnapHelper.attachToRecyclerView(this)
+            } else {
+                // 不分页,只网格
+                layoutManager = GridLayoutManager(
+                    context,
+                    Constants.AI_KEYBOARD_KEY_LIST_SPAN_COUNT,
+                    GridLayoutManager.VERTICAL,
+                    false
+                )
             }
-            pageSnapHelper.attachToRecyclerView(this)
             // 设置适配器
             adapter = mKeyListAdapter
             // 添加分割线

+ 1 - 1
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/QuickSwitchComponent.kt

@@ -94,7 +94,7 @@ class QuickSwitchComponent @JvmOverloads constructor(
             // 监听键盘名称切换
             getKeyboardViewModel().currentKeyboardInfo.observe(getLifecycleOwner()) { newKeyboard ->
                 LogUtil.d("更新键盘名称:${newKeyboard.name}")
-                var name = newKeyboard.name
+                var name = newKeyboard.name ?: ""
                 if (name.isBlank()) {
                     name = context.resources.getString(R.string.common_keyboard)
                 }

+ 7 - 1
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/ToolBarComponent.kt

@@ -50,7 +50,13 @@ class ToolBarComponent @JvmOverloads constructor(
             rootComponent?.switchSettingPage()
         }
 //        vIcon.longClick {
-//            PermissionDialogUtil.showPermissionDialog(context)
+//            // 测试权限申请提示弹窗
+//            // PermissionDialogUtil.showPermissionDialog(context)
+//
+//            // 测试Vip页面
+//            val keyboardRootComponent =
+//                ComponentMediator.findComponent(IKeyboardRootComponent::class.java)
+//            keyboardRootComponent?.switchVipPage()
 //            true
 //        }
         vPinyinSwitchBtn.click {

+ 1 - 1
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/KeyboardSelectViewBinder.kt

@@ -34,7 +34,7 @@ class KeyboardSelectViewBinder(
     ) {
         val context = holder.itemView.context
 
-        val isSystemKeyboard = KeyboardType.isSystem(item.type)
+        val isSystemKeyboard = KeyboardType.isSystem(item.type ?: "")
         val iconDefaultResId = if (isSystemKeyboard) {
             R.mipmap.ic_common_keyboard_icon
         } else {

+ 5 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/constant/Constants.kt

@@ -16,6 +16,11 @@ interface Constants {
         const val AI_KEYBOARD_KEY_LIST_SPAN_COUNT: Int = 3
 
         /**
+         * Ai键盘,按键列表,是否开启翻页
+         */
+        const val AI_KEYBOARD_KEY_LIST_PAGE_ENABLE: Boolean = false
+
+        /**
          * 组件动画,是否开启
          */
         const val COMPONENT_ANIMATOR_ENABLE = false

+ 5 - 5
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/KeyboardSelectModel.kt

@@ -13,23 +13,23 @@ data class KeyboardSelectModel(
     /**
      * 键盘类型,system:系统键盘、custom:定制键盘
      */
-    val type: String,
+    val type: String?,
     /**
      * 键盘名称
      */
-    val name: String,
+    val name: String?,
     /**
      * 性别
      */
-    val gender: Int,
+    val gender: Int?,
     /**
      * 生日
      */
-    val birthday: String,
+    val birthday: String?,
     /**
      * 亲密度
      */
-    val intimacy: Int,
+    val intimacy: Int?,
     /**
      * 键盘图标Url
      */

+ 17 - 9
plugins/keyboard_android/android/src/main/res/layout/component_tool_bar.xml

@@ -4,6 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:clipChildren="false"
     android:gravity="center_vertical"
     android:orientation="horizontal"
     tools:background="@android:color/background_dark"
@@ -36,6 +37,7 @@
         android:layout_height="wrap_content"
         android:layout_gravity="end"
         android:layout_marginEnd="10dp"
+        android:clipChildren="false"
         android:gravity="center_vertical"
         android:orientation="horizontal"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -53,26 +55,32 @@
             android:id="@+id/keyboard_switch_btn"
             android:layout_width="34dp"
             android:layout_height="34dp"
-            android:layout_marginStart="10dp"
-            android:layout_marginEnd="10dp"
+            android:layout_marginStart="6dp"
+            android:layout_marginEnd="6dp"
             android:src="@mipmap/ic_keyboard" />
 
         <FrameLayout
             android:id="@+id/intimacy_layout"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:clipChildren="false">
 
-            <ImageView
-                android:layout_width="35dp"
-                android:layout_height="30dp"
-                android:src="@mipmap/bg_love" />
+            <com.airbnb.lottie.LottieAnimationView
+                android:id="@+id/intimacy_value_love_lottie_view"
+                android:layout_width="40dp"
+                android:layout_height="35dp"
+                android:layout_gravity="center"
+                android:visibility="visible"
+                app:lottie_autoPlay="true"
+                app:lottie_fileName="lottie/anim_intimacy_value_love.json"
+                app:lottie_loop="true" />
 
             <TextView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
-                android:layout_marginTop="7dp"
-                android:text="30%"
+                android:layout_marginTop="10dp"
+                android:text="0%"
                 android:textColor="@color/text_color_white"
                 android:textSize="10sp"
                 android:textStyle="bold" />

+ 30 - 15
plugins/keyboard_android/android/src/main/res/layout/component_vip_page.xml

@@ -51,24 +51,39 @@
             android:textStyle="bold" />
     </FrameLayout>
 
-    <com.atmob.keyboard_android.widget.IconFontTextView
-        android:id="@+id/join_vip_btn"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
         android:layout_marginStart="30dp"
         android:layout_marginTop="154dp"
         android:layout_marginEnd="30dp"
-        android:layout_marginBottom="24dp"
-        android:background="@drawable/bg_gradient2"
-        android:gravity="center"
-        android:paddingTop="9dp"
-        android:paddingBottom="10dp"
-        android:text="@string/go_vip"
-        android:textColor="@color/text_color_white"
-        android:textSize="24sp"
-        android:textStyle="italic"
-        app:ift_typeface="app"
+        android:layout_marginBottom="20dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
+        app:layout_constraintStart_toStartOf="parent">
+
+        <com.airbnb.lottie.LottieAnimationView
+            android:id="@+id/join_vip_lottie_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="visible"
+            app:lottie_autoPlay="true"
+            app:lottie_fileName="lottie/anim_join_vip_btn.json"
+            app:lottie_loop="true" />
+
+        <com.atmob.keyboard_android.widget.IconFontTextView
+            android:id="@+id/join_vip_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:gravity="center"
+            android:paddingTop="9dp"
+            android:paddingBottom="10dp"
+            android:text="@string/go_vip"
+            android:textColor="@color/text_color_white"
+            android:textSize="24sp"
+            android:textStyle="italic"
+            app:ift_typeface="app" />
+    </FrameLayout>
 </androidx.constraintlayout.widget.ConstraintLayout>