docs(chat): 表情/贴纸/附件面板架构文档(#51~#56)
StickerMessageBubble/AttachmentPanelSheet/EmojiPanel/SendVideoUseCase 完整架构说明:组件设计、CDN路径规则、DI装配、待完成事项。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
205
Doc/emoji_sticker_attachment_architecture.md
Normal file
205
Doc/emoji_sticker_attachment_architecture.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 表情 / 贴纸 / 附件面板架构设计文档
|
||||||
|
|
||||||
|
> 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<AttachmentOption?>
|
||||||
|
│
|
||||||
|
├── _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<AttachmentOption?> show(BuildContext context) {
|
||||||
|
return showModalBottomSheet<AttachmentOption>(...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `GridView.count(crossAxisCount: 3)`,2 行 6 格
|
||||||
|
- 每格:56pt 着色圆形图标 + 标签文字
|
||||||
|
- 颜色方案:
|
||||||
|
|
||||||
|
| 选项 | 色号 |
|
||||||
|
|------|------|
|
||||||
|
| 拍照 | #5667FF |
|
||||||
|
| 相册 | #0BB8A9 |
|
||||||
|
| 视频 | #FF5FA2 |
|
||||||
|
| 文件 | #FF8B5E |
|
||||||
|
| 录音 | #8A5CF6 |
|
||||||
|
| 红包 | #E8600A |
|
||||||
|
|
||||||
|
- 返回 `Future<AttachmentOption?>` — 父页面负责所有业务逻辑,面板本身无状态
|
||||||
|
|
||||||
|
### 3.3 EmojiPanel(#56)
|
||||||
|
|
||||||
|
**文件**:`features/chat/view/widgets/emoji_panel.dart`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class EmojiPanel extends StatefulWidget {
|
||||||
|
static Future<void> 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<void> _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<SendVideoUseCase>((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` |
|
||||||
Reference in New Issue
Block a user