@@ -2,6 +2,7 @@ import 'dart:convert';
22import 'dart:io' ;
33
44import 'package:flutter/material.dart' ;
5+ import 'package:flutter/services.dart' ;
56import 'package:flutter_riverpod/flutter_riverpod.dart' ;
67import 'package:drift/drift.dart' hide Column;
78
@@ -352,19 +353,25 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
352353 final isUndone = metadata['isUndone' ] == true ;
353354 final billInfo = BillInfo .fromJson (metadata['billInfo' ] ?? metadata);
354355
355- return BillCardWidget (
356- billInfo: billInfo,
357- transactionId: message.transactionId,
358- isUndone: isUndone,
359- onUndo: message.transactionId != null && ! isUndone
360- ? () => _handleUndo (message.id, message.transactionId! )
361- : null ,
362- onEdit: message.transactionId != null && ! isUndone
363- ? () => _handleEdit (message.transactionId! )
364- : null ,
365- onChangeLedger: message.transactionId != null && ! isUndone
366- ? () => _handleChangeLedger (message.id, message.transactionId! )
367- : null ,
356+ return GestureDetector (
357+ onLongPressStart: (details) => _showBillCardMenu (
358+ details.globalPosition,
359+ message,
360+ ),
361+ child: BillCardWidget (
362+ billInfo: billInfo,
363+ transactionId: message.transactionId,
364+ isUndone: isUndone,
365+ onUndo: message.transactionId != null && ! isUndone
366+ ? () => _handleUndo (message.id, message.transactionId! )
367+ : null ,
368+ onEdit: message.transactionId != null && ! isUndone
369+ ? () => _handleEdit (message.transactionId! )
370+ : null ,
371+ onChangeLedger: message.transactionId != null && ! isUndone
372+ ? () => _handleChangeLedger (message.id, message.transactionId! )
373+ : null ,
374+ ),
368375 );
369376 }
370377
@@ -383,49 +390,56 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
383390 ],
384391 // 消息气泡
385392 Flexible (
386- child: Container (
387- margin: EdgeInsets .only (
388- left: isUser ? 60.0 .scaled (context, ref) : 0 ,
389- right: isUser ? 0 : 60.0 .scaled (context, ref),
390- ),
391- padding: EdgeInsets .symmetric (
392- horizontal: 12.0 .scaled (context, ref),
393- vertical: 10.0 .scaled (context, ref),
393+ child: GestureDetector (
394+ onLongPressStart: (details) => _showTextMessageMenu (
395+ details.globalPosition,
396+ message,
397+ isUser,
394398 ),
395- decoration: BoxDecoration (
396- color: isUser
397- ? ref.watch (primaryColorProvider).withOpacity (0.1 )
398- : BeeTokens .surface (context),
399- borderRadius: BorderRadius .circular (12.0 .scaled (context, ref)),
400- border: Border .all (
399+ child: Container (
400+ margin: EdgeInsets .only (
401+ left: isUser ? 60.0 .scaled (context, ref) : 0 ,
402+ right: isUser ? 0 : 60.0 .scaled (context, ref),
403+ ),
404+ padding: EdgeInsets .symmetric (
405+ horizontal: 12.0 .scaled (context, ref),
406+ vertical: 10.0 .scaled (context, ref),
407+ ),
408+ decoration: BoxDecoration (
401409 color: isUser
402- ? ref.watch (primaryColorProvider).withOpacity (0.3 )
403- : BeeTokens .border (context),
410+ ? ref.watch (primaryColorProvider).withOpacity (0.1 )
411+ : BeeTokens .surface (context),
412+ borderRadius: BorderRadius .circular (12.0 .scaled (context, ref)),
413+ border: Border .all (
414+ color: isUser
415+ ? ref.watch (primaryColorProvider).withOpacity (0.3 )
416+ : BeeTokens .border (context),
417+ ),
404418 ),
405- ),
406- child: TypewriterText (
407- text: message.content,
408- animate: shouldAnimate, // 只对标记的消息启用动画
409- onTextChange: shouldAnimate
410- ? () {
411- // 每次文本更新时滚动到底部
412- _scrollToBottomSmooth ();
413- }
414- : null ,
415- onComplete: shouldAnimate
416- ? () {
417- // 动画完成后清除标记
418- if (mounted) {
419- setState (() {
420- _animatingMessageId = null ;
421- });
419+ child: TypewriterText (
420+ text: message.content,
421+ animate: shouldAnimate, // 只对标记的消息启用动画
422+ onTextChange: shouldAnimate
423+ ? () {
424+ // 每次文本更新时滚动到底部
425+ _scrollToBottomSmooth ();
422426 }
423- }
424- : null ,
425- style: TextStyle (
426- color: BeeTokens .textPrimary (context),
427- fontSize: 14.0 .scaled (context, ref),
428- height: 1.5 ,
427+ : null ,
428+ onComplete: shouldAnimate
429+ ? () {
430+ // 动画完成后清除标记
431+ if (mounted) {
432+ setState (() {
433+ _animatingMessageId = null ;
434+ });
435+ }
436+ }
437+ : null ,
438+ style: TextStyle (
439+ color: BeeTokens .textPrimary (context),
440+ fontSize: 14.0 .scaled (context, ref),
441+ height: 1.5 ,
442+ ),
429443 ),
430444 ),
431445 ),
@@ -1002,6 +1016,81 @@ class _AIChatPageState extends ConsumerState<AIChatPage>
10021016 }
10031017 }
10041018
1019+ /// 显示文字消息的长按菜单
1020+ void _showTextMessageMenu (Offset position, Message message, bool isUser) {
1021+ final l10n = AppLocalizations .of (context);
1022+ final primaryColor = ref.read (primaryColorProvider);
1023+
1024+ MessagePopoverMenu .show (
1025+ context: context,
1026+ globalPosition: position,
1027+ primaryColor: primaryColor,
1028+ items: [
1029+ PopoverMenuItem (
1030+ icon: Icons .copy,
1031+ label: l10n.aiChatCopy,
1032+ onTap: () {
1033+ Clipboard .setData (ClipboardData (text: message.content));
1034+ showToast (context, l10n.aiChatCopied);
1035+ },
1036+ ),
1037+ PopoverMenuItem (
1038+ icon: Icons .delete_outline,
1039+ label: l10n.commonDelete,
1040+ color: Colors .red,
1041+ onTap: () => _deleteMessage (message),
1042+ ),
1043+ ],
1044+ );
1045+ }
1046+
1047+ /// 显示记账卡片的长按菜单
1048+ void _showBillCardMenu (Offset position, Message message) {
1049+ final l10n = AppLocalizations .of (context);
1050+ final primaryColor = ref.read (primaryColorProvider);
1051+
1052+ MessagePopoverMenu .show (
1053+ context: context,
1054+ globalPosition: position,
1055+ primaryColor: primaryColor,
1056+ items: [
1057+ PopoverMenuItem (
1058+ icon: Icons .delete_outline,
1059+ label: l10n.commonDelete,
1060+ color: Colors .red,
1061+ onTap: () => _deleteMessage (message),
1062+ ),
1063+ ],
1064+ );
1065+ }
1066+
1067+ /// 删除单条消息
1068+ Future <void > _deleteMessage (Message message) async {
1069+ final l10n = AppLocalizations .of (context);
1070+
1071+ // 确认删除
1072+ final confirmed = await AppDialog .confirm <bool >(
1073+ context,
1074+ title: l10n.commonDelete,
1075+ message: l10n.aiChatDeleteMessageConfirm,
1076+ );
1077+
1078+ if (confirmed != true ) return ;
1079+
1080+ try {
1081+ final repo = ref.read (repositoryProvider);
1082+ await repo.deleteMessage (message.id);
1083+
1084+ if (mounted) {
1085+ showToast (context, l10n.aiChatMessageDeleted);
1086+ }
1087+ } catch (e) {
1088+ if (mounted) {
1089+ showToast (context, l10n.commonFailed);
1090+ }
1091+ }
1092+ }
1093+
10051094 @override
10061095 void dispose () {
10071096 WidgetsBinding .instance.removeObserver (this );
0 commit comments