diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 68e7e88b3..a62b88065 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -3,15 +3,31 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component } from 'vue'; +import type { + UploadChangeParam, + UploadFile, + UploadProps, +} from 'ant-design-vue'; + +import type { Component, Ref } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; +import { + defineAsyncComponent, + defineComponent, + h, + ref, + render, + unref, + watch, +} from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; +import { IconifyIcon } from '@vben/icons'; import { $t } from '@vben/locales'; +import { isEmpty } from '@vben/utils'; import { notification } from 'ant-design-vue'; @@ -22,9 +38,6 @@ const AutoComplete = defineAsyncComponent( () => import('ant-design-vue/es/auto-complete'), ); const Button = defineAsyncComponent(() => import('ant-design-vue/es/button')); -const Cascader = defineAsyncComponent( - () => import('ant-design-vue/es/cascader'), -); const Checkbox = defineAsyncComponent( () => import('ant-design-vue/es/checkbox'), ); @@ -68,7 +81,14 @@ const TimeRangePicker = defineAsyncComponent(() => const TreeSelect = defineAsyncComponent( () => import('ant-design-vue/es/tree-select'), ); +const Cascader = defineAsyncComponent( + () => import('ant-design-vue/es/cascader'), +); const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload')); +const Image = defineAsyncComponent(() => import('ant-design-vue/es/image')); +const PreviewGroup = defineAsyncComponent(() => + import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup), +); const withDefaultPlaceholder = ( component: T, @@ -104,12 +124,223 @@ const withDefaultPlaceholder = ( }); }; +const withPreviewUpload = () => { + return defineComponent({ + name: Upload.name, + emits: ['change', 'update:modelValue'], + setup: ( + props: any, + { attrs, slots, emit }: { attrs: any; emit: any; slots: any }, + ) => { + const previewVisible = ref(false); + + const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`); + + const listType = attrs?.listType || attrs?.['list-type'] || 'text'; + + const fileList = ref( + attrs?.fileList || attrs?.['file-list'] || [], + ); + + const handleChange = async (event: UploadChangeParam) => { + fileList.value = event.fileList; + emit('change', event); + emit( + 'update:modelValue', + event.fileList?.length ? fileList.value : undefined, + ); + }; + + const handlePreview = async (file: UploadFile) => { + previewVisible.value = true; + await previewImage(file, previewVisible, fileList); + }; + + const renderUploadButton = (): any => { + const isDisabled = attrs.disabled; + + // 如果禁用,不渲染上传按钮 + if (isDisabled) { + return null; + } + + // 否则渲染默认上传按钮 + return isEmpty(slots) + ? createDefaultSlotsWithUpload(listType, placeholder) + : slots; + }; + + // 可以监听到表单API设置的值 + watch( + () => attrs.modelValue, + (res) => { + fileList.value = res; + }, + ); + + return () => + h( + Upload, + { + ...props, + ...attrs, + fileList: fileList.value, + onChange: handleChange, + onPreview: handlePreview, + }, + renderUploadButton(), + ); + }, + }); +}; + +const createDefaultSlotsWithUpload = ( + listType: string, + placeholder: string, +) => { + switch (listType) { + case 'picture-card': { + return { + default: () => placeholder, + }; + } + default: { + return { + default: () => + h( + Button, + { + icon: h(IconifyIcon, { + icon: 'ant-design:upload-outlined', + class: 'mb-1 size-4', + }), + }, + () => placeholder, + ), + }; + } + } +}; + +const previewImage = async ( + file: UploadFile, + visible: Ref, + fileList: Ref, +) => { + // 检查是否为图片文件的辅助函数 + const isImageFile = (file: UploadFile): boolean => { + const imageExtensions = new Set([ + 'bmp', + 'gif', + 'jpeg', + 'jpg', + 'png', + 'webp', + ]); + if (file.url) { + const ext = file.url?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + if (!file.type) { + const ext = file.name?.split('.').pop()?.toLowerCase(); + return ext ? imageExtensions.has(ext) : false; + } + return file.type.startsWith('image/'); + }; + + // 如果当前文件不是图片,直接打开 + if (!isImageFile(file)) { + if (file.url) { + window.open(file.url, '_blank'); + } else if (file.preview) { + window.open(file.preview, '_blank'); + } else { + console.warn('无法打开文件,没有可用的URL或预览地址'); + } + return; + } + + // 对于图片文件,继续使用预览组 + const [ImageComponent, PreviewGroupComponent] = await Promise.all([ + Image, + PreviewGroup, + ]); + + const 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)); + }); + }; + // 从fileList中过滤出所有图片文件 + const imageFiles = (unref(fileList) || []).filter((element) => + isImageFile(element), + ); + + // 为所有没有预览地址的图片生成预览 + for (const imgFile of imageFiles) { + if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) { + imgFile.preview = (await getBase64(imgFile.originFileObj)) as string; + } + } + const container: HTMLElement | null = document.createElement('div'); + document.body.append(container); + + // 用于追踪组件是否已卸载 + let isUnmounted = false; + + const PreviewWrapper = { + setup() { + return () => { + if (isUnmounted) return null; + return h( + PreviewGroupComponent, + { + class: 'hidden', + preview: { + visible: visible.value, + // 设置初始显示的图片索引 + current: imageFiles.findIndex((f) => f.uid === file.uid), + onVisibleChange: (value: boolean) => { + visible.value = value; + if (!value) { + // 延迟清理,确保动画完成 + setTimeout(() => { + if (!isUnmounted && container) { + isUnmounted = true; + render(null, container); + container.remove(); + } + }, 300); + } + }, + }, + }, + () => + // 渲染所有图片文件 + imageFiles.map((imgFile) => + h(ImageComponent, { + key: imgFile.uid, + src: imgFile.url || imgFile.preview, + }), + ), + ); + }; + }, + }; + + render(h(PreviewWrapper), container); +}; + // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiCascader' | 'ApiSelect' | 'ApiTreeSelect' | 'AutoComplete' + | 'Cascader' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -143,21 +374,13 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiCascader: withDefaultPlaceholder( - { - ...ApiComponent, - name: 'ApiCascader', - }, - 'select', - { - component: Cascader, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }, - ), + ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', { + component: Cascader, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + visibleEvent: 'onVisibleChange', + }), ApiSelect: withDefaultPlaceholder( { ...ApiComponent, @@ -187,6 +410,7 @@ async function initComponentAdapter() { }, ), AutoComplete, + Cascader, Checkbox, CheckboxGroup, DatePicker, @@ -221,6 +445,7 @@ async function initComponentAdapter() { TimeRangePicker, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, + PreviewUpload: withPreviewUpload(), FileUpload, ImageUpload, }; diff --git a/apps/web-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index d41f39113..15ab9212a 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -84,9 +84,10 @@ setupVbenVxeTable({ // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; - return h(Image, { src: row[column.field] }); + return h(Image, { src: row[column.field], ...props }); }, }); diff --git a/apps/web-antd/src/api/bpm/model/index.ts b/apps/web-antd/src/api/bpm/model/index.ts index 545a73c03..e0c033a44 100644 --- a/apps/web-antd/src/api/bpm/model/index.ts +++ b/apps/web-antd/src/api/bpm/model/index.ts @@ -30,6 +30,7 @@ export namespace BpmModelApi { deploymentTime: number; suspensionState: number; formType?: number; + formCustomCreatePath?: string; formCustomViewPath?: string; formFields?: string[]; } diff --git a/apps/web-antd/src/api/bpm/task/index.ts b/apps/web-antd/src/api/bpm/task/index.ts index 8572b7e6e..b396594f7 100644 --- a/apps/web-antd/src/api/bpm/task/index.ts +++ b/apps/web-antd/src/api/bpm/task/index.ts @@ -7,13 +7,29 @@ import { requestClient } from '#/api/request'; export namespace BpmTaskApi { /** 流程任务 */ export interface Task { - id: number; // 编号 - name: string; // 监听器名字 - type: string; // 监听器类型 - status: number; // 监听器状态 - event: string; // 监听事件 - valueType: string; // 监听器值类型 - processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例 + id: string; // 编号 + name: string; // 任务名字 + status: number; // 任务状态 + createTime: number; // 创建时间 + endTime: number; // 结束时间 + durationInMillis: number; // 持续时间 + reason: string; // 审批理由 + ownerUser: any; // 负责人 + assigneeUser: any; // 处理人 + taskDefinitionKey: string; // 任务定义的标识 + processInstanceId: string; // 流程实例id + processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例 + parentTaskId: any; // 父任务id + children: any; // 子任务 + formId: any; // 表单id + formName: any; // 表单名称 + formConf: any; // 表单配置 + formFields: any; // 表单字段 + formVariables: any; // 表单变量 + buttonsSetting: any; // 按钮设置 + signEnable: any; // 签名设置 + reasonRequire: any; // 原因设置 + nodeType: any; // 节点类型 } } diff --git a/apps/web-antd/src/api/infra/file-config/index.ts b/apps/web-antd/src/api/infra/file-config/index.ts index 1a2fb7707..3b1e82e6c 100644 --- a/apps/web-antd/src/api/infra/file-config/index.ts +++ b/apps/web-antd/src/api/infra/file-config/index.ts @@ -17,6 +17,7 @@ export namespace InfraFileConfigApi { accessSecret?: string; pathStyle?: boolean; enablePublicAccess?: boolean; + region?: string; domain: string; } diff --git a/apps/web-antd/src/api/iot/device/device/index.ts b/apps/web-antd/src/api/iot/device/device/index.ts index c41044756..a0a993028 100644 --- a/apps/web-antd/src/api/iot/device/device/index.ts +++ b/apps/web-antd/src/api/iot/device/device/index.ts @@ -77,14 +77,6 @@ export namespace IotDeviceApi { } } -/** IoT 设备状态枚举 */ -// TODO @haohao:packages/constants/src/biz-iot-enum.ts 枚举; -export enum DeviceStateEnum { - INACTIVE = 0, // 未激活 - OFFLINE = 2, // 离线 - ONLINE = 1, // 在线 -} - /** 查询设备分页 */ export function getDevicePage(params: PageParam) { return requestClient.get>( @@ -154,6 +146,14 @@ export function importDeviceTemplate() { return requestClient.download('/iot/device/get-import-template'); } +/** 导入设备 */ +export function importDevice(file: File, updateSupport: boolean) { + return requestClient.upload('/iot/device/import', { + file, + updateSupport, + }); +} + /** 获取设备属性最新数据 */ export function getLatestDeviceProperties(params: any) { return requestClient.get( diff --git a/apps/web-antd/src/api/iot/product/product/index.ts b/apps/web-antd/src/api/iot/product/product/index.ts index 28b2f7b2e..2833b1e5d 100644 --- a/apps/web-antd/src/api/iot/product/product/index.ts +++ b/apps/web-antd/src/api/iot/product/product/index.ts @@ -27,27 +27,6 @@ export namespace IotProductApi { } } -// TODO @haohao:packages/constants/src/biz-iot-enum.ts 枚举; - -/** IOT 产品设备类型枚举类 */ -export enum DeviceTypeEnum { - DEVICE = 0, // 直连设备 - GATEWAY = 2, // 网关设备 - GATEWAY_SUB = 1, // 网关子设备 -} - -/** IOT 产品定位类型枚举类 */ -export enum LocationTypeEnum { - IP = 1, // IP 定位 - MANUAL = 3, // 手动定位 - MODULE = 2, // 设备定位 -} - -/** IOT 数据格式(编解码器类型)枚举类 */ -export enum CodecTypeEnum { - ALINK = 'Alink', // 阿里云 Alink 协议 -} - /** 查询产品分页 */ export function getProductPage(params: PageParam) { return requestClient.get>( diff --git a/apps/web-antd/src/api/iot/statistics/index.ts b/apps/web-antd/src/api/iot/statistics/index.ts index 9f3a04266..a02b342d2 100644 --- a/apps/web-antd/src/api/iot/statistics/index.ts +++ b/apps/web-antd/src/api/iot/statistics/index.ts @@ -1,21 +1,20 @@ import { requestClient } from '#/api/request'; export namespace IotStatisticsApi { - // TODO @haohao:需要跟后端对齐,必要的 ReqVO、RespVO /** 统计摘要数据 */ - export interface StatisticsSummary { - productCategoryCount: number; - productCount: number; - deviceCount: number; - deviceMessageCount: number; - productCategoryTodayCount: number; - productTodayCount: number; - deviceTodayCount: number; - deviceMessageTodayCount: number; - deviceOnlineCount: number; - deviceOfflineCount: number; - deviceInactiveCount: number; - productCategoryDeviceCounts: Record; + export interface StatisticsSummaryRespVO { + productCategoryCount: number; // 品类数量 + productCount: number; // 产品数量 + deviceCount: number; // 设备数量 + deviceMessageCount: number; // 上报数量 + productCategoryTodayCount: number; // 今日新增品类数量 + productTodayCount: number; // 今日新增产品数量 + deviceTodayCount: number; // 今日新增设备数量 + deviceMessageTodayCount: number; // 今日新增上报数量 + deviceOnlineCount: number; // 在线数量 + deviceOfflineCount: number; // 离线数量 + deviceInactiveCount: number; // 待激活设备数量 + productCategoryDeviceCounts: Record; // 按品类统计的设备数量 } /** 时间戳-数值的键值对类型 */ @@ -30,15 +29,15 @@ export namespace IotStatisticsApi { downstreamCounts: TimeValueItem[]; } - /** 消息统计数据项(按日期) */ - export interface DeviceMessageSummaryByDate { - time: string; - upstreamCount: number; - downstreamCount: number; + /** 设备消息数量统计(按日期) */ + export interface DeviceMessageSummaryByDateRespVO { + time: string; // 时间轴 + upstreamCount: number; // 上行消息数量 + downstreamCount: number; // 下行消息数量 } - /** 消息统计接口参数 */ - export interface DeviceMessageReq { + /** 设备消息统计请求 */ + export interface DeviceMessageReqVO { interval: number; times?: string[]; } @@ -46,26 +45,17 @@ export namespace IotStatisticsApi { /** 获取 IoT 统计摘要数据 */ export function getStatisticsSummary() { - return requestClient.get( + return requestClient.get( '/iot/statistics/get-summary', ); } /** 获取设备消息的数据统计(按日期) */ export function getDeviceMessageSummaryByDate( - params: IotStatisticsApi.DeviceMessageReq, + params: IotStatisticsApi.DeviceMessageReqVO, ) { - return requestClient.get( + return requestClient.get( '/iot/statistics/get-device-message-summary-by-date', { params }, ); } - -// TODO @haohao:貌似这里,没用到?是不是后面哪里用,或者可以删除哈? -/** 获取设备消息统计摘要 */ -export function getDeviceMessageSummary(statType: number) { - return requestClient.get( - '/iot/statistics/get-device-message-summary', - { params: { statType } }, - ); -} diff --git a/apps/web-antd/src/api/mall/promotion/combination/combinationActivity.ts b/apps/web-antd/src/api/mall/promotion/combination/combinationActivity.ts index e497ae204..19af95e95 100644 --- a/apps/web-antd/src/api/mall/promotion/combination/combinationActivity.ts +++ b/apps/web-antd/src/api/mall/promotion/combination/combinationActivity.ts @@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi { limitDuration?: number; // 限制时长 combinationPrice?: number; // 拼团价格 products: CombinationProduct[]; // 商品列表 + picUrl?: any; } /** 拼团活动所需属性 */ diff --git a/apps/web-antd/src/api/mall/promotion/seckill/seckillActivity.ts b/apps/web-antd/src/api/mall/promotion/seckill/seckillActivity.ts index 82297effd..1c18c5e3b 100644 --- a/apps/web-antd/src/api/mall/promotion/seckill/seckillActivity.ts +++ b/apps/web-antd/src/api/mall/promotion/seckill/seckillActivity.ts @@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi { totalStock?: number; // 秒杀总库存 seckillPrice?: number; // 秒杀价格 products?: SeckillProduct[]; // 秒杀商品列表 + picUrl?: any; } } diff --git a/apps/web-antd/src/api/system/mail/template/index.ts b/apps/web-antd/src/api/system/mail/template/index.ts index 9e2a5a78d..57f722cf5 100644 --- a/apps/web-antd/src/api/system/mail/template/index.ts +++ b/apps/web-antd/src/api/system/mail/template/index.ts @@ -14,7 +14,6 @@ export namespace SystemMailTemplateApi { content: string; params: string[]; status: number; - remark: string; createTime: Date; } diff --git a/apps/web-antd/src/api/system/social/client/index.ts b/apps/web-antd/src/api/system/social/client/index.ts index 181cdf86b..55978dd14 100644 --- a/apps/web-antd/src/api/system/social/client/index.ts +++ b/apps/web-antd/src/api/system/social/client/index.ts @@ -12,6 +12,7 @@ export namespace SystemSocialClientApi { clientId: string; clientSecret: string; agentId?: string; + publicKey?: string; status: number; createTime?: Date; } diff --git a/apps/web-antd/src/components/select-modal/index.ts b/apps/web-antd/src/components/select-modal/index.ts deleted file mode 100644 index 2b6e91893..000000000 --- a/apps/web-antd/src/components/select-modal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as DeptSelectModal } from './dept-select-modal.vue'; -export { default as UserSelectModal } from './user-select-modal.vue'; diff --git a/apps/web-antd/src/components/upload/file-upload.vue b/apps/web-antd/src/components/upload/file-upload.vue index b2a800785..3838642ea 100644 --- a/apps/web-antd/src/components/upload/file-upload.vue +++ b/apps/web-antd/src/components/upload/file-upload.vue @@ -51,12 +51,12 @@ const { getStringAccept } = useUploadType({ maxSizeRef: maxSize, }); -// 计算当前绑定的值,优先使用 modelValue +/** 计算当前绑定的值,优先使用 modelValue */ const currentValue = computed(() => { return props.modelValue === undefined ? props.value : props.modelValue; }); -// 判断是否使用 modelValue +/** 判断是否使用 modelValue */ const isUsingModelValue = computed(() => { return props.modelValue !== undefined; }); @@ -82,19 +82,21 @@ watch( } else { value.push(v); } - fileList.value = value.map((item, i) => { - if (item && isString(item)) { - return { - uid: `${-i}`, - name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), - status: UploadResultStatus.DONE, - url: item, - }; - } else if (item && isObject(item)) { - return item; - } - return null; - }) as UploadProps['fileList']; + fileList.value = value + .map((item, i) => { + if (item && isString(item)) { + return { + uid: `${-i}`, + name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), + status: UploadResultStatus.DONE, + url: item, + }; + } else if (item && isObject(item)) { + return item; + } + return null; + }) + .filter(Boolean) as UploadProps['fileList']; } if (!isFirstRender.value) { emit('change', value); @@ -107,6 +109,7 @@ watch( }, ); +/** 处理文件删除 */ async function handleRemove(file: UploadFile) { if (fileList.value) { const index = fileList.value.findIndex((item) => item.uid === file.uid); @@ -120,17 +123,17 @@ async function handleRemove(file: UploadFile) { } } -// 处理文件预览 +/** 处理文件预览 */ function handlePreview(file: UploadFile) { emit('preview', file); } -// 处理文件数量超限 +/** 处理文件数量超限 */ function handleExceed() { message.error($t('ui.upload.maxNumber', [maxNumber.value])); } -// 处理上传错误 +/** 处理上传错误 */ function handleUploadError(error: any) { console.error('上传错误:', error); message.error($t('ui.upload.uploadError')); @@ -138,6 +141,11 @@ function handleUploadError(error: any) { uploadNumber.value = Math.max(0, uploadNumber.value - 1); } +/** + * 上传前校验 + * @param file 待上传的文件 + * @returns 是否允许上传 + */ async function beforeUpload(file: File) { const fileContent = await file.text(); emit('returnText', fileContent); @@ -171,7 +179,8 @@ async function beforeUpload(file: File) { return true; } -async function customRequest(info: UploadRequestOption) { +/** 自定义上传请求 */ +async function customRequest(info: UploadRequestOption) { let { api } = props; if (!api || !isFunction(api)) { api = useUpload(props.directory).httpRequest; @@ -196,7 +205,11 @@ async function customRequest(info: UploadRequestOption) { } } -// 处理上传成功 +/** + * 处理上传成功 + * @param res 上传响应结果 + * @param file 上传的文件 + */ function handleUploadSuccess(res: any, file: File) { // 删除临时文件 const index = fileList.value?.findIndex((item) => item.name === file.name); @@ -228,6 +241,10 @@ function handleUploadSuccess(res: any, file: File) { } } +/** + * 获取当前文件列表的值 + * @returns 文件 URL 列表或字符串 + */ function getValue() { const list = (fileList.value || []) .filter((item) => item?.status === UploadResultStatus.DONE) diff --git a/apps/web-antd/src/components/upload/image-upload.vue b/apps/web-antd/src/components/upload/image-upload.vue index fe962f538..b78d8f15d 100644 --- a/apps/web-antd/src/components/upload/image-upload.vue +++ b/apps/web-antd/src/components/upload/image-upload.vue @@ -55,12 +55,12 @@ const { getStringAccept } = useUploadType({ maxSizeRef: maxSize, }); -// 计算当前绑定的值,优先使用 modelValue +/** 计算当前绑定的值,优先使用 modelValue */ const currentValue = computed(() => { return props.modelValue === undefined ? props.value : props.modelValue; }); -// 判断是否使用 modelValue +/** 判断是否使用 modelValue */ const isUsingModelValue = computed(() => { return props.modelValue !== undefined; }); @@ -89,19 +89,21 @@ watch( } else { value.push(v); } - fileList.value = value.map((item, i) => { - if (item && isString(item)) { - return { - uid: `${-i}`, - name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), - status: UploadResultStatus.DONE, - url: item, - }; - } else if (item && isObject(item)) { - return item; - } - return null; - }) as UploadProps['fileList']; + fileList.value = value + .map((item, i) => { + if (item && isString(item)) { + return { + uid: `${-i}`, + name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), + status: UploadResultStatus.DONE, + url: item, + }; + } else if (item && isObject(item)) { + return item; + } + return null; + }) + .filter(Boolean) as UploadProps['fileList']; } if (!isFirstRender.value) { emit('change', value); @@ -114,6 +116,7 @@ watch( }, ); +/** 将文件转换为 Base64 格式 */ function getBase64(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -125,6 +128,7 @@ function getBase64(file: File) { }); } +/** 处理图片预览 */ async function handlePreview(file: UploadFile) { if (!file.url && !file.preview) { file.preview = await getBase64(file.originFileObj!); @@ -138,6 +142,7 @@ async function handlePreview(file: UploadFile) { ); } +/** 处理文件删除 */ async function handleRemove(file: UploadFile) { if (fileList.value) { const index = fileList.value.findIndex((item) => item.uid === file.uid); @@ -151,11 +156,17 @@ async function handleRemove(file: UploadFile) { } } +/** 关闭预览弹窗 */ function handleCancel() { previewOpen.value = false; previewTitle.value = ''; } +/** + * 上传前校验 + * @param file 待上传的文件 + * @returns 是否允许上传 + */ async function beforeUpload(file: File) { // 检查文件数量限制 if (fileList.value!.length >= props.maxNumber) { @@ -186,7 +197,8 @@ async function beforeUpload(file: File) { return true; } -async function customRequest(info: UploadRequestOption) { +/** 自定义上传请求 */ +async function customRequest(info: UploadRequestOption) { let { api } = props; if (!api || !isFunction(api)) { api = useUpload(props.directory).httpRequest; @@ -211,7 +223,11 @@ async function customRequest(info: UploadRequestOption) { } } -// 处理上传成功 +/** + * 处理上传成功 + * @param res 上传响应结果 + * @param file 上传的文件 + */ function handleUploadSuccess(res: any, file: File) { // 删除临时文件 const index = fileList.value?.findIndex((item) => item.name === file.name); @@ -243,14 +259,18 @@ function handleUploadSuccess(res: any, file: File) { } } -// 处理上传错误 +/** 处理上传错误 */ function handleUploadError(error: any) { console.error('上传错误:', error); - message.error('上传错误!!!'); + message.error($t('ui.upload.uploadError')); // 上传失败时减少计数器 uploadNumber.value = Math.max(0, uploadNumber.value - 1); } +/** + * 获取当前文件列表的值 + * @returns 文件 URL 列表或字符串 + */ function getValue() { const list = (fileList.value || []) .filter((item) => item?.status === UploadResultStatus.DONE) diff --git a/apps/web-antd/src/components/upload/input-upload.vue b/apps/web-antd/src/components/upload/input-upload.vue index 66495467a..90b2f4f7c 100644 --- a/apps/web-antd/src/components/upload/input-upload.vue +++ b/apps/web-antd/src/components/upload/input-upload.vue @@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing'; import { computed } from 'vue'; import { useVModel } from '@vueuse/core'; -import { Col, Input, Row, Textarea } from 'ant-design-vue'; +import { Input, Textarea } from 'ant-design-vue'; import FileUpload from './file-upload.vue'; @@ -30,6 +30,7 @@ const modelValue = useVModel(props, 'modelValue', emits, { passive: true, }); +/** 处理文件内容返回 */ function handleReturnText(text: string) { modelValue.value = text; emits('change', modelValue.value); @@ -37,6 +38,7 @@ function handleReturnText(text: string) { emits('update:modelValue', modelValue.value); } +/** 计算输入框属性 */ const inputProps = computed(() => { return { ...props.inputProps, @@ -44,6 +46,7 @@ const inputProps = computed(() => { }; }); +/** 计算文本域属性 */ const textareaProps = computed(() => { return { ...props.textareaProps, @@ -51,6 +54,7 @@ const textareaProps = computed(() => { }; }); +/** 计算文件上传属性 */ const fileUploadProps = computed(() => { return { ...props.fileUploadProps, @@ -58,17 +62,17 @@ const fileUploadProps = computed(() => { });