气泡滑动交互重构与多选功能恢复

January 11, 2026
5 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

涉及文件:

  • chat_page.dart
  • widgets/chat/animations.dart
  • widgets/chat/message_bubbles.dart
  • widgets/chat/message_detail_sheet.dart

一、背景与需求

1.1 原有交互模式

聊天气泡原有三种交互模式:

交互方式功能实现组件
长按弹出上下文菜单(复制、删除、划选、多选等)MessageBubble.onLongPress
左滑进入多选模式SwipeToSelectMessage
划选选中气泡内文本(isSelectable 模式)MessageBubble.isSelectable

1.2 需求变更

我明确要求:

  1. 滑动气泡 → 不再进入多选,改为弹出详情/编辑界面
  2. 删除划选功能 → 长按菜单里的”划选”选项,以及 isSelectable 相关代码
  3. 保留多选功能 → 通过长按菜单进入,而不是滑动

关键澄清

  • 划选 (isSelectable) - 选中气泡内文字的功能 → 删除
  • 多选 (_isMultiSelectMode) - 批量选择消息的功能 → 保留

二、概念区分(重要)

这次任务的核心难点在于划选多选两个概念极易混淆:

2.1 划选(Text Selection)

// message_bubbles.dart
class MessageBubble extends StatefulWidget {
  /// 是否处于划选模式
  final bool isSelectable;  // ← 这是划选
  
  /// 退出划选模式回调
  final VoidCallback? onExitSelectable;  // ← 这是划选
}

