import { api } from '../api' const DEFAULT_COS_UPLOAD_HOST = 'https://ranking-1309648802.cos.ap-shanghai.myqcloud.com' const imageExtensions: Record = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/webp': '.webp' } const signatureFields = ['key', 'q-sign-algorithm', 'q-ak', 'q-key-time', 'q-signature', 'policy'] as const export function createUploadId(): string { if (typeof globalThis.crypto.randomUUID === 'function') return globalThis.crypto.randomUUID() const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)) bytes[6] = (bytes[6]! & 0x0f) | 0x40 bytes[8] = (bytes[8]! & 0x3f) | 0x80 const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` } /** * 解析 COS 上传地址:优先使用凭证中的地址,其次使用环境变量,最后回退到默认地址。 * 仅接受 HTTP/HTTPS 地址,返回其源地址(协议、域名和端口),不保留路径、查询参数及片段。 * @param signature 后端返回的 COS 直传凭证。 * @returns 用于直传和拼接文件 URL 的源地址。 * @throws 地址无法解析或协议不受支持时抛出错误。 */ function uploadHost(signature: Record) { const configured = import.meta.env.VITE_COS_UPLOAD_URL const returned = ['uploadUrl', 'cosUrl', 'url', 'host', 'domain', 'bucketUrl', 'fileUrl'].map(field => signature[field]).find(value => typeof value === 'string' && value.trim()) const value = String(returned || configured || DEFAULT_COS_UPLOAD_HOST).trim() try { const url = new URL(value) if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error() return url.origin } catch { throw new Error('上传签名中的 COS 地址格式不正确') } } /** * 获取后端签名后,将非空 PNG、JPEG 或 WebP 图片通过表单直接上传至 COS。 * @param file 待上传的图片文件,格式根据 MIME 类型校验。 * @param options 上传配置;keyPrefix 为对象路径前缀,默认使用 wms/INBOUND。 * @returns 上传成功后由 COS 源地址和签名 key 拼接的文件 URL,可用于业务附件提交。 * @throws 文件校验、签名解析、必填字段校验或上传失败时抛出错误,由调用方处理。 */ export async function uploadImage(file: File, options: { keyPrefix?: string } = {}): Promise { const extension = imageExtensions[file.type] if (!extension) throw new Error('仅支持 PNG、JPEG、WebP 图片') if (!file.size) throw new Error('不能上传空图片') // 按前缀、本地年月日和随机文件名申请签名,实际上传路径以后端返回的 key 为准。 const prefix = (options.keyPrefix ?? 'wms/INBOUND').replace(/^\/+|\/+$/g, '') const now = new Date() const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}` const filename = [prefix, date, `${createUploadId()}${extension}`].filter(Boolean).join('/') // 兼容接口直接返回凭证对象或返回 JSON 字符串两种格式。 let params: unknown = await api.wmsApi.file.oss(filename) if (typeof params === 'string') { try { params = JSON.parse(params) } catch { throw new Error('上传签名格式不正确') } } if (!params || typeof params !== 'object' || Array.isArray(params)) throw new Error('上传签名格式不正确') const signature = params as Record const cosUploadHost = uploadHost(signature) const form = new FormData() // 保留签名字段原值,并在使用临时凭证时附带安全令牌。 for (const field of signatureFields) { const value = signature[field] if (typeof value !== 'string' || !value.trim()) throw new Error(`上传签名缺少有效字段:${field}`) form.append(field, value) } if (typeof signature['x-cos-security-token'] === 'string') { form.append('x-cos-security-token', signature['x-cos-security-token']) } form.append('success_action_status', '200') form.append('file', file, file.name) // COS 直传不携带 Cookie,表单的 Content-Type 和边界由浏览器自动生成。 const response = await fetch(cosUploadHost, { method: 'POST', body: form, credentials: 'omit' }) if (!response.ok) throw new Error(`图片上传失败,状态码:${response.status}`) // 逐段编码对象路径中的中文和空格,保留目录分隔符。 const encodedKey = (signature.key as string).replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/') return `${cosUploadHost}/${encodedKey}` }