| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- 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<Array<{previewUrl:string,url:string,fileName?:string,size?:number}>>}
- */
- 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))
- }
|