chore: 流程定义(开发中)

This commit is contained in:
dap 2024-12-11 21:33:59 +08:00
parent 7577f17dd9
commit 97b91aaf7c
7 changed files with 670 additions and 4 deletions

View File

@ -30,6 +30,7 @@ export interface CategoryVO {
*
*/
children: CategoryVO[];
key: string;
}
export interface CategoryForm extends BaseEntity {

View File

@ -0,0 +1,130 @@
import type { ID, IDS, PageQuery } from '#/api/common';
import { requestClient } from '#/api/request';
export function workflowDefinitionList(params?: PageQuery) {
return requestClient.get('/workflow/definition/list', { params });
}
/**
*
* @param flowCode
* @returns
*/
export function getHisListByKey(flowCode: string) {
return requestClient.get(`/workflow/definition/getHisListByKey/${flowCode}`);
}
/**
*
* @param id id
* @returns
*/
export function workflowDefinitionInfo(id: ID) {
return requestClient.get(`/workflow/definition/${id}`);
}
/**
*
* @param data
*/
export function workflowDefinitionAdd(data: any) {
return requestClient.postWithMsg<void>('/workflow/definition', data);
}
/**
*
* @param data
*/
export function workflowDefinitionUpdate(data: any) {
return requestClient.putWithMsg<void>('/workflow/definition', data);
}
/**
*
* @param id id
* @returns boolean
*/
export function workflowDefinitionPublish(id: ID) {
return requestClient.putWithMsg<boolean>(
`/workflow/definition/publish/${id}`,
);
}
/**
*
* @param id id
* @returns boolean
*/
export function workflowDefinitionUnPublish(id: ID) {
return requestClient.putWithMsg<boolean>(
`/workflow/definition/unPublish/${id}`,
);
}
/**
*
* @param ids idList
*/
export function workflowDefinitionDelete(ids: IDS) {
return requestClient.deleteWithMsg<void>(`/workflow/definition/${ids}`);
}
/**
*
* @param id id
*/
export function workflowDefinitionCopy(id: ID) {
return requestClient.postWithMsg<void>(`/workflow/definition/copy/${id}`);
}
/**
*
* @param file
* @returns boolean
*/
export function workflowDefinitionImport(file: File) {
return requestClient.postWithMsg<boolean>(
'/workflow/definition/importDef',
{
file,
},
{ headers: { 'Content-Type': 'multipart/form-data' } },
);
}
/**
*
* @param id id
* @returns blob
*/
export function workflowDefinitionExport(id: ID) {
return requestClient.postWithMsg<Blob>(
`/workflow/definition/exportDef/${id}`,
{
responseType: 'blob',
isTransformResponse: false,
},
);
}
/**
* xml字符串
* @param id id
* @returns xml
*/
export function workflowDefinitionXml(id: ID) {
return requestClient.get<string>(`/workflow/definition/xmlString/${id}`);
}
/**
* /
* @param id id
* @param active /
* @returns boolean
*/
export function workflowDefinitionActive(id: ID, active: boolean) {
return requestClient.putWithMsg<boolean>(
`/workflow/definition/active/${id}?active=${active}`,
);
}

View File

@ -105,6 +105,30 @@ const profileRoute: RouteRecordStringComponent[] = [
},
],
},
{
component: 'BasicLayout',
meta: {
hideChildrenInMenu: true,
hideInMenu: true,
title: '流程设计',
},
name: 'WorkflowDesigner',
path: '/',
redirect: '/workflow/designer',
children: [
{
component: '/workflow/components/flow-designer',
meta: {
activePath: '/workflow/processDefinition',
icon: 'eos-icons:role-binding-outlined',
keepAlive: true,
title: '流程设计',
},
name: 'RoleAssignIndex',
path: '/workflow/designer',
},
],
},
];
/**

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
defineOptions({ name: 'FlowDesigner' });
const route = useRoute();
const definitionId = route.query.definitionId as string;
const disabled = route.query.disabled === 'true';
const accessStore = useAccessStore();
const params = {
Authorization: `Bearer ${accessStore.accessToken}`,
id: definitionId,
disabled,
};
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
</script>
<template>
<iframe :src="url" class="size-full"></iframe>
</template>

View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import type { CategoryVO } from '#/api/workflow/category/model';
import { onMounted, type PropType, ref } from 'vue';
import { listToTree } from '@vben/utils';
import { SyncOutlined } from '@ant-design/icons-vue';
import { InputSearch, Skeleton, Tree } from 'ant-design-vue';
import { categoryList } from '#/api/workflow/category';
defineOptions({ inheritAttrs: false });
const emit = defineEmits<{
/**
* 点击刷新按钮的事件
*/
reload: [];
/**
* 点击节点的事件
*/
select: [];
}>();
const selectCode = defineModel('selectCode', {
required: true,
type: Array as PropType<string[]>,
});
const searchValue = defineModel('searchValue', {
type: String,
default: '',
});
const categoryTreeArray = ref<CategoryVO[]>([]);
/** 骨架屏加载 */
const showTreeSkeleton = ref<boolean>(true);
async function loadTree() {
showTreeSkeleton.value = true;
searchValue.value = '';
selectCode.value = [];
const ret = await categoryList();
let treeData = listToTree(ret, {
id: 'id',
pid: 'parentId',
});
treeData = [
{
categoryName: '根目录',
id: 0,
children: treeData,
},
];
categoryTreeArray.value = treeData;
showTreeSkeleton.value = false;
}
async function handleReload() {
await loadTree();
emit('reload');
}
onMounted(loadTree);
</script>
<template>
<div :class="$attrs.class">
<Skeleton
:loading="showTreeSkeleton"
:paragraph="{ rows: 8 }"
active
class="p-[8px]"
>
<div
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
>
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
<div class="bg-background z-100 sticky left-0 top-0 p-[8px]">
<InputSearch
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"
size="small"
>
<template #enterButton>
<a-button @click="handleReload">
<SyncOutlined class="text-primary" />
</a-button>
</template>
</InputSearch>
</div>
<div class="h-full overflow-x-hidden px-[8px]">
<Tree
v-bind="$attrs"
v-if="categoryTreeArray.length > 0"
v-model:selected-keys="selectCode"
:class="$attrs.class"
:field-names="{ title: 'categoryName', key: 'categoryCode' }"
:show-line="{ showLeafIcon: false }"
:tree-data="categoryTreeArray"
:virtual="false"
default-expand-all
@select="$emit('select')"
>
<template #title="{ categoryName: label }">
<span v-if="label.indexOf(searchValue) > -1">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,
)
}}
</span>
<span v-else>{{ label }}</span>
</template>
</Tree>
</div>
</div>
</Skeleton>
</div>
</template>

