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

7.1 KiB
Raw Blame History

图片预览与编辑架构文档

对应 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 单张图片

{"url":"Image/xxx.jpg","width":1024,"height":768}
  • urlCDN 相对路径,经 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
// 伪码
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

沿用现有 UploadFileRequestFormData → /app/api/upload/file),返回 UploadResult.urlCDN 相对路径)。

注意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 侧无需自建裁剪页,直接调用:

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 接入)