diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts new file mode 100644 index 00000000..880cc5ea --- /dev/null +++ b/apps/backend-mock/api/demo/bigint.ts @@ -0,0 +1,28 @@ +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const data = ` + { + "code": 0, + "message": "success", + "data": [ + { + "id": 123456789012345678901234567890123456789012345678901234567890, + "name": "John Doe", + "age": 30, + "email": "john-doe@demo.com" + }, + { + "id": 987654321098765432109876543210987654321098765432109876543210, + "name": "Jane Smith", + "age": 25, + "email": "jane@demo.com" + } + ] + } + `; + setHeader(event, 'Content-Type', 'application/json'); + return data; +}); diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index fdb068a5..b779d03a 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -26,6 +26,7 @@ "#/*": "./src/*" }, "dependencies": { + "@ant-design/icons-vue": "catalog:", "@form-create/ant-design-vue": "catalog:", "@form-create/antd-designer": "catalog:", "@tinymce/tinymce-vue": "catalog:", @@ -53,7 +54,8 @@ "pinia": "catalog:", "vue": "catalog:", "vue-dompurify-html": "catalog:", - "vue-router": "catalog:" + "vue-router": "catalog:", + "vue3-signature": "catalog:" }, "devDependencies": { "@types/crypto-js": "catalog:" diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index d0932a0d..d9edaf19 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -8,63 +8,64 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -/** 手机号正则表达式(中国) */ const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; -setupVbenForm({ - config: { - // ant design vue组件库默认都是 v-model:value - baseModelPropName: 'value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', - // 一些组件是 v-model:checked 或者 v-model:fileList - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - RichTextarea: 'modelValue', - Switch: 'checked', - Upload: 'fileList', + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - // 输入项目必填国际化适配 - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; - }, - // 选择项目必填国际化适配 - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - // 手机号非必填 - mobile: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } return true; - } else if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + // 手机号非必填 + mobile: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return true; + } else if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, + // 手机号必填 + mobileRequired: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + if (!MOBILE_REGEX.test(value)) { + return $t('ui.formRules.mobile', [ctx.label]); + } + return true; + }, }, - // 手机号必填 - mobileRequired: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - if (!MOBILE_REGEX.test(value)) { - return $t('ui.formRules.mobile', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; +export type FormSchemaGetter = () => VbenFormSchema[]; diff --git a/apps/web-antd/src/api/bpm/definition/index.ts b/apps/web-antd/src/api/bpm/definition/index.ts index 562b0009..795caa1b 100644 --- a/apps/web-antd/src/api/bpm/definition/index.ts +++ b/apps/web-antd/src/api/bpm/definition/index.ts @@ -4,6 +4,7 @@ import { requestClient } from '#/api/request'; /** 流程定义 */ export namespace BpmProcessDefinitionApi { + // 流程定义 export interface ProcessDefinitionVO { id: string; version: number; @@ -36,11 +37,12 @@ export async function getProcessDefinitionPage(params: PageParam) { /** 查询流程定义列表 */ export async function getProcessDefinitionList(params: any) { - return requestClient.get< - PageResult - >('/bpm/process-definition/list', { - params, - }); + return requestClient.get( + '/bpm/process-definition/list', + { + params, + }, + ); } /** 查询流程定义列表(简单列表) */ diff --git a/apps/web-antd/src/api/bpm/form/index.ts b/apps/web-antd/src/api/bpm/form/index.ts index 581178c5..78ce4d6d 100644 --- a/apps/web-antd/src/api/bpm/form/index.ts +++ b/apps/web-antd/src/api/bpm/form/index.ts @@ -3,7 +3,7 @@ import type { PageParam, PageResult } from '@vben/request'; import { requestClient } from '#/api/request'; export namespace BpmFormApi { - // TODO @siye:注释加一个。。嘿嘿 + // 流程表单 export interface FormVO { id?: number | undefined; name: string; @@ -11,7 +11,7 @@ export namespace BpmFormApi { fields: string[]; status: number; remark: string; - createTime: string; + createTime: number; } } @@ -23,7 +23,7 @@ export async function getFormPage(params: PageParam) { } /** 获取表单详情 */ -export async function getFormDetail(id: number) { +export async function getFormDetail(id: number | string) { return requestClient.get(`/bpm/form/get?id=${id}`); } diff --git a/apps/web-antd/src/api/bpm/model/index.ts b/apps/web-antd/src/api/bpm/model/index.ts index 713fedc4..45598178 100644 --- a/apps/web-antd/src/api/bpm/model/index.ts +++ b/apps/web-antd/src/api/bpm/model/index.ts @@ -14,6 +14,7 @@ export namespace BpmModelApi { /** 流程定义 VO */ export interface ProcessDefinitionVO { id: string; + key?: string; version: number; deploymentTime: number; suspensionState: number; diff --git a/apps/web-antd/src/api/bpm/processInstance/index.ts b/apps/web-antd/src/api/bpm/processInstance/index.ts index 23afa8c3..4f9b7795 100644 --- a/apps/web-antd/src/api/bpm/processInstance/index.ts +++ b/apps/web-antd/src/api/bpm/processInstance/index.ts @@ -1,5 +1,7 @@ import type { PageParam, PageResult } from '@vben/request'; +import type { BpmTaskApi } from '../task'; + import type { BpmModelApi } from '#/api/bpm/model'; import type { BpmCandidateStrategyEnum, BpmNodeTypeEnum } from '#/utils'; @@ -40,6 +42,7 @@ export namespace BpmProcessInstanceApi { tasks: ApprovalTaskInfo[]; }; + // 流程实例 export type ProcessInstanceVO = { businessKey: string; category: string; @@ -59,12 +62,33 @@ export namespace BpmProcessInstanceApi { tasks?: BpmProcessInstanceApi.Task[]; }; + // 审批详情 export type ApprovalDetail = { activityNodes: BpmProcessInstanceApi.ApprovalNodeInfo[]; formFieldsPermission: any; processDefinition: BpmModelApi.ProcessDefinitionVO; processInstance: BpmProcessInstanceApi.ProcessInstanceVO; status: number; + todoTask: BpmTaskApi.TaskVO; + }; + + // 抄送流程实例 VO + export type CopyVO = { + activityId: string; + activityName: string; + createTime: number; + createUser: User; + id: number; + processInstanceId: string; + processInstanceName: string; + processInstanceStartTime: number; + reason: string; + startUser: User; + summary: { + key: string; + value: string; + }[]; + taskId: string; }; } @@ -85,9 +109,7 @@ export async function getProcessInstanceManagerPage(params: PageParam) { } /** 新增流程实例 */ -export async function createProcessInstance( - data: BpmProcessInstanceApi.ProcessInstanceVO, -) { +export async function createProcessInstance(data: any) { return requestClient.post( '/bpm/process-instance/create', data, @@ -152,7 +174,7 @@ export async function getApprovalDetail(params: any) { /** 获取下一个执行的流程节点 */ export async function getNextApprovalNodes(params: any) { - return requestClient.get( + return requestClient.get( `/bpm/process-instance/get-next-approval-nodes`, { params }, ); diff --git a/apps/web-antd/src/api/bpm/task/index.ts b/apps/web-antd/src/api/bpm/task/index.ts index 4054b8f6..19cbede8 100644 --- a/apps/web-antd/src/api/bpm/task/index.ts +++ b/apps/web-antd/src/api/bpm/task/index.ts @@ -1,5 +1,7 @@ import type { PageParam, PageResult } from '@vben/request'; +import type { BpmProcessInstanceApi } from '../processInstance'; + import { requestClient } from '#/api/request'; export namespace BpmTaskApi { @@ -11,7 +13,33 @@ export namespace BpmTaskApi { status: number; // 监听器状态 event: string; // 监听事件 valueType: string; // 监听器值类型 - value: string; // 监听器值 + } + + // 流程任务 VO + export interface TaskManagerVO { + id: string; // 编号 + name: string; // 任务名称 + createTime: number; // 创建时间 + endTime: number; // 结束时间 + durationInMillis: number; // 持续时间 + status: number; // 状态 + reason: string; // 原因 + ownerUser: any; // 负责人 + assigneeUser: any; // 处理人 + taskDefinitionKey: string; // 任务定义key + processInstanceId: string; // 流程实例id + processInstance: BpmProcessInstanceApi.ProcessInstanceVO; // 流程实例 + parentTaskId: any; // 父任务id + children: any; // 子任务 + formId: any; // 表单id + formName: any; // 表单名称 + formConf: any; // 表单配置 + formFields: any; // 表单字段 + formVariables: any; // 表单变量 + buttonsSetting: any; // 按钮设置 + signEnable: any; // 签名设置 + reasonRequire: any; // 原因设置 + nodeType: any; // 节点类型 } } @@ -54,13 +82,15 @@ export const rejectTask = async (data: any) => { }; /** 根据流程实例 ID 查询任务列表 */ -export const getTaskListByProcessInstanceId = async (data: any) => { - return await requestClient.get('/bpm/task/list-by-process-instance-id', data); +export const getTaskListByProcessInstanceId = async (id: string) => { + return await requestClient.get( + `/bpm/task/list-by-process-instance-id?processInstanceId=${id}`, + ); }; /** 获取所有可退回的节点 */ -export const getTaskListByReturn = async (data: any) => { - return await requestClient.get('/bpm/task/list-by-return', data); +export const getTaskListByReturn = async (id: string) => { + return await requestClient.get(`/bpm/task/list-by-return?id=${id}`); }; /** 退回 */ diff --git a/apps/web-antd/src/api/pay/channel/index.ts b/apps/web-antd/src/api/pay/channel/index.ts index 99a16c05..eb4c1d99 100644 --- a/apps/web-antd/src/api/pay/channel/index.ts +++ b/apps/web-antd/src/api/pay/channel/index.ts @@ -27,7 +27,7 @@ export function getChannelPage(params: PageParam) { } /** 查询支付渠道详情 */ -export function getChannel(appId: string, code: string) { +export function getChannel(appId: number, code: string) { return requestClient.get('/pay/channel/get', { params: { appId, code }, }); diff --git a/apps/web-antd/src/api/pay/order/index.ts b/apps/web-antd/src/api/pay/order/index.ts index 984a5d1f..31ba880d 100644 --- a/apps/web-antd/src/api/pay/order/index.ts +++ b/apps/web-antd/src/api/pay/order/index.ts @@ -6,8 +6,13 @@ export namespace PayOrderApi { /** 支付订单信息 */ export interface Order { id: number; + no: string; + price: number; + channelFeePrice: number; + refundPrice: number; merchantId: number; appId: number; + appName: string; channelId: number; channelCode: string; merchantOrderId: string; @@ -29,7 +34,9 @@ export namespace PayOrderApi { refundAmount: number; channelUserId: string; channelOrderNo: string; + channelNotifyData: string; createTime: Date; + updateTime: Date; } /** 支付订单分页请求 */ diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 68d19ee1..0f1ab09f 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -14,6 +14,7 @@ import { $t, setupI18n } from '#/locales'; import { setupFormCreate } from '#/plugins/form-create'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; @@ -21,6 +22,9 @@ async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, diff --git a/apps/web-antd/src/components/upload/file-upload.vue b/apps/web-antd/src/components/upload/file-upload.vue index 3fbb727b..5423ab9a 100644 --- a/apps/web-antd/src/components/upload/file-upload.vue +++ b/apps/web-antd/src/components/upload/file-upload.vue @@ -58,7 +58,7 @@ const props = withDefaults( showDescription: false, }, ); -const emit = defineEmits(['change', 'update:value', 'delete']); +const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']); const { accept, helpText, maxNumber, maxSize } = toRefs(props); const isInnerOperate = ref(false); const { getStringAccept } = useUploadType({ @@ -125,6 +125,10 @@ const handleRemove = async (file: UploadFile) => { }; const beforeUpload = async (file: File) => { + // 使用现代的Blob.text()方法替代FileReader + const fileContent = await file.text(); + emit('returnText', fileContent); + const { maxSize, accept } = props; const isAct = checkFileType(file, accept); if (!isAct) { diff --git a/apps/web-antd/src/components/user-select-modal/user-select-modal.vue b/apps/web-antd/src/components/user-select-modal/user-select-modal.vue index 105267b7..723e612c 100644 --- a/apps/web-antd/src/components/user-select-modal/user-select-modal.vue +++ b/apps/web-antd/src/components/user-select-modal/user-select-modal.vue @@ -30,6 +30,7 @@ interface DeptTreeNode { key: string; title: string; children?: DeptTreeNode[]; + name: string; } defineOptions({ name: 'UserSelectModal' }); @@ -107,22 +108,26 @@ const transferDataSource = computed(() => { const filteredDeptTree = computed(() => { if (!deptSearchKeys.value) return deptTree.value; - const filterNode = (node: any): any => { - const title = node?.title?.toLowerCase(); + const filterNode = (node: any, depth = 0): any => { + // 添加深度限制,防止过深的递归导致爆栈 + if (depth > 100) return null; + + // 按部门名称搜索 + const name = node?.name?.toLowerCase(); const search = deptSearchKeys.value.toLowerCase(); - // 如果当前节点匹配 - if (title.includes(search)) { + // 如果当前节点匹配,直接返回节点,不处理子节点 + if (name?.includes(search)) { return { ...node, - children: node.children?.map((child: any) => filterNode(child)), + children: node.children, }; } // 如果当前节点不匹配,检查子节点 if (node.children) { const filteredChildren = node.children - .map((child: any) => filterNode(child)) + .map((child: any) => filterNode(child, depth + 1)) .filter(Boolean); if (filteredChildren.length > 0) { @@ -397,6 +402,7 @@ const processDeptNode = (node: any): DeptTreeNode => { return { key: String(node.id), title: `${node.name} (${node.id})`, + name: node.name, children: node.children?.map((child: any) => processDeptNode(child)), }; }; diff --git a/apps/web-antd/src/router/routes/modules/bpm.ts b/apps/web-antd/src/router/routes/modules/bpm.ts index 215ab26a..8773271b 100644 --- a/apps/web-antd/src/router/routes/modules/bpm.ts +++ b/apps/web-antd/src/router/routes/modules/bpm.ts @@ -49,7 +49,7 @@ const routes: RouteRecordRaw[] = [ { path: '/bpm/manager/form/edit', name: 'BpmFormEditor', - component: () => import('#/views/bpm/form/editor.vue'), + component: () => import('#/views/bpm/form/designer/index.vue'), meta: { title: '编辑流程表单', activePath: '/bpm/manager/form', diff --git a/apps/web-antd/src/utils/constants.ts b/apps/web-antd/src/utils/constants.ts index affb6b1b..ed4f97a8 100644 --- a/apps/web-antd/src/utils/constants.ts +++ b/apps/web-antd/src/utils/constants.ts @@ -441,30 +441,6 @@ export const ErpBizType = { // ========== BPM 模块 ========== -export const BpmModelType = { - BPMN: 10, // BPMN 设计器 - SIMPLE: 20, // 简易设计器 -}; - -export const BpmModelFormType = { - NORMAL: 10, // 流程表单 - CUSTOM: 20, // 业务表单 -}; - -export const BpmProcessInstanceStatus = { - NOT_START: -1, // 未开始 - RUNNING: 1, // 审批中 - APPROVE: 2, // 审批通过 - REJECT: 3, // 审批不通过 - CANCEL: 4, // 已取消 -}; - -export const BpmAutoApproveType = { - NONE: 0, // 不自动通过 - APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过 - APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过 -}; - // 候选人策略枚举 ( 用于审批节点。抄送节点 ) export enum BpmCandidateStrategyEnum { /** @@ -594,6 +570,40 @@ export enum BpmNodeTypeEnum { USER_TASK_NODE = 11, } +/** + * 流程任务操作按钮 + */ +export enum BpmTaskOperationButtonTypeEnum { + /** + * 加签 + */ + ADD_SIGN = 5, + /** + * 通过 + */ + APPROVE = 1, + /** + * 抄送 + */ + COPY = 7, + /** + * 委派 + */ + DELEGATE = 4, + /** + * 拒绝 + */ + REJECT = 2, + /** + * 退回 + */ + RETURN = 6, + /** + * 转办 + */ + TRANSFER = 3, +} + /** * 任务状态枚举 */ @@ -667,3 +677,51 @@ export enum BpmFieldPermissionType { */ WRITE = '2', } + +/** + * 流程模型类型 + */ +export const BpmModelType = { + BPMN: 10, // BPMN 设计器 + SIMPLE: 20, // 简易设计器 +}; + +/** + * 流程模型表单类型 + */ +export const BpmModelFormType = { + NORMAL: 10, // 流程表单 + CUSTOM: 20, // 业务表单 +}; + +/** + * 流程实例状态 + */ +export const BpmProcessInstanceStatus = { + NOT_START: -1, // 未开始 + RUNNING: 1, // 审批中 + APPROVE: 2, // 审批通过 + REJECT: 3, // 审批不通过 + CANCEL: 4, // 已取消 +}; + +/** + * 自动审批类型 + */ +export const BpmAutoApproveType = { + NONE: 0, // 不自动通过 + APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过 + APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过 +}; + +/** + * 审批操作按钮名称 + */ +export const OPERATION_BUTTON_NAME = new Map(); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.APPROVE, '通过'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.REJECT, '拒绝'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.TRANSFER, '转办'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.DELEGATE, '委派'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.ADD_SIGN, '加签'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.RETURN, '退回'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.COPY, '抄送'); diff --git a/apps/web-antd/src/utils/download.ts b/apps/web-antd/src/utils/download.ts new file mode 100644 index 00000000..acb0129c --- /dev/null +++ b/apps/web-antd/src/utils/download.ts @@ -0,0 +1,214 @@ +/** + * 下载工具模块 + * 提供多种文件格式的下载功能 + */ + +/** + * 图片下载配置接口 + */ +interface ImageDownloadOptions { + /** 图片 URL */ + url: string; + /** 指定画布宽度 */ + canvasWidth?: number; + /** 指定画布高度 */ + canvasHeight?: number; + /** 将图片绘制在画布上时带上图片的宽高值,默认为 true */ + drawWithImageSize?: boolean; +} + +/** + * 基础文件下载函数 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + * @param mimeType - MIME 类型 + */ +export const download0 = (data: Blob, fileName: string, mimeType: string) => { + try { + // 创建 blob + const blob = new Blob([data], { type: mimeType }); + // 创建 href 超链接,点击进行下载 + window.URL = window.URL || window.webkitURL; + const href = URL.createObjectURL(blob); + const downA = document.createElement('a'); + downA.href = href; + downA.download = fileName; + downA.click(); + // 销毁超链接 + window.URL.revokeObjectURL(href); + } catch (error) { + console.error('文件下载失败:', error); + throw new Error( + `文件下载失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } +}; + +/** + * 触发文件下载的通用方法 + * @param url - 下载链接 + * @param fileName - 文件名 + */ +const triggerDownload = (url: string, fileName: string) => { + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); +}; + +export const download = { + /** + * 下载 Excel 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + excel: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/vnd.ms-excel'); + }, + + /** + * 下载 Word 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + word: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/msword'); + }, + + /** + * 下载 Zip 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + zip: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/zip'); + }, + + /** + * 下载 HTML 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + html: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/html'); + }, + + /** + * 下载 Markdown 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + markdown: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/markdown'); + }, + + /** + * 下载 JSON 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + json: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/json'); + }, + + /** + * 下载图片(允许跨域) + * @param options - 图片下载配置 + */ + image: (options: ImageDownloadOptions) => { + const { + url, + canvasWidth, + canvasHeight, + drawWithImageSize = true, + } = options; + + const image = new Image(); + // image.setAttribute('crossOrigin', 'anonymous') + image.src = url; + image.addEventListener('load', () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth || image.width; + canvas.height = canvasHeight || image.height; + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + ctx?.clearRect(0, 0, canvas.width, canvas.height); + + if (drawWithImageSize) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } else { + ctx.drawImage(image, 0, 0); + } + + const dataUrl = canvas.toDataURL('image/png'); + triggerDownload(dataUrl, 'image.png'); + } catch (error) { + console.error('图片下载失败:', error); + throw new Error( + `图片下载失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } + }); + + image.addEventListener('error', () => { + throw new Error('图片加载失败'); + }); + }, + + /** + * 将 Base64 字符串转换为文件对象 + * @param base64 - Base64 字符串 + * @param fileName - 文件名 + * @returns File 对象 + */ + base64ToFile: (base64: string, fileName: string): File => { + // 输入验证 + if (!base64 || typeof base64 !== 'string') { + throw new Error('base64 参数必须是非空字符串'); + } + + // 将 base64 按照逗号进行分割,将前缀与后续内容分隔开 + const data = base64.split(','); + if (data.length !== 2 || !data[0] || !data[1]) { + throw new Error('无效的 base64 格式'); + } + + // 利用正则表达式从前缀中获取类型信息(image/png、image/jpeg、image/webp等) + const typeMatch = data[0].match(/:(.*?);/); + if (!typeMatch || !typeMatch[1]) { + throw new Error('无法解析 base64 类型信息'); + } + const type = typeMatch[1]; + + // 从类型信息中获取具体的文件格式后缀(png、jpeg、webp) + const typeParts = type.split('/'); + if (typeParts.length !== 2 || !typeParts[1]) { + throw new Error('无效的 MIME 类型格式'); + } + const suffix = typeParts[1]; + + try { + // 使用 atob() 对 base64 数据进行解码,结果是一个文件数据流以字符串的格式输出 + const bstr = window.atob(data[1]); + + // 获取解码结果字符串的长度 + const n = bstr.length; + // 根据解码结果字符串的长度创建一个等长的整型数字数组 + const u8arr = new Uint8Array(n); + + // 优化的 Uint8Array 填充逻辑 + for (let i = 0; i < n; i++) { + // 使用 charCodeAt() 获取字符对应的字节值(Base64 解码后的字符串是字节级别的) + // eslint-disable-next-line unicorn/prefer-code-point + u8arr[i] = bstr.charCodeAt(i); + } + + // 返回 File 文件对象 + return new File([u8arr], `${fileName}.${suffix}`, { type }); + } catch (error) { + throw new Error( + `Base64 解码失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } + }, +}; diff --git a/apps/web-antd/src/utils/formCreate.ts b/apps/web-antd/src/utils/formCreate.ts index 1129f63f..60264934 100644 --- a/apps/web-antd/src/utils/formCreate.ts +++ b/apps/web-antd/src/utils/formCreate.ts @@ -34,7 +34,7 @@ export const decodeFields = (fields: string[]) => { export const setConfAndFields = ( designerRef: object, conf: string, - fields: string, + fields: string | string[], ) => { // @ts-ignore designerRef.value is dynamically added by form-create-designer designerRef.value.setOption(JSON.parse(conf)); diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts index 022e6441..101e381e 100644 --- a/apps/web-antd/src/utils/index.ts +++ b/apps/web-antd/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './constants'; export * from './dict'; +export * from './download'; export * from './formatTime'; export * from './formCreate'; export * from './rangePickerProps'; diff --git a/apps/web-antd/src/views/bpm/category/index.vue b/apps/web-antd/src/views/bpm/category/index.vue index da7d694e..8dd26b7c 100644 --- a/apps/web-antd/src/views/bpm/category/index.vue +++ b/apps/web-antd/src/views/bpm/category/index.vue @@ -6,11 +6,13 @@ import type { import type { BpmCategoryApi } from '#/api/bpm/category'; import { Page, useVbenModal } from '@vben/common-ui'; +import { Plus } from '@vben/icons'; import { Button, message } from 'ant-design-vue'; import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { deleteCategory, getCategoryPage } from '#/api/bpm/category'; +import { DocAlert } from '#/components/doc-alert'; import { $t } from '#/locales'; import { useGridColumns, useGridFormSchema } from './data'; @@ -100,6 +102,10 @@ async function onDelete(row: BpmCategoryApi.CategoryVO) {