View File

@ -0,0 +1,173 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'flowName',
label: '流程名称',
},
{
component: 'Input',
fieldName: 'flowCode',
label: '流程code',
},
];
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
field: 'flowName',
title: '流程名称',
minWidth: 150,
},
{
field: 'flowCode',
title: '流程code',
minWidth: 150,
},
{
field: 'version',
title: '版本号',
minWidth: 80,
formatter: ({ cellValue }) => `V${cellValue}.0`,
},
{
field: 'activityStatus',
title: '状态',
minWidth: 100,
},
{
field: 'isPublish',
title: '发布状态',
minWidth: 100,
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
resizable: false,
width: 180,
},
];
export const drawerSchema: FormSchemaGetter = () => [
{
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
fieldName: 'userId',
},
{
component: 'Input',
fieldName: 'userName',
label: '用户账号',
rules: 'required',
},
{
component: 'InputPassword',
fieldName: 'password',
label: '用户密码',
rules: 'required',
},
{
component: 'Input',
fieldName: 'nickName',
label: '用户昵称',
rules: 'required',
},
{
component: 'TreeSelect',
// 在drawer里更新 这里不需要默认的componentProps
defaultValue: undefined,
fieldName: 'deptId',
label: '所属部门',
rules: 'selectRequired',
},
{
component: 'Input',
fieldName: 'phonenumber',
label: '手机号码',
defaultValue: undefined,
rules: z
.string()
.regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码')
.optional()
.or(z.literal('')),
},
{
component: 'Input',
fieldName: 'email',
defaultValue: undefined,
label: '邮箱',
/**
* z.literal Zod
*
* 使 z.literal
*
*/
rules: z.string().email('请输入正确的邮箱').optional().or(z.literal('')),
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: getDictOptions(DictEnum.SYS_USER_SEX),
optionType: 'button',
},
defaultValue: '0',
fieldName: 'sex',
formItemClass: 'col-span-2 lg:col-span-1',
label: '性别',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),
optionType: 'button',
},
defaultValue: '0',
fieldName: 'status',
formItemClass: 'col-span-2 lg:col-span-1',
label: '状态',
},
{
component: 'Select',
componentProps: {
getPopupContainer,
mode: 'multiple',
optionFilterProp: 'label',
optionLabelProp: 'label',
placeholder: '请先选择部门',
},
fieldName: 'postIds',
help: '选择部门后, 将自动加载该部门下所有的岗位',
label: '岗位',
},
{
component: 'Select',
componentProps: {
getPopupContainer,
mode: 'multiple',
optionFilterProp: 'title',
optionLabelProp: 'title',
},
fieldName: 'roleIds',
label: '角色',
},
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'items-baseline',
label: '备注',
},
];

