feat(bpm): 完善流程模型导入导出功能
- 优化 web-antd 模型导入弹窗 - 同步 web-ele 和 web-antdv-next - 修复上传文件状态残留问题 - 统一导出下载逻辑与权限控制
This commit is contained in:
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除流程模型 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
111
apps/web-antd/src/views/bpm/model/modules/import-form.vue
Normal file
111
apps/web-antd/src/views/bpm/model/modules/import-form.vue
Normal 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>
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
111
apps/web-antdv-next/src/views/bpm/model/modules/import-form.vue
Normal file
111
apps/web-antdv-next/src/views/bpm/model/modules/import-form.vue
Normal 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>
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)"
|
||||
|
||||
122
apps/web-ele/src/views/bpm/model/modules/import-form.vue
Normal file
122
apps/web-ele/src/views/bpm/model/modules/import-form.vue
Normal 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>
|
||||
Reference in New Issue
Block a user