StickerMessageBubble/AttachmentPanelSheet/EmojiPanel/SendVideoUseCase 完整架构说明:组件设计、CDN路径规则、DI装配、待完成事项。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
5.9 KiB
Markdown
206 lines
5.9 KiB
Markdown
# 表情 / 贴纸 / 附件面板架构设计文档
|
||
|
||
> 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` |
|