From eca0a3fd5a916fcd15b53e756de7e1dfe1b7a8c6 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Sat, 10 Jan 2026 00:09:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(call):=20=E6=B7=BB=E5=8A=A0=E5=BF=99?= =?UTF-8?q?=E7=BA=BF=E6=8F=90=E7=A4=BA=E5=B9=B6=E4=BC=98=E5=8C=96=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=BE=93=E5=85=A5=E6=A1=86=E8=A1=A8=E6=83=85=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加对方忙线中提示功能 - 移除过期的extended_text_field依赖 - 实现新的表情显示机制替换原有特殊文本构建器 - 重构聊天输入框表情处理逻辑 - 优化输入框光标位置管理 - 添加表情输入内容构建方法 --- lib/controller/message/call_controller.dart | 4 + lib/widget/message/chat_input_bar.dart | 257 ++++++++------------ pubspec.yaml | 1 + 3 files changed, 103 insertions(+), 159 deletions(-) diff --git a/lib/controller/message/call_controller.dart b/lib/controller/message/call_controller.dart index cb7f7f8..bc94f03 100644 --- a/lib/controller/message/call_controller.dart +++ b/lib/controller/message/call_controller.dart @@ -184,6 +184,10 @@ class CallController extends GetxController { }); return null; } + if (!response.data.data!.success && response.data.data!.code == 'E0003') { + SmartDialog.showToast('对方忙线中'); + return null; + } rtcChannel.value = response.data.data; print('✅ 创建一对一RTC频道成功: ${response.data.data?.channelId}'); isCreatingChannel.value = false; diff --git a/lib/widget/message/chat_input_bar.dart b/lib/widget/message/chat_input_bar.dart index db31b12..bfbd66a 100644 --- a/lib/widget/message/chat_input_bar.dart +++ b/lib/widget/message/chat_input_bar.dart @@ -1,8 +1,6 @@ import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:extended_text_field/extended_text_field.dart'; import '../../generated/assets.dart'; import '../../config/emoji_config.dart'; @@ -10,135 +8,6 @@ import '../emoji_panel.dart'; import 'more_options_view.dart'; import 'voice_input_view.dart'; -/// 表情特殊文本构建器 - 用于在 ExtendedTextField 中显示表情 -class EmojiSpecialTextSpanBuilder extends SpecialTextSpanBuilder { - @override - SpecialText? createSpecialText( - String flag, { - TextStyle? textStyle, - SpecialTextGestureTapCallback? onTap, - required int index, - }) { - // 匹配 [emoji:xxx] 格式 - if (flag.startsWith('[emoji:')) { - return EmojiSpecialText( - textStyle: textStyle, - onTap: onTap, - ); - } - return null; - } -} - -/// 表情特殊文本类 -class EmojiSpecialText extends SpecialText { - EmojiSpecialText({ - TextStyle? textStyle, - SpecialTextGestureTapCallback? onTap, - }) : super( - '[emoji:', - ']', - textStyle, - onTap: onTap, - ); - - @override - InlineSpan finishText() { - // 提取表情ID - final emojiId = toString().replaceAll('[emoji:', '').replaceAll(']', ''); - final emoji = EmojiConfig.getEmojiById(emojiId); - - if (emoji != null) { - // 返回包含表情图片的 WidgetSpan - return WidgetSpan( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 2.w), - child: Image.asset( - emoji.path, - width: 24.w, - height: 24.w, - fit: BoxFit.contain, - ), - ), - ); - } - - // 如果表情不存在,返回普通文本 - return TextSpan( - text: toString(), - style: textStyle, - ); - } -} - -/// 表情文本输入格式化器 - 处理删除时一次性删除整个表情标记 -class EmojiTextInputFormatter extends TextInputFormatter { - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - // 如果文本长度减少(删除操作) - if (newValue.text.length < oldValue.text.length) { - final oldSelection = oldValue.selection; - final cursorOffset = oldSelection.baseOffset; - final deletedLength = oldValue.text.length - newValue.text.length; - - // 在整个文本中查找所有表情标记 - final emojiRegex = RegExp(r'\[emoji:\d+\]'); - final allMatches = emojiRegex.allMatches(oldValue.text); - - // 查找光标所在位置或光标前最近的表情标记 - for (final match in allMatches) { - final emojiStart = match.start; - final emojiEnd = match.end; - - // 如果光标在表情标记内部(不包括开始位置,包括结束位置) - if (cursorOffset > emojiStart && cursorOffset <= emojiEnd) { - // 删除整个表情标记 - final beforeEmoji = oldValue.text.substring(0, emojiStart); - final afterEmoji = oldValue.text.substring(emojiEnd); - final newText = beforeEmoji + afterEmoji; - - // 删除表情后,光标保持在删除操作发生的位置 - // 跟删除普通文本一样:删除后光标停留在删除位置 - // 光标位置 = 删除位置 - 表情长度 - final emojiLength = emojiEnd - emojiStart; - int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length); - - return TextEditingValue( - text: newText, - selection: TextSelection.collapsed(offset: newCursorOffset), - ); - } - - // 如果光标刚好在表情标记的开始位置,且删除的是表情标记内部的字符 - if (cursorOffset == emojiStart) { - // 检查删除的字符是否在表情标记范围内 - if (cursorOffset + deletedLength <= emojiEnd) { - // 删除整个表情标记 - final beforeEmoji = oldValue.text.substring(0, emojiStart); - final afterEmoji = oldValue.text.substring(emojiEnd); - final newText = beforeEmoji + afterEmoji; - - // 删除表情后,光标保持在删除位置 - // 光标位置 = 删除位置 - 表情长度 - final emojiLength = emojiEnd - emojiStart; - int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length); - - return TextEditingValue( - text: newText, - selection: TextSelection.collapsed(offset: newCursorOffset), - ); - } - } - } - } - - return newValue; - } -} - class ChatInputBar extends StatefulWidget { final ValueChanged onSendMessage; final ValueChanged>? onImageSelected; @@ -260,15 +129,71 @@ class _ChatInputBarState extends State { // 将表情添加到输入框 final currentText = _textController.text; final emojiText = '[emoji:${emoji.id}]'; - final newText = currentText + emojiText; - _textController.text = newText; + _textController.text = currentText + emojiText; // 将光标移到末尾 _textController.selection = TextSelection.fromPosition( - TextPosition(offset: newText.length), + TextPosition(offset: _textController.text.length), ); setState(() {}); // 刷新显示 } + /// 构建输入框内容(文本+表情) + List _buildInputContentWidgets() { + final List widgets = []; + final text = _textController.text; + final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); + + int lastMatchEnd = 0; + final matches = emojiRegex.allMatches(text); + + for (final match in matches) { + // 添加表情之前的文本 + if (match.start > lastMatchEnd) { + final textPart = text.substring(lastMatchEnd, match.start); + widgets.add( + Text( + textPart, + style: TextStyle(fontSize: 14.sp, color: Colors.black), + ), + ); + } + + // 添加表情图片 + final emojiId = match.group(1); + if (emojiId != null) { + final emoji = EmojiConfig.getEmojiById(emojiId); + if (emoji != null) { + widgets.add( + Padding( + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Image.asset( + emoji.path, + width: 24.w, + height: 24.w, + fit: BoxFit.contain, + ), + ), + ); + } + } + + lastMatchEnd = match.end; + } + + // 添加剩余的文本 + if (lastMatchEnd < text.length) { + final textPart = text.substring(lastMatchEnd); + widgets.add( + Text( + textPart, + style: TextStyle(fontSize: 14.sp, color: Colors.black), + ), + ); + } + + return widgets; + } + @override Widget build(BuildContext context) { return Column( @@ -290,32 +215,46 @@ class _ChatInputBarState extends State { borderRadius: BorderRadius.circular(5.h), ), padding: EdgeInsets.symmetric(horizontal: 16.w), - alignment: Alignment.center, - child: ExtendedTextField( - controller: _textController, - focusNode: _focusNode, - specialTextSpanBuilder: EmojiSpecialTextSpanBuilder(), - inputFormatters: [ - EmojiTextInputFormatter(), - ], - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - border: InputBorder.none, - hintText: "请输入聊天内容~", - hintStyle: TextStyle( - fontSize: 14.sp, - color: Colors.grey, + child: Stack( + children: [ + // 真实的输入框 + TextField( + controller: _textController, + focusNode: _focusNode, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "请输入聊天内容~", + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: _textController.text.contains('[emoji:') + ? Colors.transparent + : Colors.black, + ), + onChanged: (value) { + setState(() {}); // 刷新以更新表情显示 + }, ), - isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 0), - ), - style: TextStyle( - fontSize: 14.sp, - color: Colors.black, - ), - onChanged: (value) { - setState(() {}); // 刷新以更新表情显示 - }, + // 表情显示层 + if (_textController.text.contains('[emoji:')) + Positioned.fill( + child: IgnorePointer( + child: Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _buildInputContentWidgets(), + ), + ), + ), + ), + ), + ], ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 64acfe9..58be826 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: flutter_local_notifications: ^19.5.0 app_badge_plus: ^1.2.6 extended_text_field: ^16.0.2 + emoji_text_field: ^1.0.0 dev_dependencies: flutter_test: