- 将应用版本更新至 1.0.2,修改相关配置文件 - 集成腾讯云 COS 上传功能,新增相关服务和钩子 - 更新 AI 体态评估页面,支持照片上传和评估结果展示 - 添加雷达图组件以展示评估结果 - 更新样式以适应新功能的展示和交互 - 修改登录页面背景效果,提升用户体验
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import { COS_BUCKET, COS_REGION } from '@/constants/Cos';
|
|
import { api } from '@/services/api';
|
|
|
|
type CosCredential = {
|
|
credentials: {
|
|
tmpSecretId: string;
|
|
tmpSecretKey: string;
|
|
sessionToken: string;
|
|
};
|
|
startTime?: number;
|
|
expiredTime?: number;
|
|
};
|
|
|
|
type UploadOptions = {
|
|
key: string;
|
|
body: any;
|
|
contentType?: string;
|
|
onProgress?: (progress: { percent: number }) => void;
|
|
signal?: AbortSignal;
|
|
};
|
|
|
|
let CosSdk: any | null = null;
|
|
|
|
async function ensureCosSdk(): Promise<any> {
|
|
if (CosSdk) return CosSdk;
|
|
// 动态导入避免影响首屏
|
|
const mod = await import('cos-js-sdk-v5');
|
|
CosSdk = mod.default ?? mod;
|
|
return CosSdk;
|
|
}
|
|
|
|
async function fetchCredential(): Promise<CosCredential> {
|
|
return await api.get<CosCredential>('/users/cos-token');
|
|
}
|
|
|
|
export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record<string, string> }> {
|
|
const { key, body, contentType, onProgress, signal } = options;
|
|
if (!COS_BUCKET || !COS_REGION) {
|
|
throw new Error('未配置 COS_BUCKET / COS_REGION');
|
|
}
|
|
const COS = await ensureCosSdk();
|
|
const cred = await fetchCredential();
|
|
|
|
const controller = new AbortController();
|
|
if (signal) {
|
|
if (signal.aborted) controller.abort();
|
|
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
}
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
const cos = new COS({
|
|
getAuthorization: (_opts: any, cb: any) => {
|
|
cb({
|
|
TmpSecretId: cred.credentials.tmpSecretId,
|
|
TmpSecretKey: cred.credentials.tmpSecretKey,
|
|
SecurityToken: cred.credentials.sessionToken,
|
|
StartTime: cred.startTime,
|
|
ExpiredTime: cred.expiredTime,
|
|
});
|
|
},
|
|
});
|
|
|
|
const task = cos.putObject(
|
|
{
|
|
Bucket: COS_BUCKET,
|
|
Region: COS_REGION,
|
|
Key: key,
|
|
Body: body,
|
|
ContentType: contentType,
|
|
onProgress: (progressData: any) => {
|
|
if (onProgress) {
|
|
const percent = progressData && progressData.percent ? progressData.percent : 0;
|
|
onProgress({ percent });
|
|
}
|
|
},
|
|
},
|
|
(err: any, data: any) => {
|
|
if (err) return reject(err);
|
|
resolve({ key, etag: data && data.ETag, headers: data && data.headers });
|
|
}
|
|
);
|
|
|
|
controller.signal.addEventListener('abort', () => {
|
|
try { task && task.cancel && task.cancel(); } catch { }
|
|
reject(new DOMException('Aborted', 'AbortError'));
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function uploadWithRetry(options: UploadOptions & { maxRetries?: number; backoffMs?: number }): Promise<{ key: string; etag?: string }> {
|
|
const { maxRetries = 2, backoffMs = 800, ...rest } = options;
|
|
let attempt = 0;
|
|
// 简单指数退避
|
|
while (true) {
|
|
try {
|
|
return await uploadToCos(rest);
|
|
} catch (e: any) {
|
|
if (rest.signal?.aborted) throw e;
|
|
if (attempt >= maxRetries) throw e;
|
|
const wait = backoffMs * Math.pow(2, attempt);
|
|
await new Promise(r => setTimeout(r, wait));
|
|
attempt++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|