| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- import { api } from '../api'
- const DEFAULT_COS_UPLOAD_HOST = 'https://ranking-1309648802.cos.ap-shanghai.myqcloud.com'
- const imageExtensions: Record<string, string> = { '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<string, unknown>) {
- 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<string> {
- 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<string, unknown>
- 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}`
- }
|