ソースを参照

[feat]亲密度分析,增加Ai模型切换按钮和弹窗

hezihao 7 ヶ月 前
コミット
829a8e06ac

+ 9 - 0
assets/color/business_color.xml

@@ -27,4 +27,13 @@
     <!-- 亲密关系文字的渐变色 -->
     <color name="intimacy_relation_color1">#FF5521F6</color>
     <color name="intimacy_relation_color2">#FFC456F5</color>
+
+    <!-- Ai模式切换按钮的边框颜色 -->
+    <color name="bg_ai_model_switch_border_btn">#FFE8E8E8</color>
+    <!-- Ai模式切换按钮的渐变色 -->
+    <color name="ai_model_switch_btn_color1">#FFB618FF</color>
+    <color name="ai_model_switch_btn_color2">#FF3794FF</color>
+
+    <!-- Ai模式选中时的背景颜色 -->
+    <color name="bg_ai_model_selected">#FFDDCFFD</color>
 </resources>

BIN
assets/images/icon_lock.webp


BIN
assets/images/icon_mode_switch_arrow.webp


+ 15 - 2
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_controller.dart

@@ -13,6 +13,13 @@ class IntimacyAnalyseUploadController extends BaseController {
   /// 已选择的图片列表
   RxList<AssetEntity> selectedAssetList = <AssetEntity>[].obs;
 
+  /// Ai模型列表
+  RxList<String> aiModelList =
+      <String>['通用模式', 'DeepSeek R2', 'DeepSeek R1'].obs;
+
+  /// 当前应用的Ai模型
+  Rx<String> currentAiModel = 'DeepSeek R1'.obs;
+
   @override
   void onInit() {
     super.onInit();
@@ -26,7 +33,8 @@ class IntimacyAnalyseUploadController extends BaseController {
     if (arguments?[AppPageArguments.selectedAssetList] == null) {
       AtmobLog.i(tag, '没有传递 selectedAssetList 参数');
     } else {
-      final List<AssetEntity>? argumentList = arguments?[AppPageArguments.selectedAssetList] as List<AssetEntity>?;
+      final List<AssetEntity>? argumentList =
+          arguments?[AppPageArguments.selectedAssetList] as List<AssetEntity>?;
       if (argumentList != null) {
         selectedAssetList.assignAll(argumentList);
         AtmobLog.i(tag, "selectedAssetList: $selectedAssetList");
@@ -38,4 +46,9 @@ class IntimacyAnalyseUploadController extends BaseController {
   void clickBack() {
     Get.back();
   }
-}
+
+  /// 切换Ai模型
+  void switchAiModel(String newAiModel) {
+    currentAiModel.value = newAiModel;
+  }
+}

+ 142 - 11
lib/module/intimacy_analyse/intimacy_analyse_upload/intimacy_analyse_upload_page.dart

@@ -2,6 +2,7 @@ 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/popup/ai_model_select_popup.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';
@@ -15,6 +16,7 @@ import '../../../resource/assets.gen.dart';
 import '../../../router/app_page_arguments.dart';
 import '../../../router/app_pages.dart';
 import '../../../utils/string_format_util.dart';
+import '../../../widget/actionbtn/action_btn.dart';
 import '../../../widget/gradient_text.dart';
 import '../widget/intimacy_user_widget.dart';
 import '../widget/step_card.dart';
@@ -23,7 +25,10 @@ import 'intimacy_analyse_upload_controller.dart';
 /// 亲密度分析上传页
 class IntimacyAnalyseUploadPage
     extends BasePage<IntimacyAnalyseUploadController> {
-  const IntimacyAnalyseUploadPage({super.key});
+  IntimacyAnalyseUploadPage({super.key});
+
+  /// Ai模型切换按钮的GlobalKey
+  final GlobalKey _aiModelSwitchBtnAnchorKey = GlobalKey();
 
   @override
   bool immersive() {
@@ -56,7 +61,14 @@ class IntimacyAnalyseUploadPage
           ),
         ),
         child: Column(
-          children: [_buildStatusBar(), _buildTopBar(), _buildContent()],
+          children: [
+            // 状态栏占位
+            _buildStatusBar(),
+            // 顶部栏
+            _buildTopBar(),
+            // 内容区域
+            _buildContent(),
+          ],
         ),
       ),
     );
@@ -306,18 +318,137 @@ class IntimacyAnalyseUploadPage
     );
   }
 
