feat: 更新应用版本和集成腾讯云 COS 上传功能

- 将应用版本更新至 1.0.2,修改相关配置文件
- 集成腾讯云 COS 上传功能,新增相关服务和钩子
- 更新 AI 体态评估页面,支持照片上传和评估结果展示
- 添加雷达图组件以展示评估结果
- 更新样式以适应新功能的展示和交互
- 修改登录页面背景效果,提升用户体验
This commit is contained in:
richarjiang
2025-08-13 15:21:54 +08:00
parent 5814044cee
commit 321947db98
20 changed files with 1664 additions and 342 deletions

107
services/cos.ts Normal file
View File

@@ -0,0 +1,107 @@
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++;
}
}
}