# 表情 / 贴纸 / 附件面板架构设计文档 > Gitea Issues: #51–#56 > Commit: `bb9f1aa` > 参考:iOS `StickerMessageBubble`, `AttachmentPanelView`, `EmojiKeyboardView` --- ## 一、功能范围 | Issue | 功能 | 状态 | |-------|------|------| | #51 | StickerMessageBubble(typ=5,无边框 120pt CDN 图) | ✅ | | #52 | AttachmentPanelSheet(6 格面板) | ✅ | | #53 | 拍照发送(image_picker.camera → SendImageUseCase) | ✅ | | #54 | SendVideoUseCase(video picker → CDN 上传 → typ=4 发送) | ✅ | | #55 | 文件发送(file_picker 占位,SnackBar 提示) | ⏳ 待接入 | | #56 | EmojiPanel(4 分类 ~256 emoji,光标插入,⌫ 退格) | ✅ | --- ## 二、架构概览 ``` ChatDetailPage ├── _InputBar │ ├── [😊] EmojiPanel.show() ← 全屏 DraggableScrollableSheet │ └── [📎] AttachmentPanelSheet.show() ← BottomSheet Future │ ├── _buildBubble(typ) │ └── case 5 → StickerMessageBubble │ └── State handlers ├── _showEmojiPanel() → EmojiPanel (插入 / 退格) ├── _showAttachmentPanel() → dispatch AttachmentOption ├── _pickFromCamera() → image_picker → SendImageUseCase └── _pickVideo() → image_picker → SendVideoUseCase ``` --- ## 三、组件详解 ### 3.1 StickerMessageBubble(typ=5) **文件**:`features/chat/view/widgets/sticker_message_bubble.dart` ```dart class StickerMessageBubble extends StatelessWidget { final String rawContent; // {"url":"sticker/xxx.png","width":120,"height":120} static const double _maxSize = 120.0; } ``` - `rawContent` JSON 解析:`url` → `CdnUrlResolver.resolve(url)` → CDN 完整 URL - 无气泡背景(`BoxDecoration` 不设置颜色) - 固定 120×120pt `SizedBox`,`Image.network` + `BoxFit.contain` - `ClipRRect(borderRadius: 8)` 防止圆角溢出 **CDN 路径规则**(CdnUrlResolver): | 前缀 | 解析结果 | |------|---------| | `sticker/` | `AppConfig.apiBaseUrl/sticker/...` | | `Image/` | `AppConfig.apiBaseUrl/Image/...` | | `http(s)://` | 原样透传 | ### 3.2 AttachmentPanelSheet(#52) **文件**:`features/chat/view/widgets/attachment_panel_sheet.dart` ```dart enum AttachmentOption { camera, gallery, video, file, voice, redEnvelope } class AttachmentPanelSheet extends StatelessWidget { static Future show(BuildContext context) { return showModalBottomSheet(...); } } ``` - `GridView.count(crossAxisCount: 3)`,2 行 6 格 - 每格:56pt 着色圆形图标 + 标签文字 - 颜色方案: | 选项 | 色号 | |------|------| | 拍照 | #5667FF | | 相册 | #0BB8A9 | | 视频 | #FF5FA2 | | 文件 | #FF8B5E | | 录音 | #8A5CF6 | | 红包 | #E8600A | - 返回 `Future` — 父页面负责所有业务逻辑,面板本身无状态 ### 3.3 EmojiPanel(#56) **文件**:`features/chat/view/widgets/emoji_panel.dart` ```dart class EmojiPanel extends StatefulWidget { static Future show(BuildContext context, { required void Function(String) onEmojiSelected, required VoidCallback onBackspace, }) { ... } } ``` - `DraggableScrollableSheet` 从底部展开,高度 40% 屏高 - 4 个分类 Tab:😊 常用(80)/ 👤 人物(54)/ 🌿 自然(72)/ 🎯 物品(77) - `GridView.count(crossAxisCount: 8)` 展示 emoji - 退格键(⌫):`text.runes.toList()` 正确处理多码点 emoji(如 👨‍👩‍👧) - 光标插入:`TextEditingController.selection.baseOffset.clamp(0, text.length)` ### 3.4 SendVideoUseCase(#54) **文件**:`features/chat/usecases/send_video_usecase.dart` ``` video picker (image_picker.pickVideo) ↓ File.length() → size (bytes) ↓ UploadFileRequest(filePath) → CDN URL ↓ SendMessageUseCase(typ: 4, content: {"url":"...","thumb":"","size":N}) ``` - `thumb` 当前为空字符串(`video_thumbnail` 包未接入) - `VideoMessageBubble` 已处理 `thumb` 为空的情况(显示播放图标占位) - `chatType` 默认 1(单聊) --- ## 四、ChatDetailPage 集成 ### 4.1 _InputBar 改动 ```dart // 新增两个按钮(emoji + attach) Row(children: [ IconButton(icon: Icon(Icons.emoji_emotions_outlined), onPressed: onEmoji), IconButton(icon: Icon(Icons.attach_file), onPressed: onAttachPanel), Expanded(child: TextField(...)), IconButton(icon: Icon(Icons.send), onPressed: onSend), ]) ``` ### 4.2 附件派发流程 ```dart Future _showAttachmentPanel() async { final opt = await AttachmentPanelSheet.show(context); switch (opt) { case AttachmentOption.camera: _pickFromCamera(); break; case AttachmentOption.gallery: _showImagePicker(); break; case AttachmentOption.video: _pickVideo(); break; case AttachmentOption.file: // SnackBar: 即将上线 case AttachmentOption.voice: // SnackBar: 即将上线 case AttachmentOption.redEnvelope: // SnackBar: 即将上线 } } ``` ### 4.3 typ=5 气泡路由 ```dart Widget _buildBubble(int typ, String content, bool isMine) { switch (typ) { case 1: return _TextBubble(...) case 2: return ImageMessageBubble(...) case 4: return VideoMessageBubble(...) case 5: return StickerMessageBubble(rawContent: content) // ← #51 case 6: return FileMessageBubble(...) ... } } ``` --- ## 五、DI 装配 **文件**:`features/chat/di/chat_service_providers.dart` ```dart final sendVideoUseCaseProvider = Provider((ref) { return SendVideoUseCase( apiClient: ref.read(networkSdkApiProvider), sendMessage: ref.read(sendMessageUseCaseProvider), ); }); ``` --- ## 六、待完成事项 | Issue | 内容 | 前提条件 | |-------|------|---------| | #55 | 文件发送完整实现 | `flutter pub add file_picker` | | — | 录音发送(VoiceRecordSheet) | 录音 API 对接 | | — | 附件面板红包入口接入 | RedEnvelope SendView 接入 | | — | 视频缩略图(thumb) | `flutter pub add video_thumbnail` |