feat: 多图宫格气泡全量实现 (#36~#38)
- #36 ImageGridBubble: 2 列×116pt / 3 列×78pt, 3pt 间距, 8pt 外层/4pt 单格圆角, tap→ImageViewerPage - #37 ChatDisplayItem + _buildDisplayItems: 连续 typ=2 同 sendId Δt<5s 分组为宫格,iOS ChatView.buildDisplayItems parity - #38 SendImageUseCase.sendBatch: Future.wait 并行上传 → 顺序快速发送,ImagePickerSheet 改用 sendBatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,17 +8,21 @@ import 'package:networks_sdk/networks_sdk.dart';
|
||||
import 'package:im_app/data/remote/upload_file_request.dart';
|
||||
import 'package:im_app/features/chat/usecases/send_message_use_case.dart';
|
||||
|
||||
/// 图片上传并发送消息用例(#33)
|
||||
/// 图片上传并发送消息用例(#33 / #38)
|
||||
///
|
||||
/// 对应 iOS `ImageProcessor.swift` + `ChatView.swift sendImageMessage()`
|
||||
///
|
||||
/// ## 执行流程
|
||||
/// ## 单图流程 [execute]
|
||||
/// 1. 读取图片字节 → [Uint8List]
|
||||
/// 2. `dart:ui.ImageDescriptor.encoded()` → 解析压缩后宽高(零外部依赖)
|
||||
/// 3. 写入临时文件 → [UploadFileRequest] FormData → [UploadResult.url]
|
||||
/// 4. `jsonEncode({"url":url,"width":w,"height":h})` → [SendMessageUseCase] typ=2
|
||||
/// 5. 删除临时文件
|
||||
///
|
||||
/// ## 多图批量流程 [sendBatch](#38)
|
||||
/// 1. **并行**上传所有图片([Future.wait])
|
||||
/// 2. 快速顺序发送消息 → sendTime 各差毫秒级,满足宫格分组 < 5s 条件
|
||||
///
|
||||
/// 发送中进度通过 [onProgress] 回调传出(0.0~1.0),
|
||||
/// 用于 [ImageMessageBubble] 渲染进度环。
|
||||
class SendImageUseCase {
|
||||
@@ -98,6 +102,67 @@ class SendImageUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量并行上传并快速顺序发送(#38)
|
||||
///
|
||||
/// 并行上传保证所有消息 sendTime 在毫秒级内,满足宫格分组条件(< 5 秒)。
|
||||
///
|
||||
/// 返回失败的文件路径列表;空列表表示全部成功。
|
||||
Future<List<String>> sendBatch({
|
||||
required List<String> filePaths,
|
||||
required int chatId,
|
||||
}) async {
|
||||
if (filePaths.isEmpty) return [];
|
||||
|
||||
// 1. 并行上传所有图片
|
||||
final results = await Future.wait(
|
||||
filePaths.map((p) => _uploadOnly(p)),
|
||||
eagerError: false,
|
||||
);
|
||||
|
||||
// 2. 顺序快速发送(上传成功的)
|
||||
final failed = <String>[];
|
||||
for (var i = 0; i < filePaths.length; i++) {
|
||||
final content = results[i];
|
||||
if (content == null) {
|
||||
failed.add(filePaths[i]);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await _sendMessage.execute(chatId: chatId, content: content, typ: 2);
|
||||
} catch (e) {
|
||||
debugPrint('[SendImageUseCase.sendBatch] send error: $e');
|
||||
failed.add(filePaths[i]);
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
/// 仅上传,返回 `jsonEncode({url,width,height})` 或 null(失败)
|
||||
Future<String?> _uploadOnly(String filePath) async {
|
||||
try {
|
||||
final bytes = await File(filePath).readAsBytes();
|
||||
final (w, h) = await _resolveSize(bytes);
|
||||
final ext = _extOf(filePath);
|
||||
final tempFile = File(
|
||||
'${Directory.systemTemp.path}/upload_${DateTime.now().microsecondsSinceEpoch}$ext',
|
||||
);
|
||||
await tempFile.writeAsBytes(bytes);
|
||||
try {
|
||||
final result = await _apiClient.executeRequest(
|
||||
UploadFileRequest(filePath: tempFile.path),
|
||||
);
|
||||
final url = result?.url ?? '';
|
||||
if (url.isEmpty) return null;
|
||||
return '{"url":"$url","width":$w,"height":$h}';
|
||||
} finally {
|
||||
try { await tempFile.delete(); } catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[SendImageUseCase._uploadOnly] $filePath: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _extOf(String path) {
|
||||
final dot = path.lastIndexOf('.');
|
||||
if (dot == -1 || dot == path.length - 1) return '.jpg';
|
||||
|
||||
Reference in New Issue
Block a user