/** * 图片上传 composable,完全对齐 ai_uniapp_v2/utils/upload.js * * - POST multipart/form-data 到 /api/common/uploadpic(走服务端代理) * - Header: { token, timestr, nonce } * - FormData: { file, token, mac, client:1, source, version, version_code, upload_type:1 } */ import md5 from '~/utils/md5' export function useUpload() { const macCookie = useCookie('mac', { maxAge: 365 * 24 * 3600, path: '/' }) const tokenCookie = useCookie('token', { path: '/' }) const openIdCookie = useCookie('open_id', { path: '/' }) const sourceCookie = useCookie('source', { path: '/' }) function getNonce() { const timeStr = Date.now().toString() const str = md5(timeStr) const nonce = str.substring(4, 14) return { timeStr, nonce } } /** * 上传图片 * @param {File} file - 浏览器 File 对象 * @returns {Promise<{code: number, data: {url: string, width: number, height: number}, msg: string}>} */ async function uploadImage(file) { if (!file) { throw new Error('请选择文件') } const timestamp = Math.floor(Date.now() / 1000) const { timeStr, nonce } = getNonce() const formData = new FormData() // ThinkJS 2 会在控制器 __before() 中先校验公共参数,因此这些字段 // 必须位于文件字段之前,否则 multipart 解析时会误报“client必填”。 formData.append('token', tokenCookie.value || '') formData.append('uid', openIdCookie.value || '') formData.append('mac', macCookie.value || '') formData.append('base_timestamp', timestamp) formData.append('client', '1') formData.append('source', sourceCookie.value || '') formData.append('client_ios', '0') formData.append('version', '1.0.0') formData.append('version_code', '1') formData.append('upload_type', '1') formData.append('file', file) // 走 /api 代理,对齐 V2 const url = '/api/common/uploadpic' try { const result = await $fetch(url, { method: 'POST', headers: { token: tokenCookie.value || '', timestr: timeStr, nonce, }, body: formData, // 不设置 Content-Type,让浏览器自动设置 multipart boundary }) if (result.code === 0) { return result } if (result.code === 1009) { const { logout } = useUser() logout() const showLogin = useState('showLogin', () => false) showLogin.value = true } throw new Error(result.msg || '上传失败') } catch (e) { if (e.data) { const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data if (data && data.code !== undefined) { throw new Error(data.msg || '上传失败') } } throw e } } /** * 打开文件选择器并上传 */ function selectAndUpload(options = {}) { const { accept = 'image/*' } = options return new Promise((resolve, reject) => { const input = document.createElement('input') input.type = 'file' input.accept = accept input.onchange = async () => { const file = input.files[0] if (!file) { reject(new Error('未选择文件')) return } try { const result = await uploadImage(file) resolve(result) } catch (e) { reject(e) } } input.click() }) } return { uploadImage, selectAndUpload, } }