diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4f7dc8..1331b486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ -# 1.2.4 +# 1.3.0 + +**REFACTOR** + +- 文件上传/图片上传重构(破坏性更新 不兼容之前的api) +- 文件上传/图片上传**不再支持**url用法 强制使用ossId **BUG FIX** - 测试菜单 请假申请 选中删除 需要根据状态判断 +- 修复文件/图片在Safari中无法上传 file-type库与Safari不兼容导致 **OTHER** diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 633fb205..2f08835f 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-antd", - "version": "1.2.3", + "version": "1.3.0", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/web-antd/src/api/system/oss/index.ts b/apps/web-antd/src/api/system/oss/index.ts index f1772aeb..073ae482 100644 --- a/apps/web-antd/src/api/system/oss/index.ts +++ b/apps/web-antd/src/api/system/oss/index.ts @@ -29,7 +29,7 @@ export function ossList(params?: PageQuery) { * @param ossIds id数组 * @returns 信息数组 */ -export function ossInfo(ossIds: IDS) { +export function ossInfo(ossIds: ID | IDS) { return requestClient.get(`${Api.ossInfo}/${ossIds}`); } diff --git a/apps/web-antd/src/components/upload/src/file-upload.vue b/apps/web-antd/src/components/upload/src/file-upload.vue index c6f14f11..2316cb52 100644 --- a/apps/web-antd/src/components/upload/src/file-upload.vue +++ b/apps/web-antd/src/components/upload/src/file-upload.vue @@ -1,243 +1,143 @@ - - - diff --git a/apps/web-antd/src/components/upload/src/helper.ts b/apps/web-antd/src/components/upload/src/helper.ts index ff70b413..9b85a533 100644 --- a/apps/web-antd/src/components/upload/src/helper.ts +++ b/apps/web-antd/src/components/upload/src/helper.ts @@ -1,51 +1,28 @@ -import { fileTypeFromBlob } from '@vben/utils'; +import type { UploadFile } from 'ant-design-vue'; /** - * 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55 - * 需要自行修改 - * @param file file对象 - * @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*) - * @returns 是否通过文件类型校验 + * 默认支持上传的图片文件类型 */ -export async function checkFileType(file: File, accepts: string[]) { - if (!accepts || accepts?.length === 0) { - return true; - } - console.log(file); - const fileType = await fileTypeFromBlob(file); - if (!fileType) { - console.error('无法获取文件类型'); - return false; - } - console.log('文件类型', fileType); - // 是否文件拓展名/文件头任意有一个匹配 - return accepts.includes(fileType.ext) || accepts.includes(fileType.mime); -} +export const defaultImageAcceptExts = [ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.webp', +]; /** - * 默认图片类型 + * 默认支持上传的文件类型 */ -export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp']; +export const defaultFileAcceptExts = ['.xlsx', '.csv', '.docx', '.pdf']; + /** - * 判断文件类型是否符合要求 - * @param file file对象 - * @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*) - * @returns 是否通过文件类型校验 + * 文件(非图片)的默认预览逻辑 + * 默认: window.open打开 交给浏览器接管 + * @param file file */ -export async function checkImageFileType(file: File, accepts: string[]) { - // 空的accepts 使用默认规则 - if (!accepts || accepts.length === 0) { - accepts = defaultImageAccept; +export function defaultFilePreview(file: UploadFile) { + if (file?.url) { + window.open(file.url); } - const fileType = await fileTypeFromBlob(file); - if (!fileType) { - console.error('无法获取文件类型'); - return false; - } - console.log('文件类型', fileType); - // 是否文件拓展名/文件头任意有一个匹配 - if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) { - return true; - } - return false; } diff --git a/apps/web-antd/src/components/upload/src/hook.ts b/apps/web-antd/src/components/upload/src/hook.ts new file mode 100644 index 00000000..8d9500f9 --- /dev/null +++ b/apps/web-antd/src/components/upload/src/hook.ts @@ -0,0 +1,255 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { UploadChangeParam, UploadFile } from 'ant-design-vue'; +import type { FileType } from 'ant-design-vue/es/upload/interface'; + +import type { ModelRef } from 'vue'; + +import type { HttpResponse } from '@vben/request'; + +import type { UploadResult } from '#/api'; +import type { OssFile } from '#/api/system/oss/model'; + +import { computed, ref, watch } from 'vue'; + +import { useAppConfig } from '@vben/hooks'; +import { $t } from '@vben/locales'; +import { useAccessStore } from '@vben/stores'; + +import { message, Modal } from 'ant-design-vue'; + +import { ossInfo } from '#/api/system/oss'; + +/** + * 图片预览hook + * @returns 预览 + */ +export function useImagePreview() { + /** + * 获取base64字符串 + * @param file 文件 + * @returns base64字符串 + */ + function getBase64(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.addEventListener('load', () => resolve(reader.result)); + reader.addEventListener('error', (error) => reject(error)); + }); + } + + // Modal可见 + const previewVisible = ref(false); + // 预览的图片 url/base64 + const previewImage = ref(''); + // 预览的图片名称 + const previewTitle = ref(''); + + function handleCancel() { + previewVisible.value = false; + previewTitle.value = ''; + } + + async function handlePreview(file: UploadFile) { + if (!file) { + return; + } + // 文件预览 取base64 + if (!file.url && !file.preview && file.originFileObj) { + file.preview = (await getBase64(file.originFileObj)) as string; + } + // 这里不可能为空 + const url = file.url ?? ''; + previewImage.value = url || file.preview || ''; + previewVisible.value = true; + previewTitle.value = + file.name || url.slice(Math.max(0, url.lastIndexOf('/') + 1)); + } + + return { + previewVisible, + previewImage, + previewTitle, + handleCancel, + handlePreview, + }; +} + +/** + * 图片上传和文件上传的通用hook + * @param props 组件props + * @param bindValue 双向绑定的idList + * @returns hook + */ +export function useUpload( + props: Readonly, + bindValue: ModelRef, +) { + const { apiURL, clientId } = useAppConfig( + import.meta.env, + import.meta.env.PROD, + ); + + // 内部维护fileList + const innerFileList = ref([]); + + /** oss上传地址 */ + const uploadUrl = `${apiURL}/resource/oss/upload`; + + const accessStore = useAccessStore(); + + /** + * header参数 需要带上token和clientId + */ + const headers = computed(() => { + return { + Authorization: `Bearer ${accessStore.accessToken}`, + clientId, + }; + }); + + const acceptFormat = computed(() => { + return props.accept + ?.split(',') + .map((item) => { + if (item.startsWith('.')) { + return item.slice(1); + } + return item; + }) + .join(', '); + }); + + function handleChange(info: UploadChangeParam) { + function removeCurrentFile( + currentFile: UploadChangeParam['file'], + currentFileList: UploadChangeParam['fileList'], + ) { + if (props.removeOnError) { + currentFileList.splice(currentFileList.indexOf(currentFile), 1); + } else { + currentFile.status = 'error'; + } + } + + const { file: currentFile, fileList } = info; + + switch (currentFile.status) { + // 上传成功 只是判断httpStatus 200 需要手动判断业务code + case 'done': { + if (!currentFile.response) { + return; + } + // 获取返回结果 + const response = currentFile.response as HttpResponse; + // 上传异常 + if (response.code !== 200) { + message.error(response.msg); + removeCurrentFile(currentFile, fileList); + return; + } + // 上传成功 做转换 + if (props.showSuccessMsg) { + message.success($t('component.upload.uploadSuccess')); + } + const { ossId, fileName, url } = response.data; + currentFile.url = url; + currentFile.fileName = fileName; + currentFile.uid = ossId; + // ossID添加 单个文件会被当做string + if (props.maxCount === 1) { + bindValue.value = ossId; + } else { + (bindValue.value as string[]).push(ossId); + } + break; + } + // 上传失败 网络原因导致httpStatus 不等于200 + case 'error': { + message.error($t('component.upload.uploadError')); + removeCurrentFile(currentFile, fileList); + } + } + } + + function handleRemove(currentFile: UploadFile) { + function remove() { + // fileList会自行处理删除 这里只需要处理ossId + if (props.maxCount === 1) { + bindValue.value = ''; + } else { + (bindValue.value as string[]).splice( + bindValue.value.indexOf(currentFile.uid), + 1, + ); + } + } + + if (!props.removeConfirm) { + remove(); + return true; + } + + return new Promise((resolve) => { + Modal.confirm({ + title: $t('pages.common.tip'), + content: $t('component.upload.confirmDelete', [currentFile.name]), + okButtonProps: { danger: true }, + centered: true, + onOk() { + resolve(true); + remove(); + }, + onCancel() { + resolve(false); + }, + }); + }); + } + + function beforeUpload(file: FileType) { + const isLtMax = file.size / 1024 / 1024 < props.maxSize!; + if (!isLtMax) { + message.error($t('component.upload.maxSize', [props.maxSize])); + return false; + } + // 大坑 Safari不支持file-type库 去除文件类型的校验 + return file; + } + + /** + * 这里只监听list地址变化 即重新赋值才会触发watch + * immediate用于初始化触发 + */ + watch( + () => bindValue.value, + async (value) => { + if (value.length === 0) { + return; + } + const resp = await ossInfo(value); + function transformFile(info: OssFile) { + const fileitem: UploadFile = { + uid: info.ossId, + name: info.originalName, + fileName: info.originalName, + url: info.url, + status: 'done', + }; + return fileitem; + } + innerFileList.value = resp.map((item) => transformFile(item)); + }, + { immediate: true }, + ); + + return { + uploadUrl, + headers, + handleChange, + handleRemove, + beforeUpload, + innerFileList, + acceptFormat, + }; +} diff --git a/apps/web-antd/src/components/upload/src/image-upload.vue b/apps/web-antd/src/components/upload/src/image-upload.vue index 3f4a7ae9..efd70d9b 100644 --- a/apps/web-antd/src/components/upload/src/image-upload.vue +++ b/apps/web-antd/src/components/upload/src/image-upload.vue @@ -1,326 +1,170 @@ - + + + - - + + - diff --git a/apps/web-antd/src/components/upload/src/note.md b/apps/web-antd/src/components/upload/src/note.md new file mode 100644 index 00000000..23dd610e --- /dev/null +++ b/apps/web-antd/src/components/upload/src/note.md @@ -0,0 +1,22 @@ +Safari在执行到beforeUpload方法 + +有两种情况 + +1. 不继续执行 也无法上传(没有调用上传) +2. 报错 + +Unhandled Promise Rejection: TypeError: ReadableStreamBYOBReader needs a ReadableByteStreamController + +https://github.com/oven-sh/bun/issues/12908#issuecomment-2490151231 + +刚开始以为是异步的问题 由于`file-type`调用了异步方法 调试也是在这里没有后续打印了 + +使用别的异步代码测试结果是正常上传的 + +```js +return new Promise((resolve) => + setTimeout(() => resolve(file), 2000), +); +``` + +根本原因在于`file-typ`库可能不支持Safari 去掉可以正常上传 diff --git a/apps/web-antd/src/components/upload/src/props.d.ts b/apps/web-antd/src/components/upload/src/props.d.ts new file mode 100644 index 00000000..eba5bcd6 --- /dev/null +++ b/apps/web-antd/src/components/upload/src/props.d.ts @@ -0,0 +1,51 @@ +interface BaseUploadProps { + /** + * 文件上传失败 是否从展示列表中删除 + * @default true + */ + removeOnError?: boolean; + /** + * 上传成功 是否展示提示信息 + * @default true + */ + showSuccessMsg?: boolean; + /** + * 删除文件前是否需要确认 + * @default false + */ + removeConfirm?: boolean; + /** + * 同antdv参数 + */ + accept?: string; + /** + * 附带的请求参数 + */ + data?: any; + /** + * 最大上传图片数量 + * maxCount为1时 会被绑定为string而非string[] + * @default 1 + */ + maxCount?: number; + /** + * 文件最大 单位M + * @default 5 + */ + maxSize?: number; + /** + * 是否禁用 + * @default false + */ + disabled?: boolean; + /** + * 是否显示文案 请上传不超过... + * @default true + */ + helpMessage?: boolean; + /** + * 是否支持多选文件,ie10+ 支持。开启后按住 ctrl 可选择多个文件。 + * @default false + */ + multiple?: boolean; +} diff --git a/apps/web-antd/src/components/upload/src/typing.ts b/apps/web-antd/src/components/upload/src/typing.ts deleted file mode 100644 index 8d728b6c..00000000 --- a/apps/web-antd/src/components/upload/src/typing.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Recordable } from '@vben/types'; - -export enum UploadResultStatus { - DONE = 'done', - ERROR = 'error', - SUCCESS = 'success', - UPLOADING = 'uploading', -} - -export interface FileItem { - thumbUrl?: string; - name: string; - size: number | string; - type?: string; - percent: number; - file: File; - status?: UploadResultStatus; - response?: Recordable | { fileName: string; ossId: string; url: string }; - uuid: string; -} - -export interface Wrapper { - record: FileItem; - uidKey: string; - valueKey: string; -} - -export interface BaseFileItem { - uid: number | string; - url: string; - name?: string; -} -export interface PreviewFileItem { - url: string; - name: string; - type: string; -} diff --git a/apps/web-antd/src/components/upload/src/use-upload.ts b/apps/web-antd/src/components/upload/src/use-upload.ts deleted file mode 100644 index 4ae552f3..00000000 --- a/apps/web-antd/src/components/upload/src/use-upload.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Ref } from 'vue'; - -import { computed, unref } from 'vue'; - -import { $t } from '@vben/locales'; - -export function useUploadType({ - acceptRef, - helpTextRef, - maxNumberRef, - maxSizeRef, -}: { - acceptRef: Ref; - helpTextRef: Ref; - maxNumberRef: Ref; - maxSizeRef: Ref; -}) { - // 文件类型限制 - const getAccept = computed(() => { - const accept = unref(acceptRef); - if (accept && accept.length > 0) { - return accept; - } - return []; - }); - const getStringAccept = computed(() => { - return unref(getAccept) - .map((item) => { - return item.indexOf('/') > 0 || item.startsWith('.') - ? item - : `.${item}`; - }) - .join(','); - }); - - // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。 - const getHelpText = computed(() => { - const helpText = unref(helpTextRef); - if (helpText) { - return helpText; - } - const helpTexts: string[] = []; - - const accept = unref(acceptRef); - if (accept.length > 0) { - helpTexts.push($t('component.upload.accept', [accept.join(',')])); - } - - const maxSize = unref(maxSizeRef); - if (maxSize) { - helpTexts.push($t('component.upload.maxSize', [maxSize])); - } - - const maxNumber = unref(maxNumberRef); - if (maxNumber && maxNumber !== Infinity) { - helpTexts.push($t('component.upload.maxNumber', [maxNumber])); - } - return helpTexts.join(','); - }); - return { getAccept, getStringAccept, getHelpText }; -} diff --git a/apps/web-antd/src/locales/langs/en-US/component.json b/apps/web-antd/src/locales/langs/en-US/component.json index c8ebf685..9be7b782 100644 --- a/apps/web-antd/src/locales/langs/en-US/component.json +++ b/apps/web-antd/src/locales/langs/en-US/component.json @@ -50,6 +50,9 @@ "uploadError": "Upload failed", "uploading": "Uploading", "uploadWait": "Please wait for the file upload to finish", - "reUploadFailed": "Re-upload failed files" + "reUploadFailed": "Re-upload failed files", + "uploadHelpMessage": "Please upload a file in {ext} format that does not exceed {size} MB.", + "unknownFileType": "Unknown file type, unable to upload", + "confirmDelete": "Confirm file deletion {0}?" } } diff --git a/apps/web-antd/src/locales/langs/en-US/pages.json b/apps/web-antd/src/locales/langs/en-US/pages.json index ea66e516..4b736ab0 100644 --- a/apps/web-antd/src/locales/langs/en-US/pages.json +++ b/apps/web-antd/src/locales/langs/en-US/pages.json @@ -18,6 +18,7 @@ "refresh": "Refresh", "generate": "Generate", "downloadLoading": "Downloading... Please wait.", - "preview": "Preview" + "preview": "Preview", + "tip": "Tip" } } diff --git a/apps/web-antd/src/locales/langs/zh-CN/component.json b/apps/web-antd/src/locales/langs/zh-CN/component.json index b0ef5a21..738e24d8 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/component.json +++ b/apps/web-antd/src/locales/langs/zh-CN/component.json @@ -50,6 +50,9 @@ "uploadError": "上传失败", "uploading": "上传中", "uploadWait": "请等待文件上传结束后操作", - "reUploadFailed": "重新上传失败文件" + "reUploadFailed": "重新上传失败文件", + "uploadHelpMessage": "请上传不超过{size}MB的{ext}格式文件", + "unknownFileType": "未知的文件类型, 无法上传", + "confirmDelete": "确认删除文件 {0}?" } } diff --git a/apps/web-antd/src/locales/langs/zh-CN/pages.json b/apps/web-antd/src/locales/langs/zh-CN/pages.json index 1bff1bb4..0e6e9b6e 100644 --- a/apps/web-antd/src/locales/langs/zh-CN/pages.json +++ b/apps/web-antd/src/locales/langs/zh-CN/pages.json @@ -18,6 +18,7 @@ "refresh": "刷新", "generate": "生成", "downloadLoading": "下载中, 请稍后...", - "preview": "预览" + "preview": "预览", + "tip": "提示" } } diff --git a/apps/web-antd/src/views/演示使用自行删除/upload/index.vue b/apps/web-antd/src/views/演示使用自行删除/upload/index.vue index aa2971a6..5266f8ec 100644 --- a/apps/web-antd/src/views/演示使用自行删除/upload/index.vue +++ b/apps/web-antd/src/views/演示使用自行删除/upload/index.vue @@ -1,65 +1,70 @@ diff --git a/packages/locales/src/index.ts b/packages/locales/src/index.ts index d4bfd819..cccfafb1 100644 --- a/packages/locales/src/index.ts +++ b/packages/locales/src/index.ts @@ -25,6 +25,6 @@ export { } from './typing'; export type { CompileError } from '@intlify/core-base'; -export { useI18n } from 'vue-i18n'; +export { I18nT, useI18n } from 'vue-i18n'; export type { Locale } from 'vue-i18n';