feat(editor): implement deferred image uploading and pending state management
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:
@@ -7,6 +7,7 @@
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Constants from 'expo-constants';
|
||||
import { File as FSFile, UploadType } from 'expo-file-system';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { eventBus } from '@/core/events/EventBus';
|
||||
@@ -437,35 +438,10 @@ class ApiClient {
|
||||
file: { uri: string; name: string; type: string },
|
||||
additionalData?: Record<string, string>
|
||||
): Promise<ApiResponse<T>> {
|
||||
const formData = new FormData();
|
||||
|
||||
// 添加文件(使用 image 字段名)
|
||||
// Web 端需要 append Blob/File;原生端可以直接传 { uri, name, type }。
|
||||
if (Platform.OS === 'web') {
|
||||
const fileResponse = await fetch(file.uri);
|
||||
const blob = await fileResponse.blob();
|
||||
const uploadFile = new File([blob], file.name, { type: file.type || blob.type || 'application/octet-stream' });
|
||||
formData.append('image', uploadFile);
|
||||
} else {
|
||||
formData.append('image', {
|
||||
uri: file.uri,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 添加额外数据
|
||||
if (additionalData) {
|
||||
Object.keys(additionalData).forEach(key => {
|
||||
formData.append(key, additionalData[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取 token 并检查是否需要刷新
|
||||
const token = await this.getToken();
|
||||
const headers: HeadersInit = {};
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) {
|
||||
// 检查 token 是否快过期
|
||||
if (this.isTokenExpiringSoon(token)) {
|
||||
const refreshed = await this.refreshToken();
|
||||
if (refreshed) {
|
||||
@@ -479,14 +455,45 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
let data: ApiResponse<T>;
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
// Web 端:fetch + Blob/File + FormData
|
||||
const formData = new FormData();
|
||||
const fileResponse = await fetch(file.uri);
|
||||
const blob = await fileResponse.blob();
|
||||
const uploadFile = new File([blob], file.name, {
|
||||
type: file.type || blob.type || 'application/octet-stream',
|
||||
});
|
||||
formData.append('image', uploadFile);
|
||||
|
||||
if (additionalData) {
|
||||
Object.keys(additionalData).forEach(key => {
|
||||
formData.append(key, additionalData[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
data = await response.json();
|
||||
} else {
|
||||
// 原生端:使用 expo-file-system 原生 multipart 上传
|
||||
// RN 0.85 的 FormData/Blob 不支持从 URI 创建文件 part
|
||||
const fsFile = new FSFile(file.uri);
|
||||
const result = await fsFile.upload(`${this.baseUrl}${path}`, {
|
||||
httpMethod: 'POST',
|
||||
uploadType: UploadType.MULTIPART,
|
||||
fieldName: 'image',
|
||||
mimeType: file.type,
|
||||
headers,
|
||||
parameters: additionalData,
|
||||
});
|
||||
data = JSON.parse(result.body) as ApiResponse<T>;
|
||||
}
|
||||
|
||||
const data: ApiResponse<T> = await response.json();
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new ApiError(data.code, data.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user