+  /// 底部操作按钮
+  Widget _buildBottomActionBtn() {
+    return Container(
+      width: double.maxFinite,
+      margin: EdgeInsets.only(left: 13.w, top: 8.h, right: 13.w, bottom: 20.h),
+      child: ActionBtn(
+        leftBtn: _buildAiModelSwitchBtn(),
+        rightBtn: _buildNextBtn(),
+      ),
+    );
+  }
+
+  /// Ai模型切换按钮
+  Widget _buildAiModelSwitchBtn() {
+    return Builder(
+      builder: (context) {
+        return GestureDetector(
+          key: _aiModelSwitchBtnAnchorKey,
+          onTap: () {
+            if (AiModelSelectPopup.isShowing()) {
+              AiModelSelectPopup.dismiss();
+              return;
+            }
+            // 切换模型
+            AiModelSelectPopup.show(
+              _aiModelSwitchBtnAnchorKey,
+              context,
+              aiModelList: controller.aiModelList.toList(),
+              currentAiModel: controller.currentAiModel.value,
+              onChooseAiModelCallback: (String newAiModel) {
+                controller.switchAiModel(newAiModel);
+              },
+            );
+          },
+          child: Container(
+            padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 14.w),
+            decoration: BoxDecoration(
+              color: ColorName.white,
+              borderRadius: BorderRadius.all(Radius.circular(30.w)),
+              border: Border.all(
+                color: ColorName.bgAiModelSwitchBorderBtn,
+                width: 1.w,
+              ),
+            ),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                Obx(() {
+                  return
+                  // 文字
+                  GradientText(
+                    colors: [
+                      ColorName.aiModelSwitchBtnColor1,
+                      ColorName.aiModelSwitchBtnColor2,
+                    ],
+                    child: Text(
+                      controller.currentAiModel.value,
+                      style: TextStyle(
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                  );
+                }),
+                SizedBox(width: 8.w),
+                // 箭头
+                Assets.images.iconModeSwitchArrow.image(
+                  width: 15.w,
+                  height: 15.w,
+                ),
+              ],
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  /// 下一步按钮
+  Widget _buildNextBtn() {
+    return Container(
+      padding: EdgeInsets.only(top: 13.h, bottom: 13.h),
+      decoration: BoxDecoration(
+        color: ColorName.colorBrand,
+        borderRadius: BorderRadius.all(Radius.circular(30.r)),
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Assets.images.iconLock.image(width: 22.w, height: 22.h),
+          SizedBox(width: 8.w),
+          Text(
+            StringName.nextStep,
+            style: TextStyle(
+              color: ColorName.white,
+              fontSize: 16.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   /// 内容
   Widget _buildContent() {
     return Expanded(
-      child: SingleChildScrollView(
-        child: Column(
-          children: [
-            // 上传卡片
-            _buildUploadStepCard(),
-            // 预测方向卡片
-            _buildPredictionDirectionStepCard(),
-          ],
-        ),
+      child: Stack(
+        children: [
+          // 长列表
+          SingleChildScrollView(
+            child: Column(
+              children: [
+                // 上传卡片
+                _buildUploadStepCard(),
+                // 预测方向卡片
+                _buildPredictionDirectionStepCard(),
+                SizedBox(height: 50.h),
+              ],
+            ),
+          ),
+          // 底部操作按钮
+          Positioned(
+            left: 0,
+            right: 0,
+            bottom: 0,
+            child: _buildBottomActionBtn(),
+          ),
+        ],
       ),
     );
   }

+ 214 - 0
lib/module/intimacy_analyse/intimacy_analyse_upload/popup/ai_model_select_popup.dart

@@ -0,0 +1,214 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:keyboard/resource/colors.gen.dart';
+
+import '../../../../resource/assets.gen.dart';
+import '../../../../widget/gradient_text.dart';
+
+/// 选择新的Ai模型时回调
+typedef OnChooseAiModelCallback = void Function(String newAiModel);
+
+/// Ai模型选择Popup锚点弹窗
+class AiModelSelectPopup {
+  /// 用于标识弹窗的唯一性
+  static const _tag = "AiModelSelectPopup";
+
+  /// 弹窗是否显示中
+  static bool _isShowing = false;
+
+  /// 弹窗是否显示中
+  static bool isShowing() {
+    return _isShowing;
+  }
+
+  /// 显示
+  /// [anchorBtnKey] 锚点按钮的GlobalKey
+  /// [aiModelList] 模型列表
+  /// [currentAiModel] 当前使用的模型
+  /// [onChooseAiModelCallback] 选择新的Ai模型时回调
+  static void show(
+    GlobalKey anchorBtnKey,
+    BuildContext context, {
+    required List<String> aiModelList,
+    required String currentAiModel,
+    required OnChooseAiModelCallback onChooseAiModelCallback,
+  }) {
+    if (_isShowing) {
+      return;
+    }
+
+    double popupWidth = 140.w;
+
+    SmartDialog.showAttach(
+      // 绑定唯一tag
+      tag: _tag,
+      // 绑定锚点的BuildContext,才能确定弹窗的显示位置
+      targetContext: context,
+      // 在锚点的顶部显示
+      alignment: _calculateBestAlignment(anchorBtnKey, context),
+      // 使用动画
+      useAnimation: true,
+      // 动画类型
+      animationType: SmartAnimationType.fade,
+      // 是否允许事件穿透
+      usePenetrate: true,
+      // 监听弹窗关闭
+      onDismiss: () {
+        _isShowing = false;
+      },
+      // 位置偏移配置
+      targetBuilder: (Offset targetOffset, Size targetSize) {
+        // 获取锚点信息
+        final anchorRenderBox =
+            anchorBtnKey.currentContext!.findRenderObject() as RenderBox;
+        final anchorPosition = anchorRenderBox.localToGlobal(Offset.zero);
+        final anchorSize = anchorRenderBox.size;
+
+        // 计算弹窗位置
+        double popupX =
+            anchorPosition.dx + anchorSize.width / 2 - popupWidth / 2;
+
+        // 计算Y轴的偏移量(默认是在按钮的上边显示,UI设计要求盖着按钮,所以需要增加偏移量)
+        double popupY = anchorPosition.dy + 46.h;
+        return Offset(popupX, popupY);
+      },
+      // 构建弹窗内容
+      builder: (_) {
+        return Container(
+          // 固定弹窗的宽度
+          width: popupWidth,
+          decoration: BoxDecoration(
+            color: ColorName.white,
+            border: Border.all(
+              color: ColorName.bgAiModelSwitchBorderBtn,
+              width: 1.0.w,
+            ),
+            borderRadius: BorderRadius.all(Radius.circular(16.r)),
+          ),
+          child: Column(
+            // 包裹内容
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              SizedBox(height: 12.h),
+              // Ai模型列表
+              Flexible(
+                child: ListView.builder(
+                  // ListView包裹内容
+                  shrinkWrap: true,
+                  physics: const ClampingScrollPhysics(),
+                  // 去除默认padding
+                  padding: EdgeInsets.zero,
+                  itemCount: aiModelList.length,
+                  itemBuilder: (BuildContext context, int index) {
+                    String itemData = aiModelList[index];
+                    return _buildListItem(
+                      index,
+                      itemData,
+                      currentAiModel == itemData,
+                      onChooseAiModelCallback,
+                    );
+                  },
+                ),
+              ),
+              // 当前选中项
+              _buildCurrentAiModelItem(currentAiModel),
+            ],
+          ),
+        );
+      },
+    );
+
+    // 设置为正在显示中
+    _isShowing = true;
+  }
+
+  /// 计算对齐方式
+  static Alignment _calculateBestAlignment(
+    GlobalKey anchorBtnKey,
+    BuildContext context,
+  ) {
+    final renderBox =
+        anchorBtnKey.currentContext?.findRenderObject() as RenderBox;
+    final position = renderBox.localToGlobal(Offset.zero);
+    final screenHeight = MediaQuery.of(context).size.height;
+
+    return position.dy > screenHeight / 2
+        ? Alignment.topCenter
+        : Alignment.bottomCenter;
+  }
+
+  /// 列表项
+  static Widget _buildListItem(
+    int index,
+    String itemData,
+    bool isSelected,
+    OnChooseAiModelCallback onChooseAiModelCallback,
+  ) {
+    return GestureDetector(
+      onTap: () {
+        onChooseAiModelCallback(itemData);
+        SmartDialog.dismiss();
+      },
+      child: Container(
+        alignment: Alignment.center,
+        child: Container(
+          margin: EdgeInsets.only(left: 6.w, right: 6.w, bottom: 10.h),
+          padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 20.w),
+          decoration: BoxDecoration(
+            borderRadius: BorderRadius.circular(7.r),
+            color: isSelected ? ColorName.bgAiModelSelected : ColorName.white,
+          ),
+          child: Text(
+            itemData,
+            style: TextStyle(
+              color: ColorName.black80,
+              fontSize: 14.sp,
+              fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  /// 当前选中项
+  static Widget _buildCurrentAiModelItem(String currentAiModel) {
+    return GestureDetector(
+      onTap: () {
+        dismiss();
+      },
+      child: Container(
+        padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 14.w),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            // 文字
+            GradientText(
+              colors: [
+                ColorName.aiModelSwitchBtnColor1,
+                ColorName.aiModelSwitchBtnColor2,
+              ],
+              child: Text(
+                currentAiModel,
+                style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
+              ),
+            ),
+            SizedBox(width: 8.w),
+            // 箭头
+            Assets.images.iconModeSwitchArrow.image(width: 15.w, height: 15.w),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 隐藏
+  static void dismiss() {
+    if (!_isShowing) {
+      return;
+    }
+    SmartDialog.dismiss(tag: _tag);
+  }
+}

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

@@ -519,6 +519,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconKeyboardVipLogo =>
       const AssetGenImage('assets/images/icon_keyboard_vip_logo.webp');
 
+  /// File path: assets/images/icon_lock.webp
+  AssetGenImage get iconLock =>
+      const AssetGenImage('assets/images/icon_lock.webp');
+
   /// File path: assets/images/icon_member_retain_close.webp
   AssetGenImage get iconMemberRetainClose =>
       const AssetGenImage('assets/images/icon_member_retain_close.webp');
@@ -580,6 +584,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconMineVipOrderArrow =>
       const AssetGenImage('assets/images/icon_mine_vip_order_arrow.png');
 
+  /// File path: assets/images/icon_mode_switch_arrow.webp
+  AssetGenImage get iconModeSwitchArrow =>
+      const AssetGenImage('assets/images/icon_mode_switch_arrow.webp');
+
   /// File path: assets/images/icon_profile_add.webp
   AssetGenImage get iconProfileAdd =>
       const AssetGenImage('assets/images/icon_profile_add.webp');
@@ -860,6 +868,7 @@ class $AssetsImagesGen {
     iconKeyboardTitle,
     iconKeyboardTriangle,
     iconKeyboardVipLogo,
+    iconLock,
     iconMemberRetainClose,
     iconMineAbout,
     iconMineArrow,
@@ -875,6 +884,7 @@ class $AssetsImagesGen {
     iconMineVipArrow,
     iconMineVipDescArrow,
     iconMineVipOrderArrow,
+    iconModeSwitchArrow,
     iconProfileAdd,
     iconProfileEdit,
     iconProfileFemale,

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

@@ -13,9 +13,21 @@ import 'package:flutter/material.dart';
 class ColorName {
   ColorName._();
 
+  /// Color: #FFB618FF
+  static const Color aiModelSwitchBtnColor1 = Color(0xFFB618FF);
+
+  /// Color: #FF3794FF
+  static const Color aiModelSwitchBtnColor2 = Color(0xFF3794FF);
+
   /// Color: #FFF5F6F8
   static const Color bgColorPrimary = Color(0xFFF5F6F8);
 
+  /// Color: #FFDDCFFD
+  static const Color bgAiModelSelected = Color(0xFFDDCFFD);
+
+  /// Color: #FFE8E8E8
+  static const Color bgAiModelSwitchBorderBtn = Color(0xFFE8E8E8);
+
   /// Color: #FFE2DBFF
   static const Color bgIntimacyRelationColor1 = Color(0xFFE2DBFF);
 

+ 20 - 0
lib/widget/actionbtn/action_btn.dart

@@ -0,0 +1,20 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+/// 操作按钮,包含2个按钮,左侧按钮固定,右侧按钮自适应
+class ActionBtn extends StatelessWidget {
+  /// 左侧按钮
+  final Widget leftBtn;
+
+  /// 右侧按钮
+  final Widget rightBtn;
+
+  const ActionBtn({super.key, required this.leftBtn, required this.rightBtn});
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [leftBtn, SizedBox(width: 10.w), Expanded(child: rightBtn)],
+    );
+  }
+}