feat(bpm): 完善流程模型导入导出功能

- 优化 web-antd 模型导入弹窗
- 同步 web-ele 和 web-antdv-next
- 修复上传文件状态残留问题
- 统一导出下载逻辑与权限控制
This commit is contained in:
YunaiV
2026-07-12 10:24:53 +08:00
parent 6d6895753f
commit dd0809f002
12 changed files with 549 additions and 146 deletions

View File

@@ -22,6 +22,14 @@ export namespace BpmModelApi {
startUsers?: UserInfo[];
}
/** 流程模型导出数据 */
export interface ModelExport extends Partial<Model> {
key: string;
name: string;
simpleModel?: Record<string, unknown>;
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<BpmModelApi.Model>(`/bpm/model/export?id=${id}`);
return requestClient.get<BpmModelApi.ModelExport>(
`/bpm/model/export?id=${id}`,
);
}
/** 删除流程模型 */

View File

@@ -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<HTMLElement>('categoryGroupRef'); // 可以排
const sortableInstance = ref<any>(null); // 排序引用,以便后续启用或禁用排序
const isCategorySorting = ref(false); // 分类排序状态
// ========== 导入弹窗相关 ==========
const importFile = ref<File | null>(null); // 导入文件(用于最终提交)
const importFileList = ref<any[]>([]); // 上传组件的文件列表
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() {
<Page auto-content-height>
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<ImportModal @success="getList" />
<Card
:body-style="{ padding: '10px' }"
class="mb-4"
@@ -235,7 +177,11 @@ async function handleCategorySortSubmit() {
<Button class="ml-2" type="primary" @click="createModel">
<IconifyIcon icon="lucide:plus" /> 新建模型
</Button>
<Button class="ml-2" @click="handleImportClick">
<Button
v-if="hasImportPermission"
class="ml-2"
@click="handleImportClick"
>
<IconifyIcon icon="lucide:upload" /> 导入模型
</Button>
<Dropdown class="ml-2" placement="bottomRight" arrow>
@@ -291,66 +237,5 @@ async function handleCategorySortSubmit() {
/>
</div>
</Card>
<!-- 导入流程模型弹窗 -->
<ImportModal title="导入流程模型" class="w-[640px]">
<div class="mx-4 my-2">
<Alert
message="跨租户导入说明"
description="导入文件由【导出】功能生成。导入后将在当前租户下新建流程模型,审批人、可发起人、流程管理员等数据无法跨租户复用,仅保留审批节点结构,请在导入后重新配置审批人再发布。"
type="info"
show-icon
class="!mb-4"
/>
<Form ref="importFormRef" :model="importForm">
<!-- 1. 流程模型文件 -->
<Form.Item label="流程模型文件" name="file">
<Upload.Dragger
v-model:file-list="importFileList"
:before-upload="beforeUpload"
:max-count="1"
accept=".json"
:show-upload-list="{ showRemoveIcon: true }"
>
<p class="ant-upload-drag-icon flex justify-center">
<IconifyIcon icon="lucide:cloud-upload" class="text-3xl" />
</p>
<p class="ant-upload-text">点击或拖拽文件到此处上传</p>
<p class="ant-upload-hint">
仅支持上传单个 JSON 格式的流程模型文件
</p>
</Upload.Dragger>
</Form.Item>
<!-- 2. 流程标识 -->
<Form.Item
label="流程标识"
name="key"
:rules="[{ required: true, message: '请输入流程标识' }]"
>
<Input
v-model:value="importForm.key"
placeholder="请输入流程标识"
/>
<div class="text-xs text-gray-400">
同租户导入时若标识已存在请修改后再导入
</div>
</Form.Item>
<!-- 3. 流程名称 -->
<Form.Item
label="流程名称"
name="name"
:rules="[{ required: true, message: '请输入流程名称' }]"
>
<Input
v-model:value="importForm.name"
placeholder="请输入流程名称"
/>
<div class="text-xs text-gray-400">必填请填写流程名称</div>
</Form.Item>
</Form>
</div>
</ImportModal>
</Page>
</template>

View File

@@ -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)"
>
<Menu.Item key="handleCopy"> 复制 </Menu.Item>
<Menu.Item key="handleExport"> 导出 </Menu.Item>
<Menu.Item v-if="hasPermiExport" key="handleExport">
导出
</Menu.Item>
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
<Menu.Item

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
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<File>();
const fileList = ref<any[]>([]);
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();
}
</script>
<template>
<Modal title="导入流程模型" class="w-[640px]">
<div class="mx-4 my-2">
<Alert
class="!mb-4"
description="导入会完整保留流程配置,并将新模型归属到当前租户。请确认人员、部门、表单、子流程等关联在当前租户有效后再发布。"
message="导入说明"
show-icon
type="info"
/>
<Form ref="formRef" :model="formData">
<Form.Item label="流程模型文件">
<Upload.Dragger
v-model:file-list="fileList"
:before-upload="beforeUpload"
:max-count="1"
accept=".json"
@remove="file = undefined"
>
<p class="ant-upload-drag-icon flex justify-center">
<IconifyIcon class="text-3xl" icon="lucide:cloud-upload" />
</p>
<p class="ant-upload-text">点击或拖拽文件到此处上传</p>
<p class="ant-upload-hint">仅支持单个 JSON 流程模型文件</p>
</Upload.Dragger>
</Form.Item>
<Form.Item
label="流程标识"
name="key"
:rules="[{ required: true, message: '请输入流程标识' }]"
>
<Input v-model:value="formData.key" placeholder="请输入流程标识" />
</Form.Item>
<Form.Item
label="流程名称"
name="name"
:rules="[{ required: true, message: '请输入流程名称' }]"
>
<Input v-model:value="formData.name" placeholder="请输入流程名称" />
</Form.Item>
</Form>
</div>
</Modal>
</template>

View File

@@ -22,6 +22,14 @@ export namespace BpmModelApi {
startUsers?: UserInfo[];
}
/** 流程模型导出数据 */
export interface ModelExport extends Partial<Model> {
key: string;
name: string;
simpleModel?: Record<string, unknown>;
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<string>('/bpm/model/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
/** 导出流程模型 */
export async function exportModel(id: number) {
return requestClient.get<BpmModelApi.ModelExport>(
`/bpm/model/export?id=${id}`,
);
}
/** 删除流程模型 */
export async function deleteModel(id: number) {
return requestClient.delete(`/bpm/model/delete?id=${id}`);

View File

@@ -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() {
<Page auto-content-height>
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<ImportModal @success="getList" />
<Card
:styles="{ body: { padding: '10px' } }"
class="mb-4"
@@ -169,6 +184,13 @@ async function handleCategorySortSubmit() {
<Button class="ml-2" type="primary" @click="createModel">
<IconifyIcon icon="lucide:plus" /> 新建模型
</Button>
<Button
v-if="hasImportPermission"
class="ml-2"
@click="handleImportClick"
>
<IconifyIcon icon="lucide:upload" /> 导入模型
</Button>
<Dropdown class="ml-2" placement="bottomRight" arrow>
<Button>
<template #icon>

View File

@@ -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';
@@ -33,6 +38,7 @@ import {
cleanModel,
deleteModel,
deployModel,
exportModel,
updateModelSortBatch,
updateModelState,
} from '#/api/bpm/model';
@@ -84,6 +90,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: {
@@ -314,6 +323,10 @@ function handleModelCommand(command: string, row: any) {
handleDelete(row);
break;
}
case 'handleExport': {
handleExportModel(row);
break;
}
case 'handleReport': {
handleReport(row);
break;
@@ -324,6 +337,20 @@ function handleModelCommand(command: string, row: any) {
}
}
async function handleExportModel(row: any) {
const hideLoading = message.loading({ content: '正在导出...', duration: 0 });
try {
const data = await exportModel(row.id);
downloadFileFromBlobPart({
fileName: `${row.key || row.name || 'model'}.json`,
source: JSON.stringify(data, null, 2),
});
message.success('导出成功');
} finally {
hideLoading();
}
}
/** 更新状态操作 */
async function handleChangeState(row: any) {
const state = row.processDefinition.suspensionState;
@@ -691,6 +718,9 @@ function handleRenameSuccess() {
>
<MenuItem key="handleCopy"> 复制 </MenuItem>
<MenuItem key="handleDefinitionList"> 历史 </MenuItem>
<MenuItem v-if="hasPermiExport" key="handleExport">
导出
</MenuItem>
<MenuItem
key="handleReport"

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Alert, Form, FormItem, Input, message, Upload } from 'antdv-next';
import { importModel } from '#/api/bpm/model';
const emit = defineEmits(['success']);
const file = ref<File>();
const fileList = ref<any[]>([]);
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();
}
</script>
<template>
<Modal title="导入流程模型" class="w-[640px]">
<div class="mx-4 my-2">
<Alert
class="!mb-4"
description="导入会完整保留流程配置,并将新模型归属到当前租户。请确认人员、部门、表单、子流程等关联在当前租户有效后再发布。"
message="导入说明"
show-icon
type="info"
/>
<Form ref="formRef" :model="formData">
<FormItem label="流程模型文件">
<Upload.Dragger
v-model:file-list="fileList"
:before-upload="beforeUpload"
:max-count="1"
accept=".json"
@remove="file = undefined"
>
<p class="ant-upload-drag-icon flex justify-center">
<IconifyIcon class="text-3xl" icon="lucide:cloud-upload" />
</p>
<p class="ant-upload-text">点击或拖拽文件到此处上传</p>
<p class="ant-upload-hint">仅支持单个 JSON 流程模型文件</p>
</Upload.Dragger>
</FormItem>
<FormItem
label="流程标识"
name="key"
:rules="[{ required: true, message: '请输入流程标识' }]"
>
<Input v-model:value="formData.key" placeholder="请输入流程标识" />
</FormItem>
<FormItem
label="流程名称"
name="name"
:rules="[{ required: true, message: '请输入流程名称' }]"
>
<Input v-model:value="formData.name" placeholder="请输入流程名称" />
</FormItem>
</Form>
</div>
</Modal>
</template>

View File

@@ -22,6 +22,14 @@ export namespace BpmModelApi {
startUsers?: UserInfo[];
}
/** 流程模型导出数据 */
export interface ModelExport extends Partial<Model> {
key: string;
name: string;
simpleModel?: Record<string, unknown>;
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<string>('/bpm/model/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
/** 导出流程模型 */
export async function exportModel(id: number) {
return requestClient.get<BpmModelApi.ModelExport>(
`/bpm/model/export?id=${id}`,
);
}
/** 删除流程模型 */
export async function deleteModel(id: number) {
return requestClient.delete(`/bpm/model/delete?id=${id}`);

View File

@@ -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';
@@ -19,12 +20,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); // 保存排序状态
@@ -87,6 +97,10 @@ function createModel() {
});
}
function handleImportClick() {
importModalApi.open();
}
/** 处理下拉菜单命令 */
function handleCommand(command: string) {
if (command === 'handleCategoryAdd') {
@@ -143,6 +157,7 @@ async function handleCategorySortSubmit() {
<Page auto-content-height>
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<ImportModal @success="getList" />
<ElCard
body-style="padding: 10px"
class="mb-4"
@@ -162,6 +177,13 @@ async function handleCategorySortSubmit() {
<ElButton class="ml-2" type="primary" @click="createModel">
<IconifyIcon icon="lucide:plus" /> 新建模型
</ElButton>
<ElButton
v-if="hasImportPermission"
class="ml-2"
@click="handleImportClick"
>
<IconifyIcon icon="lucide:upload" /> 导入模型
</ElButton>
<ElDropdown class="ml-2" placement="bottom-end" trigger="click">
<ElButton>
<div class="flex items-center justify-center">

View File

@@ -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';
@@ -32,6 +37,7 @@ import {
cleanModel,
deleteModel,
deployModel,
exportModel,
updateModelSortBatch,
updateModelState,
} from '#/api/bpm/model';
@@ -83,6 +89,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: {
@@ -310,6 +319,10 @@ function handleModelCommand(command: string, row: any) {
handleDelete(row);
break;
}
case 'handleExport': {
handleExportModel(row);
break;
}
case 'handleReport': {
handleReport(row);
break;
@@ -320,6 +333,20 @@ function handleModelCommand(command: string, row: any) {
}
}
async function handleExportModel(row: any) {
const loadingInstance = ElLoading.service({ text: '正在导出...' });
try {
const data = await exportModel(row.id);
downloadFileFromBlobPart({
fileName: `${row.key || row.name || 'model'}.json`,
source: JSON.stringify(data, null, 2),
});
ElMessage.success('导出成功');
} finally {
loadingInstance.close();
}
}
/** 更新状态操作 */
async function handleChangeState(row: any) {
const state = row.processDefinition.suspensionState;
@@ -691,6 +718,12 @@ function handleRenameSuccess() {
>
历史
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermiExport"
@click="handleModelCommand('handleExport', row)"
>
导出
</el-dropdown-item>
<el-dropdown-item
@click="handleModelCommand('handleReport', row)"

View File

@@ -0,0 +1,122 @@
<script lang="ts" setup>
import type { UploadFile, UploadUserFile } from 'element-plus';
import { reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
ElAlert,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElUpload,
} from 'element-plus';
import { importModel } from '#/api/bpm/model';
const emit = defineEmits(['success']);
const file = ref<File>();
const fileList = ref<UploadUserFile[]>([]);
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = reactive({ key: '', name: '' });
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!file.value) {
ElMessage.warning('请上传流程模型文件');
return;
}
await formRef.value?.validate();
modalApi.lock();
try {
await importModel(file.value, formData.key, formData.name);
await modalApi.close();
emit('success');
ElMessage.success('导入成功');
} finally {
modalApi.unlock();
}
},
onOpenChange(isOpen) {
if (!isOpen) resetForm();
},
});
async function handleChange(uploadFile: UploadFile) {
if (!uploadFile.raw) return;
if (!uploadFile.name.toLowerCase().endsWith('.json')) {
ElMessage.error('仅支持上传 JSON 格式的流程模型文件');
resetFile();
return;
}
try {
const data = JSON.parse(await uploadFile.raw.text());
file.value = uploadFile.raw;
formData.key = data.key || '';
formData.name = data.name || '';
} catch {
resetFile();
ElMessage.error('JSON 文件格式不正确');
}
}
function resetFile() {
file.value = undefined;
fileList.value = [];
}
function resetForm() {
resetFile();
formData.key = '';
formData.name = '';
formRef.value?.clearValidate();
}
</script>
<template>
<Modal title="导入流程模型" class="w-[640px]">
<div class="mx-4 my-2">
<ElAlert
class="!mb-4"
description="导入会完整保留流程配置,并将新模型归属到当前租户。请确认人员、部门、表单、子流程等关联在当前租户有效后再发布。"
show-icon
title="导入说明"
type="info"
/>
<ElForm ref="formRef" :model="formData" label-width="100px">
<ElFormItem label="流程模型文件">
<ElUpload
v-model:file-list="fileList"
:auto-upload="false"
:limit="1"
accept=".json"
drag
@change="handleChange"
@remove="resetFile"
>
<IconifyIcon class="mb-2 text-3xl" icon="lucide:cloud-upload" />
<div>点击或拖拽 JSON 流程模型文件到此处</div>
</ElUpload>
</ElFormItem>
<ElFormItem
label="流程标识"
prop="key"
:rules="[{ required: true, message: '请输入流程标识' }]"
>
<ElInput v-model="formData.key" placeholder="请输入流程标识" />
</ElFormItem>
<ElFormItem
label="流程名称"
prop="name"
:rules="[{ required: true, message: '请输入流程名称' }]"
>
<ElInput v-model="formData.name" placeholder="请输入流程名称" />
</ElFormItem>
</ElForm>
</div>
</Modal>
</template>