Przeglądaj źródła

[feat]增加键盘引导页

hezihao 7 miesięcy temu
rodzic
commit
9970e2b240

+ 11 - 0
assets/color/business_color.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- 聊天气泡,我的 -->
+    <color name="msg_bubble_me">#FFE6DCFF</color>
+    <!-- 聊天气泡,对方 -->
+    <color name="msg_bubble_ta">#FFF5F6FA</color>
+    <!-- 聊天,底部输入栏的背景 -->
+    <color name="msg_input_bar">#FFD3D5E1</color>
+    <!-- 聊天,输入框的光标 -->
+    <color name="input_cursor">#FF996DFF</color>
+</resources>

BIN
assets/images/bg_go_app.webp


BIN
assets/images/icon_copy.webp


BIN
assets/images/icon_default_avatar.webp


BIN
assets/images/icon_ta_avatar.webp


BIN
assets/images/icon_wechat.webp


+ 9 - 1
assets/string/base/string.xml

@@ -152,7 +152,6 @@
     <string name="text_span_service_terms">《服务条款》</string>
 
 
-
     <string name="member_continue_pay">继续支付</string>
     <string name="member_please_choice_goods">请选择支付商品</string>
     <string name="member_please_choice_payment">请选择支付方式</string>
@@ -204,4 +203,13 @@
 
 
     <string name="keyboard_member_open">开通会员</string>
+
+    <!-- 键盘引导页 -->
+    <string name="keyboard_guide_go_wechat">去微信体验</string>
+    <string name="keyboard_guide_wechat_not_install">未安装微信</string>
+    <string name="keyboard_guide_input_hint">选择粘贴TA的话,选择人设风格回复</string>
+
+    <string name="keyboard_guide_ta_reply1">👋 欢迎使用【追爱小键盘】\n复制任意一句对话,点击人设体验回复</string>
+    <string name="keyboard_guide_ta_reply2">你睡了吗?</string>
+    <string name="keyboard_guide_ta_reply3">我先去吃饭了,一会聊</string>
 </resources>

+ 27 - 0
lib/data/model/keyboard_guide_msg.dart

