feat(upload): compress images over 10MB before OSS upload (v3.4.13)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 22:48:34 +08:00
parent bd1930f6b1
commit fa6fb80d19
15 changed files with 196 additions and 25 deletions
+1
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@dukang/shared-types": "workspace:*",
"@dukang/shared-ui": "workspace:*",
"antd": "^5.22.0",
"dayjs": "^1.11.13",
"echarts": "^6.1.0",
@@ -15,8 +15,6 @@ type OssUploadProps = {
placeholder?: string;
};
const DEFAULT_MAX_MB = 10;
export default function OssUpload({
value,
onChange,
@@ -24,7 +22,6 @@ export default function OssUpload({
bizType,
mediaType = 'IMAGE',
accept,
maxSizeMb = DEFAULT_MAX_MB,
placeholder = '上传后自动填入,或手动粘贴 URL',
}: OssUploadProps) {
const [uploading, setUploading] = useState(false);
@@ -34,12 +31,6 @@ export default function OssUpload({
const customRequest: UploadProps['customRequest'] = async ({ file, onSuccess, onError }) => {
const raw = file as File;
if (raw.size > maxSizeMb * 1024 * 1024) {
const err = new Error(`文件不能超过 ${maxSizeMb}MB`);
message.error(err.message);
onError?.(err);
return;
}
setUploading(true);
try {
const result = await uploadFileToOss(raw, { bizType, mediaType });
+16 -1
View File
@@ -1,4 +1,9 @@
import { apiBase, CLIENT_APP, getToken, clearAuth } from './api';
import {
compressImageFileIfNeeded,
DEFAULT_OSS_MAX_UPLOAD_BYTES,
formatOssMaxSizeMb,
} from '@dukang/shared-ui/compressImage';
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
@@ -9,14 +14,24 @@ export type UploadFileResult = {
mock: boolean;
};
async function prepareUploadFile(file: File, mediaType: OssMediaType): Promise<File> {
if (mediaType !== 'IMAGE') return file;
const compressed = await compressImageFileIfNeeded(file);
if (compressed.size > DEFAULT_OSS_MAX_UPLOAD_BYTES) {
throw new Error(`图片压缩后仍超过 ${formatOssMaxSizeMb()}MB,请换一张较小的图片`);
}
return compressed;
}
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
export async function uploadFileToOss(
file: File,
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
const prepared = await prepareUploadFile(file, mediaType);
const formData = new FormData();
formData.append('file', file);
formData.append('file', prepared);
formData.append('bizType', options.bizType);
formData.append('mediaType', mediaType);
@@ -19,8 +19,6 @@ type OssUploadFieldProps = {
onWechatReadyChange?: (ready: boolean) => void;
};
const DEFAULT_MAX_MB = 10;
function isCancelError(msg: string): boolean {
return /cancel|取消/i.test(msg);
}
@@ -79,10 +77,6 @@ export default function OssUploadField({
showUploadError('图片读取失败,请重新选择');
return;
}
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
showUploadError(`文件不能超过 ${DEFAULT_MAX_MB}MB`);
return;
}
const result = await uploadFileToOss(file, { bizType, mediaType });
onChange?.(result.url);
}
+16 -1
View File
@@ -1,4 +1,9 @@
import { apiBase, request } from './api';
import {
compressImageFileIfNeeded,
DEFAULT_OSS_MAX_UPLOAD_BYTES,
formatOssMaxSizeMb,
} from '@dukang/shared-ui/compressImage';
const UPLOAD_TIMEOUT_MS = 120_000;
@@ -11,13 +16,23 @@ export type UploadFileResult = {
mock: boolean;
};
async function prepareUploadFile(file: File, mediaType: OssMediaType): Promise<File> {
if (mediaType !== 'IMAGE') return file;
const compressed = await compressImageFileIfNeeded(file);
if (compressed.size > DEFAULT_OSS_MAX_UPLOAD_BYTES) {
throw new Error(`图片压缩后仍超过 ${formatOssMaxSizeMb()}MB,请换一张较小的图片`);
}
return compressed;
}
async function uploadFileToOssInner(
file: File,
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
const prepared = await prepareUploadFile(file, mediaType);
const formData = new FormData();
formData.append('file', file);
formData.append('file', prepared);
formData.append('bizType', options.bizType);
formData.append('mediaType', mediaType);
+10 -1
View File
@@ -1,5 +1,10 @@
import { apiBase } from './api';
import { getStoreProfile } from './api';
import {
compressImageFileIfNeeded,
DEFAULT_OSS_MAX_UPLOAD_BYTES,
formatOssMaxSizeMb,
} from '@dukang/shared-ui/compressImage';
const UPLOAD_TIMEOUT_MS = 120_000;
@@ -16,8 +21,12 @@ export type RegisteredResource = {
};
export async function uploadFileToOss(file: File, bizType: string): Promise<UploadFileResult> {
const compressed = await compressImageFileIfNeeded(file);
if (compressed.size > DEFAULT_OSS_MAX_UPLOAD_BYTES) {
throw new Error(`图片压缩后仍超过 ${formatOssMaxSizeMb()}MB,请换一张较小的图片`);
}
const formData = new FormData();
formData.append('file', file);
formData.append('file', compressed);
formData.append('bizType', bizType);
formData.append('mediaType', 'IMAGE');
+13 -1
View File
@@ -1,4 +1,9 @@
import { apiBase } from './api';
import {
compressImageFileIfNeeded,
DEFAULT_OSS_MAX_UPLOAD_BYTES,
formatOssMaxSizeMb,
} from '@dukang/shared-ui/compressImage';
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
@@ -15,8 +20,15 @@ export async function uploadFileToOss(
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
let prepared = file;
if (mediaType === 'IMAGE') {
prepared = await compressImageFileIfNeeded(file);
if (prepared.size > DEFAULT_OSS_MAX_UPLOAD_BYTES) {
throw new Error(`图片压缩后仍超过 ${formatOssMaxSizeMb()}MB,请换一张较小的图片`);
}
}
const formData = new FormData();
formData.append('file', file);
formData.append('file', prepared);
formData.append('bizType', options.bizType);
formData.append('mediaType', mediaType);
+34
View File
@@ -0,0 +1,34 @@
import Taro from '@tarojs/taro';
const MAX_BYTES = 10 * 1024 * 1024;
/** 小程序临时文件超过 10MB 时用 compressImage 压缩后再上传 */
export async function compressWeappImageIfNeeded(tempFilePath: string): Promise<string> {
if (process.env.TARO_ENV !== 'weapp') return tempFilePath;
let path = tempFilePath;
let size = await getFileSize(path);
if (size <= MAX_BYTES) return path;
const qualities = [80, 65, 50, 40];
for (const quality of qualities) {
const res = await Taro.compressImage({ src: path, quality });
path = res.tempFilePath;
size = await getFileSize(path);
if (size <= MAX_BYTES) return path;
}
if (size > MAX_BYTES) {
throw new Error('图片压缩后仍超过 10MB,请换一张较小的图片');
}
return path;
}
async function getFileSize(filePath: string): Promise<number> {
try {
const info = await Taro.getFileInfo({ filePath });
return typeof info.size === 'number' ? info.size : 0;
} catch {
return 0;
}
}
@@ -69,12 +69,15 @@ export function mergeWxDisplayProfile(profile: UserProfile): UserProfile {
/** 上传 chooseAvatar 临时文件到 OSS,并返回已登记到当前用户的真实资源。 */
export async function uploadAvatarTempFile(tempFilePath: string): Promise<UploadedAvatarResource> {
const { API_BASE, CLIENT_APP, getToken } = await import('./api');
const { compressWeappImageIfNeeded } = await import('./compress-image');
const token = getToken();
if (!token) throw new Error('请先登录');
const filePath = await compressWeappImageIfNeeded(tempFilePath);
const res = await Taro.uploadFile({
url: `${API_BASE}/common/resources/upload`,
filePath: tempFilePath,
filePath,
name: 'file',
formData: {
bizType: 'AVATAR',