feat: 图片预览与发送全量实现 (#31~#35)

- #31 ImageMessageBubble: typ=2 气泡,max 220pt / min 80pt 尺寸规则,进度环叠层
- #32 ImageViewerPage: photo_view 全屏查看,PhotoViewGallery 多图滑动,保存+分享工具栏
- #33 ImagePickerSheet + SendImageUseCase: 相册/相机选图(最多9张),裁剪,dart:ui 解析宽高,FormData CDN 上传,typ=2 发送
- #34 image_cropper 接入:_PreviewTile 点击裁剪,iOS TOCropViewController 对齐
- #35 _MessageBubble BubbleKind 路由:typ switch → 各媒体气泡,输入栏新增附件按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pp-bot
2026-03-24 13:20:56 +09:00
parent 23fc6b0c86
commit b971900263
8 changed files with 1082 additions and 27 deletions

View File

@@ -0,0 +1,176 @@
# 图片预览与编辑架构文档
对应 Gitea issues #31#35
---
## 1. 背景与目标
iOS 老项目 (`ImageMessageBubble.swift`, `ImageGridBubble.swift`, `ImageProcessor.swift`) 已实现图片收发、全屏查看和编辑。Flutter 新项目目前 `_MessageBubble` 对所有消息类型均渲染为纯文本,需补齐以下能力:
| 功能 | Issue | iOS 对应 |
|------|-------|---------|
| typ=2 图片气泡展示 | #31 | `ImageMessageBubble.swift` |
| 全屏查看 + 保存 + 分享 | #32 | `ImageFullscreenView.swift` |
| 相册/相机选图 + CDN 上传 | #33 | `ChatView.swift photosPicker` |
| 图片编辑(裁剪/旋转) | #34 | `ImageProcessor.swift` + `TOCropViewController` |
| 消息气泡 BubbleKind 路由 | #35 | `ChatMessageBubble.swift switch` |
---
## 2. 消息格式
### typ=2 单张图片
```json
{"url":"Image/xxx.jpg","width":1024,"height":768}
```
- `url`CDN 相对路径,经 `CdnUrlResolver.resolve()` 转为完整 URL
- `width` / `height`:原始像素尺寸,用于计算显示比例
---
## 3. 架构层次
```
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ImagePickerSheet → (image_picker + image_cropper) │
│ ImageMessageBubble → ImageViewerPage (photo_view) │
│ _MessageBubble routing (typ switch → MediaBubble) │
├─────────────────────────────────────────────────────────────────┤
│ UseCase Layer │
│ SendImageUseCase │
│ ├─ readAsBytes() → dart:ui ImageDescriptor (width/height) │
│ ├─ UploadFileRequest (POST /app/api/upload/file) │
│ └─ SendMessageUseCase.execute(typ=2, content=JSON) │
├─────────────────────────────────────────────────────────────────┤
│ Data Layer │
│ UploadFileRequest → UploadResult.url (CDN path) │
│ CdnUrlResolver.resolve(path) → full URL │
└─────────────────────────────────────────────────────────────────┘
```
---
## 4. 关键设计决策
### 4.1 图片尺寸计算iOS 对齐)
| 参数 | 值 |
|------|---|
| 最长边上限 | 220 pt |
| 最短边下限 | 80 pt |
| 宽高比 | 保持原比例 |
| 圆角 | 12 pt |
```dart
// 伪码
final longest = max(w, h);
if (longest > 220) { w *= 220/longest; h *= 220/longest; }
final shortest = min(w, h);
if (shortest < 80) { w *= 80/shortest; h *= 80/shortest; }
```
### 4.2 图片压缩策略iOS ImageProcessor 对齐)
| 参数 | 值 |
|------|---|
| 最大边长 | 1920 px |
| JPEG 质量 | 85 |
| 方式 | `image_picker` 内置 `maxWidth/maxHeight/imageQuality` |
不引入 `flutter_image_compress`——`image_picker` 的内置参数已满足 iOS 等价逻辑。
### 4.3 尺寸解析
上传前使用 `dart:ui.ImageDescriptor.encoded(ImmutableBuffer)` 解析压缩后尺寸,零外部依赖。
### 4.4 CDN 上传(单步 FormData
沿用现有 `UploadFileRequest`FormData → `/app/api/upload/file`),返回 `UploadResult.url`CDN 相对路径)。
> 注意iOS 使用三步 presign→S3 PUT→finishFlutter 项目因后端已提供单步上传端点,使用更简单的 FormData 模式。
### 4.5 全屏查看器iOS ImageFullscreenView 对齐)
| iOS | Flutter |
|-----|---------|
| `UIScrollView` pinch-to-zoom | `photo_view` PhotoView |
| 双击 2.5x | `PhotoView.enableDoubleTapZoom` |
| 底部保存/分享 | `image_gallery_saver_plus` + `share_plus` |
| 多图滑动 | `PhotoViewGallery` |
### 4.6 图片编辑iOS TOCropViewController 对齐)
`image_cropper` 在 iOS 上也调用 `TOCropViewController`(通过 Plugin BridgeUI 效果一致。
Flutter 侧无需自建裁剪页,直接调用:
```dart
await ImageCropper().cropImage(sourcePath: ..., compressQuality: 85, ...)
```
---
## 5. 数据流:发送图片
```
用户点击附件图标
└─ ImagePickerSheet.show()
├─ 拍照: ImagePicker.pickImage(camera, maxW=1920, q=85)
└─ 相册: ImagePicker.pickMultiImage(maxW=1920, q=85, limit=9)
├─ (可选) _PreviewTile 点击 → ImageCropper.cropImage()
└─ "发送" → SendImageUseCase.execute(imageFile, chatId)
├─ readAsBytes() → Uint8List
├─ dart:ui.ImageDescriptor → (width, height)
├─ 写临时文件 → UploadFileRequest → UploadResult.url
├─ jsonEncode({"url":url,"width":w,"height":h})
└─ SendMessageUseCase(chatId, content, typ=2)
├─ 乐观写入 MessageRepository (DB Stream → UI)
└─ HTTP POST /app/api/chat/send-message
```
## 6. 数据流:接收/查看图片
```
WS / HTTP 历史 → DB → MessageRepository Stream
└─ messagesByChatIdProvider(chatId)
└─ ListView → _MessageBubble(message)
└─ message.typ == 2
└─ ImageMessageBubble(rawContent)
├─ parse JSON → url, width, height
├─ CdnUrlResolver.resolve(url) → fullUrl
├─ Image.network(fullUrl, fit: cover)
└─ GestureTap → ImageViewerPage(urls: [fullUrl])
└─ PhotoView pinch-to-zoom
├─ 保存 → ImageGallerySaverPlus.saveNetworkImage(url)
└─ 分享 → Share.share(url)
```
---
## 7. 新增文件清单
| 文件 | 说明 |
|------|------|
| `lib/features/chat/view/widgets/image_message_bubble.dart` | typ=2 气泡(#31 |
| `lib/features/chat/view/image_viewer_page.dart` | 全屏查看(#32 |
| `lib/features/chat/view/widgets/image_picker_sheet.dart` | 选图底部弹窗(#33 |
| `lib/features/chat/usecases/send_image_usecase.dart` | 上传+发送(#33 |
## 8. 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `pubspec.yaml` | 新增 photo_view / image_picker / image_cropper / image_gallery_saver_plus / share_plus |
| `lib/features/chat/di/chat_service_providers.dart` | 新增 sendImageUseCaseProvider |
| `lib/features/chat/view/chat_detail_page.dart` | 输入栏附件按钮 + _MessageBubble typ 路由(#35 |
---
## 9. 待完成
- 多图网格气泡 `ImageGridBubble`(微信 2-9 图 layout待 typ 确认)
- 图片 sticker (typ=5) 展示
- GIF 消息 (typ=25) 支持
- 端到端加密图片(`MediaCrypto` decrypt待 cipher_guard_sdk 接入)