Files
customer-im-client-dev/Doc/image_preview_edit_architecture.md
pp-bot b971900263 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>
2026-03-24 13:20:56 +09:00

177 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 图片预览与编辑架构文档
对应 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 接入)