diff --git a/.env.prod b/.env.prod index b33a845..899b45b 100644 --- a/.env.prod +++ b/.env.prod @@ -21,3 +21,9 @@ VITE_IMAGE_BASE_PATH = https://www.pjzhhq.cn/api # 代码生成服务地址 VITE_GEN_PROXY_PATH = http://123.156.230.15:5003 + + +VITE_FILE_ACCESS_APP_ID="admin" +VITE_FILE_ACCESS_KEY_ID="v1" +VITE_FILE_ACCESS_KEY="5UhL2DT+y//+0qCF4Abm9h6MPbEOvJfGuLf2eZ28QSw=" +VITE_FILE_ACCESS_ALLOWED_ORIGINS="https://www.pjzhhq.cn,https://stgl.pj.gov.cn" diff --git a/src/components/Upload/Image.vue b/src/components/Upload/Image.vue index 1837cdf..b219d2b 100644 --- a/src/components/Upload/Image.vue +++ b/src/components/Upload/Image.vue @@ -66,7 +66,7 @@ interface UploadFileProps { drag?: boolean // 是否支持拖拽上传 ==> 非必传(默认为 true) disabled?: boolean // 是否禁用上传组件 ==> 非必传(默认为 false) fileSize?: number // 图片大小限制 ==> 非必传(默认为 5M) - fileType?: File.ImageMimeType[] // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]) + fileType?: string[] // 图片类型限制 ==> 非必传(默认 JPEG、PNG、GIF、WebP) height?: string // 组件高度 ==> 非必传(默认为 150px) width?: string // 组件宽度 ==> 非必传(默认为 150px) borderRadius?: string // 组件边框圆角 ==> 非必传(默认为 8px) @@ -82,7 +82,7 @@ const props = withDefaults(defineProps(), { drag: true, disabled: false, fileSize: 5, - fileType: () => ["image/jpeg", "image/png", "image/gif"], + fileType: () => ["image/jpeg", "image/png", "image/gif", "image/webp"], height: "150px", width: "150px", borderRadius: "8px", @@ -116,21 +116,17 @@ const handleHttpUpload = async (options: UploadRequestOptions) => { let formData = new FormData() formData.append("file", options.file) formData.append("dir", props.dir) - try { - const { data } = await request({ - url: props.uploadFileUrl, - method: "post", - headers: { - "Content-Type": "multipart/form-data", - }, - data: formData, - }) - emit("update:imageUrl", data.url) - // 调用 el-form 内部的校验方法(可自动校验) - formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]) - } catch (error) { - options.onError(error as any) - } + const { data } = await request({ + url: props.uploadFileUrl, + method: "post", + headers: { + "Content-Type": "multipart/form-data", + }, + data: formData, + }) + emit("update:imageUrl", data.url) + // 调用 el-form 内部的校验方法(可自动校验) + formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]) } /** @@ -186,10 +182,10 @@ const uploadSuccess = () => { /** * @description 图片上传错误 * */ -const uploadError = () => { +const uploadError = (error: any) => { ElNotification({ title: "温馨提示", - message: "图片上传失败,请您重新上传!", + message: error?.msg || error?.message || "图片上传失败,请您重新上传!", type: "error", }) } diff --git a/src/components/Upload/Images.vue b/src/components/Upload/Images.vue index 127b06a..c2d5f96 100644 --- a/src/components/Upload/Images.vue +++ b/src/components/Upload/Images.vue @@ -42,7 +42,7 @@ interface UploadFileProps { } const props = withDefaults(defineProps(), { disabled: false, - fileType: () => ["image/jpeg", "image/png", "image/gif"], + fileType: () => ["image/jpeg", "image/png", "image/gif", "image/webp"], fileSize: 5, height: "150px", width: "150px", @@ -66,20 +66,16 @@ const formContext = inject(formContextKey, void 0) const handleHttpUpload = async (options: UploadRequestOptions) => { let formData = new FormData() formData.append("file", options.file) - try { - const res = await request({ - url: "/admin/sys-file/upload", - method: "post", - headers: { - "Content-Type": "multipart/form-data", - }, - data: formData, - }) - if (res.code == 0) { - return res - } - } catch (error) { - options.onError(error as any) + const res = await request({ + url: "/admin/sys-file/upload", + method: "post", + headers: { + "Content-Type": "multipart/form-data", + }, + data: formData, + }) + if (res.code == 0) { + return res } } @@ -151,10 +147,10 @@ const handleRemove = (uploadFile: any, uploadFiles: any[]) => { /** * @description 图片上传错误 * */ -const uploadError = () => { +const uploadError = (error: any) => { ElNotification({ title: "温馨提示", - message: "图片上传失败,请您重新上传!", + message: error?.msg || error?.message || "图片上传失败,请您重新上传!", type: "error", }) } diff --git a/src/components/Upload/qnImages.vue b/src/components/Upload/qnImages.vue index f2d350c..89949cf 100644 --- a/src/components/Upload/qnImages.vue +++ b/src/components/Upload/qnImages.vue @@ -31,7 +31,7 @@ interface UploadFileProps { } const props = withDefaults(defineProps(), { disabled: false, - fileType: () => ["image/jpeg", "image/png", "image/gif"], + fileType: () => ["image/jpeg", "image/png", "image/gif", "image/webp"], fileSize: 5, height: "150px", width: "150px", @@ -51,20 +51,16 @@ const formContext = inject(formContextKey, void 0) const handleHttpUpload = async (options: UploadRequestOptions) => { let formData = new FormData() formData.append("file", options.file) - try { - const res = await request({ - url: "/admin/sys-file/qn/upload", - method: "post", - headers: { - "Content-Type": "multipart/form-data", - }, - data: formData, - }) - if (res.code == 0) { - return res - } - } catch (error) { - options.onError(error as any) + const res = await request({ + url: "/admin/sys-file/qn/upload", + method: "post", + headers: { + "Content-Type": "multipart/form-data", + }, + data: formData, + }) + if (res.code == 0) { + return res } } @@ -137,10 +133,10 @@ const handleRemove = (uploadFile: any, uploadFiles: any[]) => { /** * @description 图片上传错误 * */ -const uploadError = () => { +const uploadError = (error: any) => { ElNotification({ title: "温馨提示", - message: "图片上传失败,请您重新上传!", + message: error?.msg || error?.message || "图片上传失败,请您重新上传!", type: "error", }) } diff --git a/tests/image-upload-types.test.cjs b/tests/image-upload-types.test.cjs new file mode 100644 index 0000000..7e6d46d --- /dev/null +++ b/tests/image-upload-types.test.cjs @@ -0,0 +1,62 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const vue = require('vue') +const { parse, compileScript } = require('@vue/compiler-sfc') +const { compile, root } = require('./helpers/file-access-runtime.cjs') + +function setup(name, request) { + const notifications = [], events = [], model = vue.ref('') + const descriptor = parse(fs.readFileSync(path.join(root, 'src/components/Upload', name), 'utf8')).descriptor + const loaded = { exports: {} } + const mockVue = { ...vue, inject() {}, onMounted() {}, onBeforeUnmount() {}, useModel: () => model } + const mocks = { + vue: mockVue, + 'element-plus': { ElNotification: value => notifications.push(value), formContextKey: Symbol(), formItemContextKey: Symbol() }, + '@element-plus/icons-vue': {}, + '/@/utils/request': { default: request }, + '/@/utils/other': { generateUUID: () => 'upload-test' }, + '/@/utils/file-access': { fileUrl: value => value, originalFileUrl: value => value, resolveFileUrl: value => value }, + '/@/utils/file-access-dom': { bindFileAccessDom() {} }, + } + const globals = { inject: mockVue.inject, computed: vue.computed, onMounted() {}, onBeforeUnmount() {} } + const source = compileScript(descriptor, { id: name }).content.replace(/import\.meta\.env/g, '({})') + new Function('module', 'exports', 'require', ...Object.keys(globals), compile(source))( + loaded, loaded.exports, key => key in mocks ? mocks[key] : require(key), ...Object.values(globals)) + const props = {} + for (const [name, settings] of Object.entries(loaded.exports.default.props)) { + if ('default' in settings) props[name] = typeof settings.default === 'function' ? settings.default() : settings.default + } + const state = loaded.exports.default.setup(props, { expose() {}, emit: (...args) => events.push(args) }) + return { state, props, notifications, events, model } +} + +for (const name of ['Image.vue', 'Images.vue', 'qnImages.vue']) { + test(name + ': WebP is allowed by default and backend errors never trigger a success notification', async () => { + const failure = { msg: '图片实际格式未在白名单中' } + const { state, props, notifications, events } = setup(name, async () => { throw failure }) + const file = new File(['public test fixture'], 'logo.webp', { type: 'image/webp' }) + assert.ok(props.fileType.includes('image/webp')) + assert.equal(state.beforeUpload(file), true) + assert.equal(notifications.length, 0) + let callbackCount = 0 + await state.handleHttpUpload({ file, onError() { callbackCount++ } }) + .then(state.uploadSuccess, state.uploadError) + assert.equal(callbackCount, 0, 'Element Plus handles the rejected Promise once') + assert.deepEqual(notifications.map(value => [value.type, value.message]), [['error', failure.msg]]) + assert.equal(events.length, 0) + props.fileType = ['image/png'] + assert.equal(state.beforeUpload(file), false, 'explicit business type restrictions are respected') + }) +} + +test('single-image uploads preserve the canonical raw WebP URL returned by the server', async () => { + const raw = '/admin/sys-file/oss/file?fileName=logo.webp' + const { state, events } = setup('Image.vue', async options => { + assert.equal(options.data.get('file').name, 'logo.png') + return { code: 0, data: { url: raw } } + }) + await state.handleHttpUpload({ file: new File(['test'], 'logo.png', { type: 'image/png' }) }) + assert.deepEqual(events, [['update:imageUrl', raw]]) +})