划选模式的 UI 特征:

  • 文本变为可选择状态(SelectableText
  • 气泡底部显示”完成划选”按钮
  • 用于复制部分文本内容

2.2 多选(Multi-Select)

// chat_page.dart
class _ChatPageState extends State<ChatPage> {
  bool _isMultiSelectMode = false;  // ← 这是多选
  final Set<String> _selectedMessageIds = {};  // ← 这是多选
}

多选模式的 UI 特征:

  • AppBar 变为”已选择 N 项”
  • 每条消息左侧显示选择图标(空心/实心圆)
  • 点击消息切换选中状态
  • 支持批量删除

三、执行过程

3.1 第一阶段:重构滑动组件

目标:将 SwipeToSelectMessage(左滑进入多选)改为 SwipeToEditMessage(左滑弹出详情)

修改文件animations.dart

// Before
class SwipeToSelectMessage extends StatefulWidget {
  final VoidCallback? onSwipeSelect;  // 进入多选
  // 图标:TablerIcons.checkbox(蓝色)
}
 
// After
class SwipeToEditMessage extends StatefulWidget {
  final VoidCallback? onSwipeEdit;  // 弹出详情
  // 图标:TablerIcons.info_circle(灰色)
}

关键改动

  1. 类名:SwipeToSelectMessageSwipeToEditMessage
  2. 回调:onSwipeSelectonSwipeEdit
  3. 阈值变量:_selectThreshold_editThreshold
  4. 图标:蓝色多选框 → 灰色详情图标
  5. 图标颜色:浅蓝→深蓝 变为 浅灰→深灰

3.2 第二阶段:删除划选功能

修改文件message_bubbles.dart

删除内容:

  1. 属性 isSelectableonExitSelectable
  2. _onTapDown 等方法中的 if (widget.isSelectable) return; 判断
  3. _buildTextContent 中的划选模式 UI(SelectableText + 完成按钮)

3.3 第三阶段:修复编译错误

问题 1_formatDateTime 方法缺失

message_detail_sheet.dart 使用了 _formatDateTime 但没有定义。

修复:添加方法

String _formatDateTime(DateTime dt) {
  return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
      '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}

问题 2:不必要的 import

chat_page.dart 导入了 animations.dart,但 SwipeToEditMessage 已通过 chat_widgets.dart 导出。

修复:删除多余导入


四、问题发现与恢复

4.1 发现多选功能丢失

我发现“多选功能不见了”,通过 grep 确认:

grep -n "_isMultiSelectMode\|_selectedMessageIds" chat_page.dart
# 结果:0 matches

多选功能确实被删除了。

4.2 问题根因分析

查看任务历史,发现在之前的会话中存在任务:

[COMPLETE] ID:swipe3 CONTENT:chat_page.dart: 删除多选相关逻辑(_isMultiSelectMode, _selectedMessageIds 等)

这是之前会话误解需求导致的错误:

  • 我决定删除”划选”,但任务描述写成了”删除多选”
  • 之前会话执行了这个错误的任务

4.3 恢复多选功能

需要恢复的内容:

4.3.1 状态变量

// === 多选模式状态 ===
bool _isMultiSelectMode = false;
final Set<String> _selectedMessageIds = {};

4.3.2 长按菜单入口

// 所有消息:多选
items.add(
  ContextMenuItem(
    icon: TablerIcons.checkbox,
    label: '多选',
    onTap: () => _enterMultiSelectMode(message),
  ),
);

4.3.3 多选相关方法

/// 进入多选模式
void _enterMultiSelectMode(SendMessage message) {
  setState(() {
    _isMultiSelectMode = true;
    _selectedMessageIds.clear();
    _selectedMessageIds.add(message.id);
  });
}
 
/// 退出多选模式
void _exitMultiSelectMode() {
  setState(() {
    _isMultiSelectMode = false;
    _selectedMessageIds.clear();
  });
}
 
/// 切换消息选中状态
void _toggleMessageSelection(String messageId) {
  setState(() {
    if (_selectedMessageIds.contains(messageId)) {
      _selectedMessageIds.remove(messageId);
    } else {
      _selectedMessageIds.add(messageId);
    }
  });
}
 
/// 删除选中的消息
Future<void> _deleteSelectedMessages() async {
  // 确认对话框 + 批量删除逻辑
}

4.3.4 多选模式 AppBar

PreferredSizeWidget _buildMultiSelectAppBar(bool isDark) {
  return AppBar(
    leading: /* X 按钮退出多选 */,
    title: Text('已选择 ${_selectedMessageIds.length} 项'),
    actions: [/* 删除按钮 */],
  );
}

4.3.5 AppBar 选择逻辑

appBar: isDesktop
    ? null
    : (_isMultiSelectMode
        ? _buildMultiSelectAppBar(isDark)
        : (_isSearchMode
            ? _buildSearchAppBar(isDark)
            : _buildAppBar(isDark))),

4.3.6 消息 UI 显示选择图标

// 多选模式:显示选择图标
if (_isMultiSelectMode) {
  final isSelected = _selectedMessageIds.contains(message.id);
  finalWidget = GestureDetector(
    onTap: () => _toggleMessageSelection(message.id),
    behavior: HitTestBehavior.opaque,
    child: Row(
      children: [
        // 选择图标
        Icon(
          isSelected ? TablerIcons.circle_check_filled : TablerIcons.circle,
          color: isSelected ? primaryColor : Colors.grey[400],
        ),
        // 消息内容
        Expanded(child: finalWidget),
      ],
    ),
  );
}

五、API 调用修正

批量删除时使用了错误的 API 方法名:

// 错误
await appState.api.deleteMessage(widget.session.id, id);
 
// 正确
await appState.api.deleteSendMessage(id);

六、最终交互流程

正常状态
├── 长按(200ms) ──→ 弹出菜单
│ │
│ ├── 复制 ──→ 复制到剪贴板
│ ├── 详情 ──→ 弹出详情弹窗
│ ├── 多选 ──→ 进入多选模式 ★ 保留
│ └── 删除 ──→ 确认删除
├── 左滑 ──→ 弹出详情弹窗 ★ 新行为
└── 点击 ──→ 显示时间
多选模式
├── 点击消息 ──→ 切换选中状态
├── 删除按钮 ──→ 批量删除
└── 返回/X ──→ 退出多选

七、教训与反思

7.1 概念混淆的危害

“划选”和”多选”两个概念在中文语境下极易混淆:

  • 划选:drag to select text(选中文本)
  • 多选:multi-select messages(批量选择消息)

教训:在执行删除/重构操作前,必须先通过 grep 确认目标代码的存在和影响范围。

7.2 任务历史的可靠性

任务列表中的 COMPLETE 状态不代表正确性。当我明确强调“不要删除 X”时,应该:

  1. 先检查:grep 确认 X 是否还存在
  2. 再执行:如果已被删除,优先恢复而不是继续其他任务
  3. 后验证:执行后再次 grep 确认

7.3 明确强调的内容是核心约束

这个点我重复强调了三次“不要删除多选功能”,属于硬性约束,不是可选建议。


八、涉及的文件变更总结

文件操作描述
animations.dart重构SwipeToSelectMessageSwipeToEditMessage
message_bubbles.dart删除移除 isSelectable 划选功能
message_detail_sheet.dart添加_formatDateTime() 方法
chat_page.dart删除+添加删除多余 import,恢复多选功能

九、验证结果

$ flutter analyze --no-fatal-infos
No issues found!

所有功能恢复正常,代码编译通过。