feat(editor): implement deferred image uploading and pending state management
Some checks failed
Frontend CI / ota-ios (push) Successful in 2m3s
Frontend CI / ota-android (push) Successful in 2m4s
Frontend CI / build-and-push-web (push) Failing after 33m21s
Frontend CI / build-android-apk (push) Successful in 59m9s

Refactor the image handling workflow to decouple image selection from the upload process. This improves user experience by allowing users to continue composing posts or trades while images are queued for upload, and increases reliability by using a centralized pending image utility.

- **Deferred Uploads**: Replaced immediate image uploads in `CreatePostScreen`, `PostDetailScreen`, and `CreateTradeScreen` with a "pending" state. Images are now uploaded in bulk during the final submission phase.
- **BlockEditor Enhancements**:
    - Updated `useBlockEditor` to support asynchronous batch uploading of all pending images via a new `uploadPendingImages` method.
    - Removed inline uploading overlays in favor of a more robust state-driven approach.
    - Improved `BlockEditorHandle` to expose block retrieval and batch upload capabilities.
- **API Layer Improvements**:
    - Migrated native image uploads from standard `FormData` to `expo-file-system`'s multipart upload to resolve compatibility issues with recent React Native versions.
    - Maintained `fetch` + `Blob` logic for web platform compatibility.
- **New Utilities**: Introduced `src/utils/pendingImages.ts` to manage the lifecycle of local (pending) vs. remote (uploaded) images across the application.
This commit is contained in:
2026-06-07 00:40:35 +08:00
parent 5c81795d39
commit b15e0c0b0b
11 changed files with 331 additions and 313 deletions

View File

@@ -0,0 +1,89 @@
/**
* 通用「待上传图片」抽象
*
* 用于发帖、评论、发布商品等场景:
* - 用户选择图片时仅保存本地 URIpending
* - 提交时统一上传,失败可重试已上传的不会丢失
*/
import type * as ImagePicker from 'expo-image-picker';
import { uploadService } from '@/services/upload';
export type PendingImage = {
id: string;
kind: 'pending';
localUri: string;
mimeType?: string;
};
export type RemoteImage = {
id: string;
kind: 'remote';
url: string;
};
export type PendingOrRemoteImage = PendingImage | RemoteImage;
let _imgIdCounter = 0;
export const generatePendingImageId = (prefix = 'img'): string =>
`${prefix}-${++_imgIdCounter}-${Date.now()}`;
/** 从 ImagePicker 选择结果构造 pending 图片 */
export const makePendingImageFromAsset = (
asset: ImagePicker.ImagePickerAsset,
idPrefix?: string,
): PendingImage => ({
id: generatePendingImageId(idPrefix),
kind: 'pending',
localUri: asset.uri,
mimeType: asset.mimeType,
});
/** 获取图片的展示 URIpending 用本地remote 用远程) */
export const getImageDisplayUri = (img: PendingOrRemoteImage): string =>
img.kind === 'pending' ? img.localUri : img.url;
/** 转换为 remote 图片(保持 id 不变) */
export const toRemoteImage = (id: string, url: string): RemoteImage => ({
id,
kind: 'remote',
url,
});
export type UploadAllResult =
| { success: true; urls: string[] }
| { success: false; urls: string[]; failedId: string };
/**
* 批量上传 pending 图片,已是 remote 的会跳过。
* 顺序串行执行,遇到失败立即中断并返回。
*
* @param images 图片列表
* @param onItemUploaded 单张图片上传成功后的回调(用于增量更新 state
* 外部可据此将该 id 的图片标记为 remote 以便重试时跳过)
*/
export const uploadAllPendingImages = async (
images: PendingOrRemoteImage[],
onItemUploaded?: (id: string, url: string) => void,
): Promise<UploadAllResult> => {
const urls: string[] = [];
for (const img of images) {
if (img.kind === 'remote') {
urls.push(img.url);
continue;
}
const result = await uploadService.uploadImage({
uri: img.localUri,
type: img.mimeType || 'image/jpeg',
});
if (result) {
urls.push(result.url);
onItemUploaded?.(img.id, result.url);
} else {
return { success: false, urls, failedId: img.id };
}
}
return { success: true, urls };
};