@@ -0,0 +1,27 @@
+/// 引导消息实体类
+class KeyboardGuideMsg {
+  /// 是否是我发的
+  bool isMe = false;
+
+  /// 消息内容
+  String content = "";
+
+  /// 创建时间
+  int createTime = 0;
+
+  KeyboardGuideMsg({required this.isMe, required this.content, required this.createTime});
+
+  KeyboardGuideMsg.fromJson(Map<String, dynamic> json) {
+    isMe = json['isMe'];
+    content = json['content'];
+    createTime = json['createTime'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final Map<String, dynamic> data = <String, dynamic>{};
+    data['isMe'] = isMe;
+    data['content'] = content;
+    data['createTime'] = createTime;
+    return data;
+  }
+}

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

@@ -39,6 +39,7 @@ import '../module/character_custom/list/character_custom_list_controller.dart'
     as _i1059;
 import '../module/feedback/feedback_controller.dart' as _i876;
 import '../module/keyboard/keyboard_controller.dart' as _i161;
+import '../module/keyboard_guide/keyboard_guide_controller.dart' as _i248;
 import '../module/keyboard_manage/keyboard_manage_controller.dart' as _i922;
 import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
@@ -48,6 +49,7 @@ import '../module/profile/profile_controller.dart' as _i244;
 import '../module/store/discount/discount_controller.dart' as _i333;
 import '../module/store/store_controller.dart' as _i344;
 import '../module/store/suprise/goods_surprise_controller.dart' as _i935;
+import '../plugins/keyboard_method_handler.dart' as _i415;
 import '../utils/payment_status_manager.dart' as _i779;
 import 'network_module.dart' as _i567;
 
@@ -61,9 +63,15 @@ extension GetItInjectableX on _i174.GetIt {
     final networkModule = _$NetworkModule();
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i248.KeyboardGuidePageController>(
+      () => _i248.KeyboardGuidePageController(),
+    );
     gh.factory<_i1008.LoginController>(() => _i1008.LoginController());
     gh.factory<_i731.MainController>(() => _i731.MainController());
     gh.factory<_i333.DiscountController>(() => _i333.DiscountController());
+    gh.factory<_i415.KeyboardMethodHandler>(
+      () => _i415.KeyboardMethodHandler(),
+    );
     gh.singleton<_i361.Dio>(
       () => networkModule.createStreamDio(),
       instanceName: 'streamDio',

+ 131 - 0
lib/module/keyboard_guide/keyboard_guide_controller.dart

@@ -0,0 +1,131 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/resource/string.gen.dart';
+
+import '../../base/base_controller.dart';
+import '../../data/model/keyboard_guide_msg.dart';
+import '../../utils/toast_util.dart';
+
+/// 键盘引导页面Controller
+@injectable
+class KeyboardGuidePageController extends BaseController {
+  /// TextField操作控制器
+  final TextEditingController editingController = TextEditingController();
+
+  /// ListView的滚动控制器
+  final ScrollController scrollController = ScrollController();
+
+  /// 输入框焦点
+  final FocusNode inputFocusNode = FocusNode();
+
+  /// 消息列表
+  final RxList<KeyboardGuideMsg> msgList = <KeyboardGuideMsg>[].obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    inputFocusNode.addListener(_handleTextFieldFocusChange);
+    // 初始化消息列表
+    _initMsgList();
+    // 进入页面,就获取输入框焦点
+    // inputFocusNode.requestFocus();
+  }
+
+  @override
+  void onClose() {
+    // 取消监听
+    inputFocusNode.removeListener(_handleTextFieldFocusChange);
+    inputFocusNode.dispose();
+    editingController.dispose();
+    scrollController.dispose();
+    super.onClose();
+  }
+
+  /// 关闭页面
+  clickBack() {
+    Get.back();
+  }
+
+  /// 发送消息
+  void sendMsg(String msg) {
+    if (msg.isEmpty) {
+      ToastUtil.show("请输入要发送的消息内容");
+      return;
+    }
+    //添加消息到列表中
+    _addMsg2List(msg, true);
+    // 延迟生成对方的回复消息
+    // Future.delayed(const Duration(milliseconds: 150), () {
+    //   //添加消息到列表中
+    //   _addMsg2List(_replyMessage2Client(msg), false);
+    // });
+    //清除输入框的内容
+    editingController.clear();
+  }
+
+  // /// 测试,生成回复消息
+  // String _replyMessage2Client(String clientMsg) {
+  //   return clientMsg
+  //       .replaceAll("我", "你")
+  //       .replaceAll("吗", "")
+  //       .replaceAll("?", "!")
+  //       .replaceAll("?", "!");
+  // }
+
+  /// 初始化消息列表
+  void _initMsgList() {
+    // 添加一些默认消息
+    msgList.add(
+      KeyboardGuideMsg(
+        isMe: false,
+        content: StringName.keyboardGuideTaReply1,
+        createTime: DateTime.now().millisecond,
+      ),
+    );
+    msgList.add(
+      KeyboardGuideMsg(
+        isMe: false,
+        content: StringName.keyboardGuideTaReply2,
+        createTime: DateTime.now().millisecond,
+      ),
+    );
+    msgList.add(
+      KeyboardGuideMsg(
+        isMe: false,
+        content: StringName.keyboardGuideTaReply3,
+        createTime: DateTime.now().millisecond,
+      ),
+    );
+  }
+
+  /// 添加消息到消息列表中
+  void _addMsg2List(String msg, bool isMe) {
+    msgList.add(
+      KeyboardGuideMsg(
+        isMe: isMe,
+        content: msg,
+        createTime: DateTime.now().millisecond,
+      ),
+    );
+    update();
+    _scrollToBottom();
+  }
+
+  /// 滚动列表到底部
+  void _scrollToBottom() {
+    if (scrollController.hasClients) {
+      scrollController.jumpTo(scrollController.position.maxScrollExtent);
+    }
+  }
+
+  /// 处理输入框的焦点变化
+  void _handleTextFieldFocusChange() {
+    // 输入框获取焦点,滚动列表到底部
+    if (inputFocusNode.hasFocus) {
+      Future.delayed(const Duration(milliseconds: 350), () {
+        _scrollToBottom();
+      });
+    }
+  }
+}

+ 331 - 0
lib/module/keyboard_guide/keyboard_guide_page.dart

@@ -0,0 +1,331 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:keyboard/module/keyboard_guide/keyboard_guide_controller.dart';
+import 'package:keyboard/router/app_pages.dart';
+import 'package:keyboard/utils/toast_util.dart';
+
+import '../../base/base_page.dart';
+import '../../data/model/keyboard_guide_msg.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
+import '../../resource/string.gen.dart';
+import '../../utils/clipboard_util.dart';
+import '../../utils/url_launcher_util.dart';
+
+/// 键盘引导页面
+class KeyboardGuidePage extends BasePage<KeyboardGuidePageController> {
+  const KeyboardGuidePage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.keyboardGuide);
+  }
+
+  @override
+  immersive() {
+    return false;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Scaffold(
+      backgroundColor: backgroundColor(),
+      body: Column(
+        children: [
+          // 标题栏
+          _buildTitleBar(),
+          // 消息列表
+          Expanded(
+            flex: 1,
+            child: Obx(() {
+              return ListView.builder(
+                controller: controller.scrollController,
+                itemCount: controller.msgList.length,
+                itemBuilder: (BuildContext context, int index) {
+                  return _buildMsgItem(controller.msgList[index]);
+                },
+              );
+            }),
+          ),
+          // 底部输入栏
+          _buildBottomInput(),
+        ],
+      ),
+    );
+  }
+
+  // 标题栏
+  Widget _buildTitleBar() {
+    return Container(
+      color: backgroundColor(),
+      height: kToolbarHeight,
+      padding: EdgeInsets.symmetric(horizontal: 16.0),
+      child: Row(
+        children: [
+          // 返回按钮
+          GestureDetector(
+            onTap: controller.clickBack,
+            child: Assets.images.iconMineBackArrow.image(
+              width: 24.w,
+              height: 24.h,
+            ),
+          ),
+          // 标题
+          Expanded(
+            child: Container(
+              alignment: Alignment.center,
+              child: Text("", style: const TextStyle(fontSize: 18)),
+            ),
+          ),
+          // 右侧按钮
+          GestureDetector(
+            onTap: () async {
+              bool result = await UrlLauncherUtil.openWeChat();
+              if (!result) {
+                ToastUtil.show(StringName.keyboardGuideWechatNotInstall);
+              }
+            },
+            child: Container(
+              padding: EdgeInsets.only(
+                left: 12.w,
+                right: 14.w,
+                top: 10.w,
+                bottom: 10.w,
+              ),
+              decoration: BoxDecoration(
+                image: DecorationImage(
+                  image: Assets.images.bgGoApp.provider(),
+                  fit: BoxFit.fill,
+                ),
+              ),
+              child: Row(
+                children: [
+                  Assets.images.iconWechat.image(height: 22.w, width: 22.w),
+                  SizedBox(width: 1.0),
+                  Text(
+                    StringName.keyboardGuideGoWechat,
+                    style: TextStyle(
+                      color: ColorName.black80,
+                      fontSize: 12,
+                      fontWeight: FontWeight.w400,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建底部输入框
+  Widget _buildBottomInput() {
+    return Center(
+      child: Column(
+        children: [
+          Container(
+            color: ColorName.msgInputBar,
+            padding: const EdgeInsets.symmetric(
+              vertical: 11.0,
+              horizontal: 12.0,
+            ),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.start,
+              children: [
+                Expanded(
+                  flex: 1,
+                  // 输入框的圆角边框
+                  child: Container(
+                    decoration: BoxDecoration(
+                      color: ColorName.white,
+                      borderRadius: BorderRadius.circular(10.0),
+                    ),
+                    child: TextField(
+                      style: TextStyle(
+                        color: ColorName.black80,
+                        fontSize: 14.0,
+                        fontWeight: FontWeight.w500,
+                      ),
+                      // 设置光标颜色
+                      cursorColor: ColorName.inputCursor,
+                      // 光标宽度
+                      cursorWidth: 2.0,
+                      // 光标圆角
+                      cursorRadius: Radius.circular(2),
+                      // 设置按钮显示为发送
+                      textInputAction: TextInputAction.send,
+                      // 用户点击软键盘的发送按钮时,触发回调
+                      onSubmitted: (value) {
+                        var msg = controller.editingController.text;
+                        controller.sendMsg(msg);
+                      },
+                      // 输入框焦点
+                      focusNode: controller.inputFocusNode,
+                      // 点击外部区域,关闭软键盘
+                      onTapUpOutside: (event) {
+                        controller.inputFocusNode.unfocus();
+                      },
+                      // 输入框控制器
+                      controller: controller.editingController,
+                      decoration: InputDecoration(
+                        // 提示文字
+                        hintText: StringName.keyboardGuideInputHint,
+                        hintStyle: TextStyle(
+                          fontSize: 14.0,
+                          fontWeight: FontWeight.w400,
+                          color: ColorName.black40,
+                        ),
+                        // 去掉默认的边框
+                        border: InputBorder.none,
+                        // 设置输入框的内边距
+                        contentPadding: EdgeInsets.symmetric(
+                          horizontal: 9.0,
+                          vertical: 13.0,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建聊天气泡
+  Widget _buildMsgBubble(KeyboardGuideMsg msg) {
+    // 设置气泡的外边距,让气泡不易过长
+    double marginValue = 59.0;
+    EdgeInsets marginEdgeInsets;
+    if (msg.isMe) {
+      marginEdgeInsets = EdgeInsets.only(left: marginValue);
+    } else {
+      marginEdgeInsets = EdgeInsets.only(right: marginValue);
+    }
+
+    // 圆角大小
+    double radiusSize = 14.0;
+    // 背景圆角
+    BorderRadius bgBorderRadius;
+    if (msg.isMe) {
+      bgBorderRadius = BorderRadius.only(
+        topLeft: Radius.circular(radiusSize),
+        topRight: Radius.circular(0),
+        bottomLeft: Radius.circular(radiusSize),
+        bottomRight: Radius.circular(radiusSize),
+      );
+    } else {
+      bgBorderRadius = BorderRadius.only(
+        topLeft: Radius.circular(0),
+        topRight: Radius.circular(radiusSize),
+        bottomLeft: Radius.circular(radiusSize),
+        bottomRight: Radius.circular(0),
+      );
+    }
+
+    // Flexible,文本超过一行时,自动换行,并且不超过最大宽度,不超过一行时,则自动包裹内容
+    return Flexible(
+      child: Container(
+        padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 10.0),
+        margin: marginEdgeInsets,
+        decoration: BoxDecoration(
+          color: msg.isMe ? ColorName.msgBubbleMe : ColorName.msgBubbleTa,
+          borderRadius: bgBorderRadius,
+        ),
+        child: Row(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Flexible(
+              // 消息文本
+              child: Text(
+                msg.content,
+                style: TextStyle(
+                  fontSize: 14.0,
+                  color: ColorName.black80,
+                  fontWeight: FontWeight.w500,
+                  height: 1.5,
+                ),
+                softWrap: true,
+              ),
+            ),
+            // 只有对方发送的,才有复制按钮
+            if (!msg.isMe)
+              Padding(
+                padding: EdgeInsets.only(left: 8.0),
+                child: GestureDetector(
+                  onTap: () {
+                    // 复制内容到剪切板
+                    ClipboardUtil.copyToClipboard(msg.content);
+                  },
+                  child: Assets.images.iconCopy.image(
+                    width: 18.w,
+                    height: 18.w,
+                  ),
+                ),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 构建聊天消息列表项
+  Widget _buildMsgItem(KeyboardGuideMsg msg) {
+    Widget content;
+    // 自己发的
+    if (msg.isMe) {
+      content = Row(
+        // 如果是自己发的,则在右边
+        mainAxisAlignment: MainAxisAlignment.end,
+        // 顶部对齐
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // 聊天气泡
+          _buildMsgBubble(msg),
+          // 头像
+          _buildAvatar(msg),
+        ],
+      );
+    } else {
+      // 对方发的
+      content = Row(
+        // 如果是自己发的,则在右边
+        mainAxisAlignment: MainAxisAlignment.start,
+        // 顶部对齐
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // 头像
+          _buildAvatar(msg),
+          // 聊天气泡
+          _buildMsgBubble(msg),
+        ],
+      );
+    }
+    return Container(padding: const EdgeInsets.all(8.0), child: content);
+  }
+
+  /// 构建头像
+  Widget _buildAvatar(KeyboardGuideMsg msg) {
+    double avatarSize = 36.0;
+    return Container(
+      margin: const EdgeInsets.symmetric(horizontal: 9.0),
+      child: CircleAvatar(
+        radius: 20,
+        child:
+            msg.isMe
+                ? Assets.images.iconDefaultAvatar.image(
+                  height: avatarSize,
+                  width: avatarSize,
+                )
+                : Assets.images.iconTaAvatar.image(
+                  height: avatarSize,
+                  width: avatarSize,
+                ),
+      ),
+    );
+  }
+}

+ 3 - 0
lib/module/mine/mine_controller.dart

@@ -16,6 +16,7 @@ import '../../data/repository/account_repository.dart';
 import '../../plugins/keyboard_android_platform.dart';
 import '../../plugins/keyboard_method_handler.dart';
 import '../../resource/colors.gen.dart';
+import '../keyboard_guide/keyboard_guide_page.dart';
 import '../profile/profile_page.dart';
 import '../store/discount/discount_controller.dart';
 import '../store/suprise/surprise_dialog.dart';
@@ -89,6 +90,8 @@ class MineController extends BaseController {
 
   clickTutorials() {
     debugPrint('clickTutorials');
+    // TODO hezhiao,测试,跳转到键盘引导页
+    KeyboardGuidePage.start();
   }
 
   clickPersonalProfile() {

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

@@ -97,6 +97,10 @@ class $AssetsImagesGen {
   AssetGenImage get bgDiscountTitle =>
       const AssetGenImage('assets/images/bg_discount_title.webp');
 
+  /// File path: assets/images/bg_go_app.webp
+  AssetGenImage get bgGoApp =>
+      const AssetGenImage('assets/images/bg_go_app.webp');
+
   /// File path: assets/images/bg_keyboard.webp
   AssetGenImage get bgKeyboard =>
       const AssetGenImage('assets/images/bg_keyboard.webp');
@@ -279,6 +283,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconCharacterVip =>
       const AssetGenImage('assets/images/icon_character_vip.webp');
 
+  /// File path: assets/images/icon_copy.webp
+  AssetGenImage get iconCopy =>
+      const AssetGenImage('assets/images/icon_copy.webp');
+
   /// File path: assets/images/icon_custom_character_add_market.webp
   AssetGenImage get iconCustomCharacterAddMarket => const AssetGenImage(
     'assets/images/icon_custom_character_add_market.webp',
@@ -288,6 +296,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconCustomDialogClose =>
       const AssetGenImage('assets/images/icon_custom_dialog_close.webp');
 
+  /// File path: assets/images/icon_default_avatar.webp
+  AssetGenImage get iconDefaultAvatar =>
+      const AssetGenImage('assets/images/icon_default_avatar.webp');
+
   /// File path: assets/images/icon_dialog_close_black.webp
   AssetGenImage get iconDialogCloseBlack =>
       const AssetGenImage('assets/images/icon_dialog_close_black.webp');
@@ -542,6 +554,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconSurpriseDialogTitle =>
       const AssetGenImage('assets/images/icon_surprise_dialog_title.webp');
 
+  /// File path: assets/images/icon_ta_avatar.webp
+  AssetGenImage get iconTaAvatar =>
+      const AssetGenImage('assets/images/icon_ta_avatar.webp');
+
   /// File path: assets/images/icon_tab_character_selected.webp
   AssetGenImage get iconTabCharacterSelected =>
       const AssetGenImage('assets/images/icon_tab_character_selected.webp');
@@ -570,6 +586,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconTicketDialogButton =>
       const AssetGenImage('assets/images/icon_ticket_dialog_button.webp');
 
+  /// File path: assets/images/icon_wechat.webp
+  AssetGenImage get iconWechat =>
+      const AssetGenImage('assets/images/icon_wechat.webp');
+
   /// File path: assets/images/icon_wechat_payment.webp
   AssetGenImage get iconWechatPayment =>
       const AssetGenImage('assets/images/icon_wechat_payment.webp');
@@ -593,6 +613,7 @@ class $AssetsImagesGen {
     bgDiscountContent,
     bgDiscountTagTop,
     bgDiscountTitle,
+    bgGoApp,
     bgKeyboard,
     bgKeyboardLove,
     bgKeyboardManage,
@@ -637,8 +658,10 @@ class $AssetsImagesGen {
     iconCharacterLock,
     iconCharacterMarket,
     iconCharacterVip,
+    iconCopy,
     iconCustomCharacterAddMarket,
     iconCustomDialogClose,
+    iconDefaultAvatar,
     iconDialogCloseBlack,
     iconDialogPayFail,
     iconDialogPayFailService,
@@ -702,6 +725,7 @@ class $AssetsImagesGen {
     iconSurpriseDialogButton,
     iconSurpriseDialogOnly,
     iconSurpriseDialogTitle,
+    iconTaAvatar,
     iconTabCharacterSelected,
     iconTabCharacterUnselect,
     iconTabKeyboardSelected,
@@ -709,6 +733,7 @@ class $AssetsImagesGen {
     iconTabMineSelected,
     iconTabMineUnselect,
     iconTicketDialogButton,
+    iconWechat,
     iconWechatPayment,
     iconWechatScanPayment,
   ];

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

@@ -88,9 +88,21 @@ class ColorName {
   /// Color: #BDBDBD
   static const Color disabledTextColor = Color(0xFFBDBDBD);
 
+  /// Color: #FF996DFF
+  static const Color inputCursor = Color(0xFF996DFF);
+
   /// Color: #FFFFFFFF
   static const Color inverseTextColor = Color(0xFFFFFFFF);
 
+  /// Color: #FFE6DCFF
+  static const Color msgBubbleMe = Color(0xFFE6DCFF);
+
+  /// Color: #FFF5F6FA
+  static const Color msgBubbleTa = Color(0xFFF5F6FA);
+
+  /// Color: #FFD3D5E1
+  static const Color msgInputBar = Color(0xFFD3D5E1);
+
   /// Color: #999999
   static const Color placeholderTextColor = Color(0xFF999999);
 

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

@@ -144,6 +144,12 @@ class StringName {
   static final String profileSave = 'profile_save'.tr; // 完成
   static final String profileEditSave = 'profile_edit_save'.tr; // 保存
   static final String keyboardMemberOpen = 'keyboard_member_open'.tr; // 开通会员
+  static final String keyboardGuideGoWechat = 'keyboard_guide_go_wechat'.tr; // 去微信体验
+  static final String keyboardGuideWechatNotInstall = 'keyboard_guide_wechat_not_install'.tr; // 未安装微信
+  static final String keyboardGuideInputHint = 'keyboard_guide_input_hint'.tr; // 选择粘贴TA的话,选择人设风格回复
+  static final String keyboardGuideTaReply1 = 'keyboard_guide_ta_reply1'.tr; // 👋 欢迎使用【追爱小键盘】\n复制任意一句对话,点击人设体验回复
+  static final String keyboardGuideTaReply2 = 'keyboard_guide_ta_reply2'.tr; // 你睡了吗?
+  static final String keyboardGuideTaReply3 = 'keyboard_guide_ta_reply3'.tr; // 我先去吃饭了,一会聊
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -291,6 +297,12 @@ class StringMultiSource {
       'profile_save': '完成',
       'profile_edit_save': '保存',
       'keyboard_member_open': '开通会员',
+      'keyboard_guide_go_wechat': '去微信体验',
+      'keyboard_guide_wechat_not_install': '未安装微信',
+      'keyboard_guide_input_hint': '选择粘贴TA的话,选择人设风格回复',
+      'keyboard_guide_ta_reply1': '👋 欢迎使用【追爱小键盘】\n复制任意一句对话,点击人设体验回复',
+      'keyboard_guide_ta_reply2': '你睡了吗?',
+      'keyboard_guide_ta_reply3': '我先去吃饭了,一会聊',
     },
   };
 }

+ 7 - 1
lib/router/app_pages.dart

@@ -24,6 +24,8 @@ import '../module/character_custom/character_custom_page.dart';
 import '../module/character_custom/detail/character_custom_detail_controller.dart';
 import '../module/character_custom/list/character_custom_list_page.dart';
 import '../module/feedback/feedback_page.dart';
+import '../module/keyboard_guide/keyboard_guide_controller.dart';
+import '../module/keyboard_guide/keyboard_guide_page.dart';
 import '../module/keyboard_manage/keyboard_manage_page.dart';
 import '../module/login/login_page.dart';
 import '../module/main/main_controller.dart';
@@ -51,6 +53,9 @@ abstract class RoutePath {
   static const store = '/store';
   static const profile = '/profile';
   static const profileEdit = '/profileEdit';
+
+  // 键盘引导页
+  static const keyboardGuide = '/keyboardGuide';
 }
 
 class AppBinding extends Bindings {
@@ -74,7 +79,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<DiscountController>());
     lazyPut(() => getIt.get<ProfileController>());
     lazyPut(() => getIt.get<ProfileEditController>());
-
+    lazyPut(() => getIt.get<KeyboardGuidePageController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -101,4 +106,5 @@ final generalPages = [
   GetPage(name: RoutePath.store, page: () => StorePage()),
   GetPage(name: RoutePath.profile, page: () => ProfilePage()),
   GetPage(name: RoutePath.profileEdit, page: () => ProfileEditPage()),
+  GetPage(name: RoutePath.keyboardGuide, page: () => KeyboardGuidePage()),
 ];

+ 15 - 0
lib/utils/clipboard_util.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/services.dart';
+
+/// 剪切板工具类
+class ClipboardUtil {
+  /// 复制文本到剪切板
+  static Future<void> copyToClipboard(String text) async {
+    await Clipboard.setData(ClipboardData(text: text));
+  }
+
+  /// 从剪切板获取文本
+  static Future<String> getClipboardText() async {
+    final data = await Clipboard.getData(Clipboard.kTextPlain);
+    return data?.text ?? "";
+  }
+}

+ 13 - 0
lib/utils/url_launcher_util.dart

@@ -0,0 +1,13 @@
+import 'package:url_launcher/url_launcher.dart';
+
+/// UrlLauncher工具类
+class UrlLauncherUtil {
+  /// 打开微信
+  static Future<bool> openWeChat() async {
+    const url = 'weixin://';
+    if (!await launchUrl(Uri.parse(url))) {
+      return Future.value(false);
+    }
+    return Future.value(true);
+  }
+}

+ 505 - 0
lib/widget/bubble/bubble_border_arrow_properties.dart

@@ -0,0 +1,505 @@
+import 'dart:math';
+import 'package:flutter/material.dart';
+
+class _BubbleBorderArrowProperties {
+  /// 箭头宽度的一半
+  final double halfWidth;
+
+  /// 箭头斜边的长度
+  final double hypotenuse;
+
+  /// 该斜边在主轴上的投影(水平时为X轴)
+  final double projectionOnMain;
+
+  /// 该斜边在纵轴上的投影(水平时为Y轴)
+  final double projectionOnCross;
+
+  /// 计算箭头半径在主轴上的投影(水平时为X轴)
+  final double arrowProjectionOnMain;
+
+  /// 计算箭头半径尖尖的长度
+  final double topLen;
+
+  _BubbleBorderArrowProperties({
+    required this.halfWidth,
+    required this.hypotenuse,
+    required this.projectionOnMain,
+    required this.projectionOnCross,
+    required this.arrowProjectionOnMain,
+    required this.topLen,
+  });
+}
+
+class BubbleShapeBorder extends OutlinedBorder {
+  final BorderRadius borderRadius;
+  final AxisDirection arrowDirection;
+  final double arrowLength;
+  final double arrowWidth;
+  final double arrowRadius;
+  final double? arrowOffset;
+  final Color? fillColor;
+
+  const BubbleShapeBorder({
+    super.side,
+    required this.arrowDirection,
+    this.borderRadius = BorderRadius.zero,
+    this.arrowLength = 12,
+    this.arrowWidth = 18,
+    this.arrowRadius = 3,
+    this.arrowOffset,
+    this.fillColor,
+  });
+
+  @override
+  OutlinedBorder copyWith({
+    AxisDirection? arrowDirection,
+    BorderSide? side,
+    BorderRadius? borderRadius,
+    double? arrowLength,
+    double? arrowWidth,
+    double? arrowRadius,
+    double? arrowOffset,
+    Color? fillColor,
+  }) {
+    return BubbleShapeBorder(
+      arrowDirection: arrowDirection ?? this.arrowDirection,
+      side: side ?? this.side,
+      borderRadius: borderRadius ?? this.borderRadius,
+      arrowLength: arrowLength ?? this.arrowLength,
+      arrowWidth: arrowWidth ?? this.arrowWidth,
+      arrowRadius: arrowRadius ?? this.arrowRadius,
+      arrowOffset: arrowOffset ?? this.arrowOffset,
+      fillColor: fillColor ?? this.fillColor,
+    );
+  }
+
+  @override
+  EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
+
+  @override
+  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
+    return _buildPath(rect);
+  }
+
+  @override
+  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
+    return _buildPath(rect);
+  }
+
+  _BubbleBorderArrowProperties _calculateArrowProperties() {
+    final arrowHalfWidth = arrowWidth / 2;
+    final double hypotenuse = sqrt(
+      arrowLength * arrowLength + arrowHalfWidth * arrowHalfWidth,
+    );
+    final double projectionOnMain = arrowHalfWidth * arrowRadius / hypotenuse;
+    final double projectionOnCross =
+        projectionOnMain * arrowLength / arrowHalfWidth;
+    final double arrowProjectionOnMain = arrowLength * arrowRadius / hypotenuse;
+    final double pointArrowTopLen =
+        arrowProjectionOnMain * arrowLength / arrowHalfWidth;
+    return _BubbleBorderArrowProperties(
+      halfWidth: arrowHalfWidth,
+      hypotenuse: hypotenuse,
+      projectionOnMain: projectionOnMain,
+      projectionOnCross: projectionOnCross,
+      arrowProjectionOnMain: arrowProjectionOnMain,
+      topLen: pointArrowTopLen,
+    );
+  }
+
+  /// 核心逻辑:构建路径
+  /// 计算方向为:上、右、下、左
+  ///
+  /// 爱今天灵眸(ijtkj.cn),一款可根据天气改变系统显示模式的软件,期待各位有钱的码友支持
+  Path _buildPath(Rect rect) {
+    final path = Path();
+    EdgeInsets padding = EdgeInsets.zero;
+    if (arrowDirection == AxisDirection.up) {
+      padding = EdgeInsets.only(top: arrowLength);
+    } else if (arrowDirection == AxisDirection.right) {
+      padding = EdgeInsets.only(right: arrowLength);
+    } else if (arrowDirection == AxisDirection.down) {
+      padding = EdgeInsets.only(bottom: arrowLength);
+    } else if (arrowDirection == AxisDirection.left) {
+      padding = EdgeInsets.only(left: arrowLength);
+    }
+    final nRect = Rect.fromLTRB(
+      rect.left + padding.left,
+      rect.top + padding.top,
+      rect.right - padding.right,
+      rect.bottom - padding.bottom,
+    );
+
+    final arrowProp = _calculateArrowProperties();
+
+    final startPoint = Offset(nRect.left + borderRadius.topLeft.x, nRect.top);
+
+    path.moveTo(startPoint.dx, startPoint.dy);
+    // 箭头在上边
+    if (arrowDirection == AxisDirection.up) {
+      Offset pointCenter = Offset(
+        nRect.left + (arrowOffset ?? nRect.width / 2),
+        nRect.top,
+      );
+      Offset pointStart = Offset(
+        pointCenter.dx - arrowProp.halfWidth,
+        nRect.top,
+      );
+      Offset pointArrow = Offset(pointCenter.dx, rect.top);
+      Offset pointEnd = Offset(pointCenter.dx + arrowProp.halfWidth, nRect.top);
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx - arrowRadius,
+          pointStart.dy,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx + arrowProp.projectionOnMain,
+          pointStart.dy - arrowProp.projectionOnCross,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx - arrowProp.arrowProjectionOnMain,
+          pointArrow.dy + arrowProp.topLen,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx + arrowProp.arrowProjectionOnMain,
+          pointArrow.dy + arrowProp.topLen,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx - arrowProp.projectionOnMain,
+          pointEnd.dy - arrowProp.projectionOnCross,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx + arrowRadius, pointEnd.dy);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.right - borderRadius.topRight.x, nRect.top);
+    // topRight radius
+    path.arcToPoint(
+      Offset(nRect.right, nRect.top + borderRadius.topRight.y),
+      radius: borderRadius.topRight,
+      rotation: 90,
+    );
+
+    // 箭头在右边
+    if (arrowDirection == AxisDirection.right) {
+      Offset pointCenter = Offset(
+        nRect.right,
+        nRect.top + (arrowOffset ?? nRect.height / 2),
+      );
+      Offset pointStart = Offset(
+        nRect.right,
+        pointCenter.dy - arrowProp.halfWidth,
+      );
+      Offset pointArrow = Offset(rect.right, pointCenter.dy);
+      Offset pointEnd = Offset(
+        nRect.right,
+        pointCenter.dy + arrowProp.halfWidth,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx,
+          pointStart.dy - arrowRadius,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx + arrowProp.projectionOnCross,
+          pointStart.dy + arrowProp.projectionOnMain,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx - arrowProp.topLen,
+          pointArrow.dy - arrowProp.arrowProjectionOnMain,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx - arrowProp.topLen,
+          pointArrow.dy + arrowProp.arrowProjectionOnMain,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx + arrowProp.projectionOnCross,
+          pointEnd.dy - arrowProp.projectionOnMain,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy + arrowRadius);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.right, nRect.bottom - borderRadius.bottomRight.y);
+    // bottomRight radius
+    path.arcToPoint(
+      Offset(nRect.right - borderRadius.bottomRight.x, nRect.bottom),
+      radius: borderRadius.bottomRight,
+      rotation: 90,
+    );
+
+    // 箭头在下边
+    if (arrowDirection == AxisDirection.down) {
+      Offset pointCenter = Offset(
+        nRect.left + (arrowOffset ?? nRect.width / 2),
+        nRect.bottom,
+      );
+      Offset pointStart = Offset(
+        pointCenter.dx + arrowProp.halfWidth,
+        nRect.bottom,
+      );
+      Offset pointArrow = Offset(pointCenter.dx, rect.bottom);
+      Offset pointEnd = Offset(
+        pointCenter.dx - arrowProp.halfWidth,
+        nRect.bottom,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx + arrowRadius,
+          pointStart.dy,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx - arrowProp.projectionOnMain,
+          pointStart.dy + arrowProp.projectionOnCross,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx + arrowProp.arrowProjectionOnMain,
+          pointArrow.dy - arrowProp.topLen,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx - arrowProp.arrowProjectionOnMain,
+          pointArrow.dy - arrowProp.topLen,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx + arrowProp.projectionOnMain,
+          pointEnd.dy + arrowProp.projectionOnCross,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx - arrowRadius, pointEnd.dy);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.left + borderRadius.bottomLeft.x, nRect.bottom);
+    // bottomLeft radius
+    path.arcToPoint(
+      Offset(nRect.left, nRect.bottom - borderRadius.bottomRight.y),
+      radius: borderRadius.bottomLeft,
+      rotation: 90,
+    );
+
+    // 箭头在左边
+    if (arrowDirection == AxisDirection.left) {
+      Offset pointCenter = Offset(
+        nRect.left,
+        nRect.top + (arrowOffset ?? nRect.height / 2),
+      );
+      Offset pointStart = Offset(
+        nRect.left,
+        pointCenter.dy + arrowProp.halfWidth,
+      );
+      Offset pointArrow = Offset(rect.left, pointCenter.dy);
+      Offset pointEnd = Offset(
+        nRect.left,
+        pointCenter.dy - arrowProp.halfWidth,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx,
+          pointStart.dy + arrowRadius,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx - arrowProp.projectionOnCross,
+          pointStart.dy - arrowProp.projectionOnMain,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx + arrowProp.topLen,
+          pointArrow.dy + arrowProp.arrowProjectionOnMain,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx + arrowProp.topLen,
+          pointArrow.dy - arrowProp.arrowProjectionOnMain,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx - arrowProp.projectionOnCross,
+          pointEnd.dy + arrowProp.projectionOnMain,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy - arrowRadius);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.left, nRect.top + borderRadius.topLeft.y);
+    path.arcToPoint(startPoint, radius: borderRadius.topLeft, rotation: 90);
+
+    return path;
+  }
+
+  @override
+  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
+    if (fillColor == null && side == BorderSide.none) {
+      return;
+    }
+
+    final path = _buildPath(rect);
+    final Paint paint =
+        Paint()
+          ..color = side.color
+          ..style = PaintingStyle.stroke;
+    if (fillColor != null) {
+      paint.color = fillColor!;
+      paint.style = PaintingStyle.fill;
+      canvas.drawPath(path, paint);
+    }
+    if (side != BorderSide.none) {
+      paint.color = side.color;
+      paint.strokeWidth = side.width;
+      paint.style = PaintingStyle.stroke;
+      canvas.drawPath(path, paint);
+    }
+  }
+
+  @override
+  ShapeBorder scale(double t) {
+    return BubbleShapeBorder(
+      arrowDirection: arrowDirection,
+      side: side.scale(t),
+      borderRadius: borderRadius * t,
+      arrowLength: arrowLength * t,
+      arrowWidth: arrowWidth * t,
+      arrowRadius: arrowRadius * t,
+      arrowOffset: (arrowOffset ?? 0) * t,
+    );
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is BubbleShapeBorder &&
+        other.side == side &&
+        other.borderRadius == borderRadius &&
+        other.arrowLength == arrowLength &&
+        other.arrowWidth == arrowWidth &&
+        other.arrowRadius == arrowRadius &&
+        other.arrowDirection == arrowDirection &&
+        other.arrowOffset == arrowOffset &&
+        other.fillColor == fillColor;
+  }
+
+  @override
+  int get hashCode => Object.hash(
+    side,
+    borderRadius,
+    arrowLength,
+    arrowWidth,
+    arrowRadius,
+    arrowDirection,
+    arrowOffset,
+    fillColor,
+  );
+}

+ 68 - 0
lib/widget/bubble/bubble_widget.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'bubble_border_arrow_properties.dart';
+
+/// 气泡组件
+class BubbleWidget extends StatelessWidget {
+  final BorderSide border;
+  final AxisDirection arrowDirection;
+  final BorderRadius? borderRadius;
+  final double arrowLength;
+  final double arrowWidth;
+  final double? arrowOffset;
+  final double arrowRadius;
+  final Color? backgroundColor;
+  final EdgeInsets? padding;
+  final WidgetBuilder contentBuilder;
+  final List<BoxShadow>? shadows;
+  final EdgeInsetsGeometry? margin;
+
+  const BubbleWidget({
+    super.key,
+    required this.arrowDirection,
+    this.arrowOffset,
+    required this.contentBuilder,
+    this.border = BorderSide.none,
+    this.borderRadius,
+    this.arrowLength = 10,
+    this.arrowWidth = 17,
+    this.arrowRadius = 3,
+    this.backgroundColor,
+    this.shadows,
+    this.padding,
+    this.margin,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    EdgeInsets bubblePadding = EdgeInsets.zero;
+    if (arrowDirection == AxisDirection.up) {
+      bubblePadding = EdgeInsets.only(top: arrowLength);
+    } else if (arrowDirection == AxisDirection.down) {
+      bubblePadding = EdgeInsets.only(bottom: arrowLength);
+    } else if (arrowDirection == AxisDirection.left) {
+      bubblePadding = EdgeInsets.only(left: arrowLength);
+    } else if (arrowDirection == AxisDirection.right) {
+      bubblePadding = EdgeInsets.only(right: arrowLength);
+    }
+    return Container(
+      margin: margin,
+      decoration: ShapeDecoration(
+        shape: BubbleShapeBorder(
+          side: border,
+          arrowDirection: arrowDirection,
+          borderRadius: borderRadius ?? BorderRadius.circular(4),
+          arrowLength: arrowLength,
+          arrowWidth: arrowWidth,
+          arrowRadius: arrowRadius,
+          arrowOffset: arrowOffset,
+          fillColor: backgroundColor ?? const Color.fromARGB(255, 65, 65, 65),
+        ),
+        shadows: shadows,
+      ),
+      child: Padding(
+        padding: bubblePadding.add(padding ?? EdgeInsets.zero),
+        child: contentBuilder(context),
+      ),
+    );
+  }
+}

+ 1 - 1
pubspec.lock

@@ -1337,7 +1337,7 @@ packages:
     source: hosted
     version: "1.4.0"
   url_launcher:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: url_launcher
       sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"

+ 4 - 0
pubspec.yaml

@@ -76,6 +76,9 @@ dependencies:
   # 动画
   lottie: ^3.3.1
 
+  # url跳转
+  url_launcher: ^6.3.1
+
   #android日志打印
   atmob_logging:
     version: ^0.0.5
@@ -140,6 +143,7 @@ flutter_gen:
   output: lib/resource/
   colors:
     inputs:
+      - assets/color/business_color.xml
       - assets/color/common_color.xml
       - assets/color/color.xml