feat(upload): compress images over 10MB before OSS upload (v3.4.13)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user