From a20b281dd2e9fcf13f45d3001963c9cd453529fe Mon Sep 17 00:00:00 2001 From: luk Date: Sun, 8 Sep 2024 11:50:06 +0800 Subject: [PATCH] improve pickupFile --- unitool.js | 155 ++++++++++++++++++++++++++++------------------------- 1 file changed, 82 insertions(+), 73 deletions(-) diff --git a/unitool.js b/unitool.js index 69bc887..ea49324 100644 --- a/unitool.js +++ b/unitool.js @@ -145,7 +145,7 @@ module.exports = { if (/^\/static\//.test(route)) { return route } - // 纯字母的cid + // 纯数字和字母的cid if (/^[\da-zA-Z]+$/.test(route) && envar.ipfsLens) { return `${envar.ipfsLens.replace(/\/$/, '')}/${route.replace(/^\//, '')}` } @@ -339,48 +339,53 @@ module.exports = { return { _state: 'CER_EMPTY_FILE', _msg: { zhCN: '文件为空,无法上传。', enUS: 'Empty files cannot be uploaded.' } } } else if (fileSize > (globalThis.wo?.envar?.fileSizeLimit || 10485760)) { let sizeLimitMB = parseInt((globalThis.wo?.envar?.fileSizeLimit || 10485760) / 1048576) + 'MB' - return { _state: 'CER_FILE_TOO_LARGE', _msg: { zhCN: `文件太大了,无法上传。最大允许 ${sizeLimitMB}`, enUS: `File too large to upload. Maximum allowed is ${sizeLimitMB}` } } + return { _state: 'CER_FILE_TOO_LARGE', _msg: { zhCN: `文件大于 ${sizeLimitMB} MB,无法上传`, enUS: `The file exceeds ${sizeLimitMB} MB and cannot be uploaded` } } } - let fileExt = filePicked?.name?.split?.('.')?.pop?.()?.toLowerCase?.() - if (mediaType === 'image' && !my.isImageFile(fileExt) - || mediaType === 'video' && !my.isVideoFile(fileExt) - || Array.isArray(mediaType) && !mediaType?.includes?.('.' + fileExt)) { - return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件:\n' + filePicked?.name, enUS: 'Unsupported file:\n' + filePicked?.name } } + const fileName = filePicked?.name || filePath?.split?.('/')?.pop?.() // filePicked.name is available in WEB only. on the other hand, filePath 在 WEB 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名。 + const fileExt = fileName?.split?.('.')?.pop?.()?.toLowerCase?.() + + if ( + // #ifndef APP + // 20240830 luk: 在 App 上,就相信 iOS/Android,不检查文件后缀名。 + mediaType === 'image' && !my.isImageFile(fileExt) || mediaType === 'video' && !my.isVideoFile(fileExt) || + // #endif + Array.isArray(mediaType) && !mediaType?.includes?.('.' + fileExt)) { + return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件类型:\n' + fileName, enUS: 'Unsupported file type:\n' + fileName } } } - if (filePath) { - for (let key in formData) { - // multer 不会自动处理 JSON 数据,必须前后端配合处理 - formData[key] = JSON.stringify(formData[key]) - } - // 在 Fileloader/fileloader.js 里,已经不再依赖 _passtoken,而且 header 在被 nginx 或 cloudflare (没搞清楚是谁干的)代理之后也被过滤掉了,因此不再使用这一句: header._passtoken = uni.getStorageSync('_passtoken') - formData['_passtoken'] = uni.getStorageSync('_passtoken') // 20230527 加回这一句,让后台可以根据验证用户来决定怎样处理文件。 - - this.showLoading() - let [errorUpload, { data, statusCode } = {}] = await uni.uploadFile({ url: this.make_server_url(url), filePath, name, header, formData }) - // 后台 Multer 处理 req.file = { destination, filename, originalname, path, mimetype, size }, 其中 path 包括了 destination 和 filename 的文件相对路径。 - // url 指向的后台方法进一步处理后,通过 uni.uploadFile 存在 data 里返回结果: { ...file, cid?, ipfsUrl?, baseUrl? } - this.hideLoading() - - if (typeof data === 'string') { - // 不知为何,uni.uploadFile返回的 data 是字符串而不是对象 - try { - data = JSON.parse(data) - } catch (exp) { - return { _state: 'BER_FAIL_RESPONSE_JSON_MALFORMED', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } - } - } - - if (data?._state === 'SUCCESS' && data?.path) { - // 后台送来的 baseUrl 在开发环境下,不一定符合前端实际,因为后台只知道预设的 servUrl 例如 https://pexserver.test.tic.cc:7739/... ,而开发环境下实际上是 http://localhost:7739/... 所以再设置一个 fileUrl 来根据 location.origin 调整。// todo: 改名叫 clientUrl 或 userUrl 或 baseUrl4Client,与 baseUrl 对应 - return { _state: 'SUCCESS', fileUrl: this.make_server_url(data.path), ...data } // { path, destination, filename, fileUrl, cid?, ipfsUrl?, baseUrl?, ...file } 注意,data.path 不包含起头的 '/' - } else { - return { _state: 'BER_FAIL_UPLOAD_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' }, error: errorUpload } - } - } else { + if (!filePath) { return { _state: 'BER_FAIL_CHOOSE_FILE', _msg: { zhCN: '文件选择失败。请稍后再试,或向客服投诉。', enUS: 'File choose failed. Please try again later, or report to customer service.' } } } + + for (let key in formData) { + // multer 不会自动处理 JSON 数据,必须前后端配合处理 + formData[key] = JSON.stringify(formData[key]) + } + // 在 Fileloader/fileloader.js 里,已经不再依赖 _passtoken,而且 header 在被 nginx 或 cloudflare (没搞清楚是谁干的)代理之后也被过滤掉了,因此不再使用这一句: header._passtoken = uni.getStorageSync('_passtoken') + formData['_passtoken'] = uni.getStorageSync('_passtoken') // 20230527 加回这一句,让后台可以根据验证用户来决定怎样处理文件。 + + this.showLoading() + let [errorUpload, { data, statusCode } = {}] = await uni.uploadFile({ url: this.make_server_url(url), filePath, name, header, formData }) + // 后台 Multer 处理 req.file = { destination, filename, originalname, path, mimetype, size }, 其中 path 包括了 destination 和 filename 的文件相对路径。 + // url 指向的后台方法进一步处理后,通过 uni.uploadFile 存在 data 里返回结果: { ...file, cid?, ipfsUrl?, baseUrl? } + this.hideLoading() + + if (typeof data === 'string') { + // 不知为何,uni.uploadFile返回的 data 是字符串而不是对象 + try { + data = JSON.parse(data) + } catch (exp) { + return { _state: 'BER_FAIL_RESPONSE_JSON_MALFORMED', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } + } + } + + if (data?._state === 'SUCCESS' && data?.path) { + // 后台送来的 baseUrl 在开发环境下,不一定符合前端实际,因为后台只知道预设的 servUrl 例如 https://pexserver.test.tic.cc:7739/... ,而开发环境下实际上是 http://localhost:7739/... 所以再设置一个 fileUrl 来根据 location.origin 调整。// todo: 改名叫 clientUrl 或 userUrl 或 baseUrl4Client,与 baseUrl 对应 + return { _state: 'SUCCESS', fileUrl: this.make_server_url(data.path), ...data } // { path, destination, filename, fileUrl, cid?, ipfsUrl?, baseUrl?, ...file } 注意,data.path 不包含起头的 '/' + } else { + return { _state: 'BER_FAIL_UPLOAD_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' }, error: errorUpload } + } }, async pickupFile2Cloud ({ mediaType = 'image', count = 1, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'], maxDuration } = {}) { @@ -442,15 +447,6 @@ module.exports = { } fileSize = tempFiles?.[0]?.size filePicked = tempFiles?.[0] - - let fileExt = filePicked?.name?.split?.('.')?.pop?.()?.toLowerCase?.() - if (Array.isArray(mediaType) && !mediaType?.includes?.('.' + fileExt)) { - return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件:\n' + filePicked?.name, enUS: 'Unsupported file:\n' + filePicked?.name } } - } - if (!fileSize) { - return { _state: 'CER_EMPTY_FILE', _msg: { zhCN: '文件为空,无法上传。', enUS: 'Empty files cannot be uploaded.' } } - } - return { _state: 'SUCCESS', fileUrl: tempFiles?.[0]?.url } // #endif // #ifndef WEB return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '请切换到网页端上传文件!', enUS: 'Please switch to WebApp to upload files.' } } @@ -461,35 +457,45 @@ module.exports = { return { _state: 'CER_EMPTY_FILE', _msg: { zhCN: '文件为空,无法上传。', enUS: 'Empty files cannot be uploaded.' } } } else if (fileSize > (globalThis.wo?.envar?.fileSizeLimit || 10485760)) { let sizeLimitMB = parseInt((globalThis.wo?.envar?.fileSizeLimit || 10485760) / 1048576) + 'MB' - return { _state: 'CER_FILE_TOO_LARGE', _msg: { zhCN: `文件太大了,无法上传。最大允许 ${sizeLimitMB}`, enUS: `File too large to upload. Maximum allowed is ${sizeLimitMB}` } } + return { _state: 'CER_FILE_TOO_LARGE', _msg: { zhCN: `文件大于 ${sizeLimitMB} MB,无法上传`, enUS: `The file exceeds ${sizeLimitMB} MB and cannot be uploaded` } } } - let fileExt = filePicked?.name?.split?.('.')?.pop?.()?.toLowerCase?.() - if (mediaType === 'image' && !my.isImageFile(fileExt) - || mediaType === 'video' && !my.isVideoFile(fileExt) - || Array.isArray(mediaType) && !mediaType?.includes?.('.' + fileExt)) { - return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件:\n' + filePicked?.name, enUS: 'Unsupported file:\n' + filePicked?.name } } + const fileName = filePicked?.name || filePath?.split?.('/')?.pop?.() // filePicked.name is available in WEB only. on the other hand, filePath 在 WEB 上并不是文件路径名,而是类似 "blob:http://localhost:8080/f0d3e54d-0694-4803-8097-641d76a10b0d“。在 iOS 上是 "_doc/uniapp_temp_1598593902955/compressed/1598593925815.png", 有时还包含从 file:/// 开始的完整路径名。 + const fileExt = fileName?.split?.('.')?.pop?.()?.toLowerCase?.() + + if ( + // #ifndef APP + // 20240830 luk: 在 App 上,就相信 iOS/Android,不检查文件后缀名。 + mediaType === 'image' && !my.isImageFile(fileExt) || mediaType === 'video' && !my.isVideoFile(fileExt) || + // #endif + Array.isArray(mediaType) && !mediaType?.includes?.('.' + fileExt)) { + return { _state: 'UNSUPPORTED_FILETYPE', _msg: { zhCN: '不支持的文件类型:\n' + fileName, enUS: 'Unsupported file type:\n' + fileName } } } - if (filePath) { - this.showLoading() - const { fileID, requestId } = await uniCloud.uploadFile({ - filePath: filePath, - cloudPath: (process.env.NODE_ENV !== 'production' ? 'dev_' : '') + cloudPath, // 关键是要具有文件格式后缀名,这样可以保持阿里云下载链接也用这个后缀名。 - //fileType: mediaType, // = image, video, audio. Looks like only necessary for for 支付宝小程序: https://uniapp.dcloud.net.cn/uniCloud/storage.html#uploadfile - onUploadProgress: function (progressEvent) { - var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) - }, - }) - this.hideLoading() - - if (fileID) { - return { _state: 'SUCCESS', fileUrl: fileID, requestId } - } else { - return { _state: 'BER_FAIL_UPLOAD_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } - } + if (mediaType !== 'video' && mediaType !== 'image') { // 这一句应该在上面的 uniCloud.chooseAndUploadFile() 分支里,不过为了合用 fileSize 和 fileExt 的判断,就放在这里。 + return { _state: 'SUCCESS', fileUrl: filePicked?.url } + } + + if (!filePath) { + return { _state: 'BER_FAIL_CHOOSE_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } + } + + this.showLoading() + const { fileID, requestId } = await uniCloud.uploadFile({ + filePath: filePath, + cloudPath: (process.env.NODE_ENV !== 'production' ? 'dev_' : '') + cloudPath, // 关键是要具有文件格式后缀名,这样可以保持阿里云下载链接也用这个后缀名。 + //fileType: mediaType, // = image, video, audio. Looks like only necessary for for 支付宝小程序: https://uniapp.dcloud.net.cn/uniCloud/storage.html#uploadfile + onUploadProgress: function (progressEvent) { + var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) + }, + }) + this.hideLoading() + + if (fileID) { + return { _state: 'SUCCESS', fileUrl: fileID, requestId } + } else { + return { _state: 'BER_FAIL_UPLOAD_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } } - return { _state: 'BER_FAIL_CHOOSE_FILE', _msg: { zhCN: '文件上传失败。请稍后再试,或向客服投诉。', enUS: 'File upload failed. Please try again later, or report to customer service.' } } }, async pickupFile ({ @@ -616,7 +622,7 @@ module.exports = { showModal (option = {}) { option.title = this.localizeText(option.title) - option.content = this.localizeText(option.content)?.substring?.(0, 100) + option.content = (uni.getSystemInfoSync().uniPlatform === 'app' ? '\n' : '') + this.localizeText(option.content)?.substring?.(0, option.contentLength || 300) if (option.content) option.content += '\n\n' option.cancelText = this.localizeText(option.cancelText || { zhCN: '取消', enUS: 'Cancel' }) option.confirmText = this.localizeText(option.confirmText || { zhCN: '好的', enUS: 'OK' }) @@ -772,7 +778,7 @@ module.exports = { } }, - copy_to_clipboard (text, { promptLength = 50, hidePrompt = false } = {}) { + copy_to_clipboard (text, { promptLength = 50, hidePrompt = false, sysToast = false } = {}) { text = this.localizeText?.(text) || text const self = this uni.setClipboardData({ @@ -785,9 +791,12 @@ module.exports = { } self.showToast?.({ type: 'success', - title: `${this.localizeText?.({ zhCN: '已成功拷贝\n', enUS: 'Successfully copied\n' }) || ''}${text}`, + title: `${this.localizeText?.({ zhCN: '已拷贝\n', enUS: 'Copied\n' }) || ''}${text}`, }) } + if (sysToast) { + uni.showToast({ title: this.localizeText?.({ zhCN: '已拷贝', enUS: 'Copied' }) }) + } }, }) },