From dd0809f002266e9355b3cfe5c71d56df236d2eaa Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 12 Jul 2026 10:24:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(bpm):=20=E5=AE=8C=E5=96=84=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=A8=A1=E5=9E=8B=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 web-antd 模型导入弹窗 - 同步 web-ele 和 web-antdv-next - 修复上传文件状态残留问题 - 统一导出下载逻辑与权限控制 --- apps/web-antd/src/api/bpm/model/index.ts | 12 +- apps/web-antd/src/views/bpm/model/index.vue | 151 +++--------------- .../modules/category-draggable-model.vue | 25 +-- .../views/bpm/model/modules/import-form.vue | 111 +++++++++++++ .../web-antdv-next/src/api/bpm/model/index.ts | 26 +++ .../src/views/bpm/model/index.vue | 22 +++ .../modules/category-draggable-model.vue | 32 +++- .../views/bpm/model/modules/import-form.vue | 111 +++++++++++++ apps/web-ele/src/api/bpm/model/index.ts | 26 +++ apps/web-ele/src/views/bpm/model/index.vue | 22 +++ .../modules/category-draggable-model.vue | 35 +++- .../views/bpm/model/modules/import-form.vue | 122 ++++++++++++++ 12 files changed, 549 insertions(+), 146 deletions(-) create mode 100644 apps/web-antd/src/views/bpm/model/modules/import-form.vue create mode 100644 apps/web-antdv-next/src/views/bpm/model/modules/import-form.vue create mode 100644 apps/web-ele/src/views/bpm/model/modules/import-form.vue diff --git a/apps/web-antd/src/api/bpm/model/index.ts b/apps/web-antd/src/api/bpm/model/index.ts index 9cce9be8e..2803e0e5b 100644 --- a/apps/web-antd/src/api/bpm/model/index.ts +++ b/apps/web-antd/src/api/bpm/model/index.ts @@ -22,6 +22,14 @@ export namespace BpmModelApi { startUsers?: UserInfo[]; } + /** 流程模型导出数据 */ + export interface ModelExport extends Partial { + key: string; + name: string; + simpleModel?: Record; + type: number; + } + /** 流程定义 */ export interface ProcessDefinition { id: string; @@ -109,7 +117,9 @@ export async function importModel(file: File, key?: string, name?: string) { /** 导出流程模型 */ export async function exportModel(id: number) { - return requestClient.get(`/bpm/model/export?id=${id}`); + return requestClient.get( + `/bpm/model/export?id=${id}`, + ); } /** 删除流程模型 */ diff --git a/apps/web-antd/src/views/bpm/model/index.vue b/apps/web-antd/src/views/bpm/model/index.vue index 6bea668d9..c35d844ce 100644 --- a/apps/web-antd/src/views/bpm/model/index.vue +++ b/apps/web-antd/src/views/bpm/model/index.vue @@ -3,38 +3,38 @@ import type { ModelCategoryInfo } from '#/api/bpm/model'; import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue'; +import { useAccess } from '@vben/access'; import { Page, useVbenModal } from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; import { cloneDeep } from '@vben/utils'; import { useSortable } from '@vueuse/integrations/useSortable'; -import { - Alert, - Button, - Card, - Dropdown, - Form, - Input, - Menu, - message, - Upload, -} from 'ant-design-vue'; +import { Button, Card, Dropdown, Input, Menu, message } from 'ant-design-vue'; import { getCategorySimpleList, updateCategorySortBatch, } from '#/api/bpm/category'; -import { getModelList, importModel as importModelApi } from '#/api/bpm/model'; +import { getModelList } from '#/api/bpm/model'; import { router } from '#/router'; import CategoryForm from '../category/modules/form.vue'; import CategoryDraggableModel from './modules/category-draggable-model.vue'; +import ModelImportForm from './modules/import-form.vue'; const [CategoryFormModal, categoryFormModalApi] = useVbenModal({ connectedComponent: CategoryForm, destroyOnClose: true, }); +const [ImportModal, importModalApi] = useVbenModal({ + connectedComponent: ModelImportForm, + destroyOnClose: true, +}); + +const { hasAccessByCodes } = useAccess(); +const hasImportPermission = hasAccessByCodes(['bpm:model:import']); + const modelListSpinning = ref(false); // 模型列表加载状态 const saveSortLoading = ref(false); // 保存排序状态 @@ -45,65 +45,6 @@ const sortable = useTemplateRef('categoryGroupRef'); // 可以排 const sortableInstance = ref(null); // 排序引用,以便后续启用或禁用排序 const isCategorySorting = ref(false); // 分类排序状态 -// ========== 导入弹窗相关 ========== -const importFile = ref(null); // 导入文件(用于最终提交) -const importFileList = ref([]); // 上传组件的文件列表 -const importFormRef = ref(); -const importForm = reactive({ - key: '', - name: '', -}); - -const [ImportModal, importModalApi] = useVbenModal({ - async onConfirm() { - if (!importFile.value) { - message.warning('请上传流程模型文件'); - return; - } - await importFormRef.value?.validate(); - importModalApi.lock(); - try { - await importModelApi(importFile.value, importForm.key, importForm.name); - message.success('导入成功'); - await importModalApi.close(); - await getList(); - } catch { - // 全局 request 拦截器已处理错误提示 - } finally { - importModalApi.unlock(); - } - }, - async onOpenChange(isOpen: boolean) { - if (!isOpen) { - importFile.value = null; - importFileList.value = []; - importForm.key = ''; - importForm.name = ''; - } - }, -}); - -/** 上传前校验 + 读取文件自动填充字段 */ -function beforeUpload(file: File) { - const isJson = - file.type === 'application/json' || file.name.endsWith('.json'); - if (!isJson) { - message.error('仅支持上传 JSON 格式的流程模型文件'); - return Upload.LIST_IGNORE; - } - importFile.value = file; - file.text().then((text) => { - try { - const json = JSON.parse(text); - importForm.key = json.key || ''; - importForm.name = json.name || ''; - } catch { - message.error('JSON 文件格式不正确'); - } - }); - return false; // 阻止默认上传,不显示上传动画 -} - /** 点击导入按钮 */ function handleImportClick() { importModalApi.open(); @@ -217,6 +158,7 @@ async function handleCategorySortSubmit() { + 新建模型 - @@ -291,66 +237,5 @@ async function handleCategorySortSubmit() { /> - - - -
- -
- - - -

- -

-

点击或拖拽文件到此处上传

-

- 仅支持上传单个 JSON 格式的流程模型文件 -

-
-
- - - - -
- 同租户导入时,若标识已存在请修改后再导入 -
-
- - - - -
必填,请填写流程名称
-
-
-
-
diff --git a/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue index 8f0926d26..700375465 100644 --- a/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue +++ b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue @@ -10,7 +10,12 @@ import { confirm, EllipsisText, useVbenModal } from '@vben/common-ui'; import { BpmModelFormType } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; import { useUserStore } from '@vben/stores'; -import { cloneDeep, formatDateTime, isEqual } from '@vben/utils'; +import { + cloneDeep, + downloadFileFromBlobPart, + formatDateTime, + isEqual, +} from '@vben/utils'; import { useDebounceFn } from '@vueuse/core'; import { useSortable } from '@vueuse/integrations/useSortable'; @@ -83,6 +88,9 @@ const hasPermiDelete = computed(() => { const hasPermiDeploy = computed(() => { return hasAccessByCodes(['bpm:model:deploy']); }); +const hasPermiExport = computed(() => { + return hasAccessByCodes(['bpm:model:export']); +}); const [Grid, gridApi] = useVbenVxeGrid({ gridOptions: { @@ -335,15 +343,10 @@ async function handleExportModel(row: any) { const hideLoading = message.loading({ content: '正在导出...', duration: 0 }); try { const data = await exportModel(row.id); - const blob = new Blob([JSON.stringify(data, null, 2)], { - type: 'application/json', + downloadFileFromBlobPart({ + fileName: `${row.key || row.name || 'model'}.json`, + source: JSON.stringify(data, null, 2), }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${row.key || row.name || 'model'}.json`; - a.click(); - URL.revokeObjectURL(url); message.success('导出成功'); } finally { hideLoading(); @@ -716,7 +719,9 @@ function handleRenameSuccess() { @click="(e) => handleModelCommand(e.key as string, row)" > 复制 - 导出 + + 导出 + 历史 +import { reactive, ref } from 'vue'; + +import { useVbenModal } from '@vben/common-ui'; +import { IconifyIcon } from '@vben/icons'; + +import { Alert, Form, Input, message, Upload } from 'ant-design-vue'; + +import { importModel } from '#/api/bpm/model'; + +const emit = defineEmits(['success']); + +const file = ref(); +const fileList = ref([]); +const formRef = ref(); +const formData = reactive({ key: '', name: '' }); + +const [Modal, modalApi] = useVbenModal({ + async onConfirm() { + if (!file.value) { + message.warning('请上传流程模型文件'); + return; + } + await formRef.value?.validate(); + modalApi.lock(); + try { + await importModel(file.value, formData.key, formData.name); + await modalApi.close(); + emit('success'); + message.success('导入成功'); + } finally { + modalApi.unlock(); + } + }, + onOpenChange(isOpen) { + if (!isOpen) resetForm(); + }, +}); + +async function beforeUpload(uploadFile: File) { + if (!uploadFile.name.toLowerCase().endsWith('.json')) { + message.error('仅支持上传 JSON 格式的流程模型文件'); + return Upload.LIST_IGNORE; + } + try { + const data = JSON.parse(await uploadFile.text()); + file.value = uploadFile; + formData.key = data.key || ''; + formData.name = data.name || ''; + return false; + } catch { + file.value = undefined; + fileList.value = []; + message.error('JSON 文件格式不正确'); + return Upload.LIST_IGNORE; + } +} + +function resetForm() { + file.value = undefined; + fileList.value = []; + formData.key = ''; + formData.name = ''; + formRef.value?.clearValidate(); +} + + + diff --git a/apps/web-antdv-next/src/api/bpm/model/index.ts b/apps/web-antdv-next/src/api/bpm/model/index.ts index e0c033a44..2803e0e5b 100644 --- a/apps/web-antdv-next/src/api/bpm/model/index.ts +++ b/apps/web-antdv-next/src/api/bpm/model/index.ts @@ -22,6 +22,14 @@ export namespace BpmModelApi { startUsers?: UserInfo[]; } + /** 流程模型导出数据 */ + export interface ModelExport extends Partial { + key: string; + name: string; + simpleModel?: Record; + type: number; + } + /** 流程定义 */ export interface ProcessDefinition { id: string; @@ -96,6 +104,24 @@ export async function createModel(data: BpmModelApi.Model) { return requestClient.post('/bpm/model/create', data); } +/** 导入流程模型 */ +export async function importModel(file: File, key?: string, name?: string) { + const formData = new FormData(); + formData.append('file', file); + if (key) formData.append('key', key); + if (name) formData.append('name', name); + return requestClient.post('/bpm/model/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); +} + +/** 导出流程模型 */ +export async function exportModel(id: number) { + return requestClient.get( + `/bpm/model/export?id=${id}`, + ); +} + /** 删除流程模型 */ export async function deleteModel(id: number) { return requestClient.delete(`/bpm/model/delete?id=${id}`); diff --git a/apps/web-antdv-next/src/views/bpm/model/index.vue b/apps/web-antdv-next/src/views/bpm/model/index.vue index 907bf0be7..0a1a2ff4d 100644 --- a/apps/web-antdv-next/src/views/bpm/model/index.vue +++ b/apps/web-antdv-next/src/views/bpm/model/index.vue @@ -3,6 +3,7 @@ import type { ModelCategoryInfo } from '#/api/bpm/model'; import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue'; +import { useAccess } from '@vben/access'; import { Page, useVbenModal } from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; import { cloneDeep } from '@vben/utils'; @@ -27,12 +28,21 @@ import { router } from '#/router'; import CategoryForm from '../category/modules/form.vue'; import CategoryDraggableModel from './modules/category-draggable-model.vue'; +import ModelImportForm from './modules/import-form.vue'; const [CategoryFormModal, categoryFormModalApi] = useVbenModal({ connectedComponent: CategoryForm, destroyOnClose: true, }); +const [ImportModal, importModalApi] = useVbenModal({ + connectedComponent: ModelImportForm, + destroyOnClose: true, +}); + +const { hasAccessByCodes } = useAccess(); +const hasImportPermission = hasAccessByCodes(['bpm:model:import']); + const modelListSpinning = ref(false); // 模型列表加载状态 const saveSortLoading = ref(false); // 保存排序状态 @@ -95,6 +105,10 @@ function createModel() { }); } +function handleImportClick() { + importModalApi.open(); +} + /** 处理下拉菜单命令 */ function handleCommand(command: string) { if (command === 'handleCategoryAdd') { @@ -151,6 +165,7 @@ async function handleCategorySortSubmit() { + 新建模型 +