import { BASE_URL } from '../config/api.js' export const ACCEPT = 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.zip' export const ACCEPT_EXTENSIONS = [ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.mp4', '.mov', '.avi', '.webm', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.zip' ] const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'] let _uploadSeq = 0 function nextUploadId() { _uploadSeq += 1 return `up_${Date.now()}_${_uploadSeq}` } export function isImageUrl(urlOrName = '') { const name = String(urlOrName).split('?')[0].toLowerCase() return IMAGE_EXTS.some((ext) => name.endsWith(ext)) } export function fileNameFromUrl(url = '') { try { const path = String(url).split('?')[0] const seg = path.substring(path.lastIndexOf('/') + 1) return decodeURIComponent(seg || '附件') } catch (e) { return '附件' } } export function revokePreview(item) { const p = item && item.previewUrl if (p && String(p).startsWith('blob:')) { try { URL.revokeObjectURL(p) } catch (e) {} } } function parseUploadBody(raw) { const body = typeof raw === 'string' ? JSON.parse(raw) : raw if (body && body.code === 200) return body.data const err = body || { message: '上传失败' } uni.showToast({ title: err.message || '上传失败', icon: 'none' }) throw err } export function uploadFile(filePath, folder = 'feedback', fileName, onProgress) { return new Promise((resolve, reject) => { const formData = { folder } if (fileName) formData.fileName = fileName const task = uni.uploadFile({ url: `${BASE_URL}/public/attachment/upload`, filePath, name: 'file', formData, success: (res) => { try { resolve(parseUploadBody(res.data)) } catch (e) { reject(e) } }, fail: reject }) if (task && typeof task.onProgressUpdate === 'function' && onProgress) { task.onProgressUpdate((res) => { const p = typeof res.progress === 'number' ? res.progress : 0 onProgress(Math.max(0, Math.min(100, Math.round(p)))) }) } }) } function pickFilesViaInput() { return new Promise((resolve, reject) => { // #ifdef H5 const input = document.createElement('input') input.type = 'file' input.multiple = true input.accept = ACCEPT input.style.display = 'none' const cleanup = () => { if (input.parentNode) input.parentNode.removeChild(input) } input.onchange = () => { const files = Array.from(input.files || []) cleanup() resolve(files) } input.oncancel = () => { cleanup() reject(new Error('cancel')) } document.body.appendChild(input) input.click() // #endif // #ifndef H5 reject(new Error('unsupported')) // #endif }) } function uploadBlobAsFile(file, folder, onProgress) { return new Promise((resolve, reject) => { // #ifdef H5 const url = `${BASE_URL}/public/attachment/upload` const fd = new FormData() fd.append('file', file, file.name) fd.append('folder', folder) const xhr = new XMLHttpRequest() xhr.open('POST', url) if (xhr.upload && onProgress) { xhr.upload.onprogress = (e) => { if (!e.lengthComputable) return onProgress(Math.max(0, Math.min(100, Math.round((e.loaded / e.total) * 100)))) } } xhr.onload = () => { try { resolve(parseUploadBody(xhr.responseText)) } catch (e) { reject(e) } } xhr.onerror = () => reject(new Error('network')) xhr.send(fd) // #endif // #ifndef H5 reject(new Error('unsupported')) // #endif }) } /** * 选择并上传多种类型附件,不限制个数。 * @param {string} folder * @param {{ * onItemAdd?: (item: object) => void, * onProgress?: (item: object, progress: number) => void, * onItemSuccess?: (item: object, data: object) => void, * onItemError?: (item: object, err: any) => void * }} handlers * @returns {Promise>} */ export function chooseAndUploadFiles(folder = 'feedback', handlers = {}) { const { onItemAdd, onProgress, onItemSuccess, onItemError } = handlers || {} const trackUpload = async (draft, uploadFn) => { onItemAdd && onItemAdd(draft) try { const data = await uploadFn((p) => { draft.progress = p onProgress && onProgress(draft, p) }) draft.uploading = false draft.progress = 100 draft.url = data?.url || '' if (data?.fileName) draft.fileName = data.fileName if (data?.size != null) draft.size = data.size onItemSuccess && onItemSuccess(draft, data) return draft } catch (err) { draft.uploading = false onItemError && onItemError(draft, err) throw err } } return new Promise(async (resolve, reject) => { try { // 优先 H5 原生多选(支持任意类型、不限个数) // #ifdef H5 try { const files = await pickFilesViaInput() if (!files.length) { resolve([]) return } const items = [] for (const file of files) { const previewUrl = URL.createObjectURL(file) const draft = { id: nextUploadId(), previewUrl, url: '', fileName: file.name, size: file.size, uploading: true, progress: 0 } try { const done = await trackUpload(draft, (onP) => uploadBlobAsFile(file, folder, onP) ) if (done?.url) items.push(done) else URL.revokeObjectURL(previewUrl) } catch (err) { URL.revokeObjectURL(previewUrl) // 单项失败已 toast;继续后续文件 } } resolve(items) return } catch (e) { if (e && e.message === 'cancel') { resolve([]) return } } // #endif // App / 部分端:uni.chooseFile if (typeof uni.chooseFile === 'function') { uni.chooseFile({ count: 99, type: 'all', extension: ACCEPT_EXTENSIONS.map((e) => e.replace(/^\./, '')), success: async (res) => { try { const items = [] const paths = res.tempFilePaths || [] const tempFiles = res.tempFiles || [] for (let i = 0; i < paths.length; i++) { const path = paths[i] const name = tempFiles[i]?.name const draft = { id: nextUploadId(), previewUrl: path, url: '', fileName: name || fileNameFromUrl(path), size: tempFiles[i]?.size, uploading: true, progress: 0 } try { const done = await trackUpload(draft, (onP) => uploadFile(path, folder, name, onP) ) if (done?.url) items.push(done) } catch (err) { // 单项失败已 toast } } resolve(items) } catch (err) { reject(err) } }, fail: (err) => { if (err && (err.errMsg || '').includes('cancel')) { resolve([]) return } reject(err) } }) return } // 兜底:仅图片 uni.chooseImage({ count: 9, success: async (res) => { try { const items = [] for (const path of res.tempFilePaths || []) { const draft = { id: nextUploadId(), previewUrl: path, url: '', fileName: fileNameFromUrl(path), uploading: true, progress: 0 } try { const done = await trackUpload(draft, (onP) => uploadFile(path, folder, undefined, onP) ) if (done?.url) items.push(done) } catch (err) { // 单项失败已 toast } } resolve(items) } catch (err) { reject(err) } }, fail: reject }) } catch (e) { reject(e) } }) } /** @deprecated 使用 chooseAndUploadFiles */ export function chooseAndUploadImages(count = 9, folder = 'feedback') { return chooseAndUploadFiles(folder).then((items) => items.map((i) => i.url)) }