View File

@ -1,9 +1,197 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
import type { Recordable } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import { Page, type VbenFormProps } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import { getVxePopupContainer } from '@vben/utils';
import { Avatar, Modal, Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { vxeCheckboxChecked } from '#/adapter/vxe-table';
import { userRemove, userStatusChange } from '#/api/system/user';
import { workflowDefinitionList } from '#/api/workflow/definition';
import { TableSwitch } from '#/components/table';
import CategoryTree from './category-tree.vue';
import { columns, querySchema } from './data';
//
const selectedCode = ref<string[]>([]);
const formOptions: VbenFormProps = {
schema: querySchema(),
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
handleReset: async () => {
selectedCode.value = [];
// eslint-disable-next-line no-use-before-define
const { formApi, reload } = tableApi;
await formApi.resetForm();
const formValues = formApi.form.values;
formApi.setLatestSubmissionValues(formValues);
await reload(formValues);
},
//
fieldMappingTime: [
[
'createTime',
['params[beginTime]', 'params[endTime]'],
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
],
],
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
trigger: 'default',
},
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
//
if (selectedCode.value.length === 1) {
formValues.categoryCode = selectedCode.value[0];
} else {
Reflect.deleteProperty(formValues, 'categoryCode');
}
return await workflowDefinitionList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
isHover: true,
keyField: 'id',
height: 48,
},
id: 'workflow-definition-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
async function handleDelete(row: Recordable<any>) {
await userRemove(row.id);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await userRemove(ids);
await tableApi.query();
},
});
}
const { hasAccessByCodes } = useAccess();
const router = useRouter();
function handlePreview(row: any) {
console.log(row);
router.push({
path: '/workflow/designer',
query: { definitionId: row.id, disabled: 'true' },
});
}
</script>
<template>
<div>
<CommonSkeleton />
</div>
<Page :auto-content-height="true">
<div class="flex h-full gap-[8px]">
<CategoryTree
v-model:select-code="selectedCode"
class="w-[260px]"
@reload="() => tableApi.reload()"
@select="() => tableApi.reload()"
/>
<BasicTable class="flex-1 overflow-hidden" table-title="流程定义列表">
<template #toolbar-tools>
<Space>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['system:user:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button type="primary" v-access:code="['system:user:add']">
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #avatar="{ row }">
<!-- 可能要判断空字符串情况 所以没有使用?? -->
<Avatar :src="row.avatar || preferences.app.defaultAvatar" />
</template>
<template #status="{ row }">
<TableSwitch
v-model="row.status"
:api="() => userStatusChange(row)"
:disabled="
row.userId === 1 || !hasAccessByCodes(['system:user:edit'])
"
:reload="() => tableApi.query()"
/>
</template>
<template #action="{ row }">
<Space>
<ghost-button v-access:code="['system:user:edit']">
{{ $t('pages.common.edit') }}
</ghost-button>
<a-button size="small" type="link" @click="handlePreview(row)">
查看流程
</a-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['system:user:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
</div>
</Page>
</template>