pull/122/head^2
xingyu4j 2025-05-29 23:55:08 +08:00
commit daee025866
4 changed files with 663 additions and 42 deletions

View File

@ -0,0 +1,529 @@
<script setup lang="ts">
import type { Rule } from 'ant-design-vue/es/form';
import type { Ref } from 'vue';
import type { SimpleFlowNode } from '../../consts';
import type { CopyTaskFormType } from '../../helpers';
import { computed, onMounted, reactive, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Col,
Form,
FormItem,
Input,
Radio,
RadioGroup,
Row,
Select,
SelectOption,
TabPane,
Tabs,
Textarea,
TreeSelect,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils';
import {
CANDIDATE_STRATEGY,
CandidateStrategy,
FieldPermissionType,
MULTI_LEVEL_DEPT,
NodeType,
} from '../../consts';
import {
useFormFieldsPermission,
useNodeForm,
useNodeName,
useWatchNode,
} from '../../helpers';
defineOptions({
name: 'CopyTaskNodeConfig',
});
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true,
},
});
const deptLevelLabel = computed(() => {
let label = '部门负责人来源';
label =
configForm.value.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
? `${label}(指定部门向上)`
: `${label}(发起人部门向上)`;
return label;
});
//
const [Drawer, drawerApi] = useVbenDrawer({
header: true,
closable: true,
title: '',
placement: 'right',
onCancel() {
drawerApi.close();
},
onConfirm() {
saveConfig();
},
});
//
const currentNode = useWatchNode(props);
//
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.COPY_TASK_NODE,
);
// Tab
const activeTabName = ref('user');
//
const {
formType,
fieldsPermissionConfig,
formFieldOptions,
getNodeConfigFormFields,
} = useFormFieldsPermission(FieldPermissionType.READ);
// ,
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect');
});
// ,
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
});
//
const formRef = ref(); // Ref
//
const formRules: Record<string, Rule[]> = reactive({
candidateStrategy: [
{ required: true, message: '抄送人设置不能为空', trigger: 'change' },
],
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [
{ required: true, message: '用户组不能为空', trigger: 'change' },
],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
formUser: [
{ required: true, message: '表单内用户字段不能为空', trigger: 'change' },
],
formDept: [
{ required: true, message: '表单内部门字段不能为空', trigger: 'change' },
],
expression: [
{ required: true, message: '流程表达式不能为空', trigger: 'blur' },
],
});
const {
configForm: tempConfigForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
getShowText,
handleCandidateParam,
parseCandidateParam,
} = useNodeForm(NodeType.COPY_TASK_NODE);
const configForm = tempConfigForm as Ref<CopyTaskFormType>;
//
const copyUserStrategies = computed(() => {
return CANDIDATE_STRATEGY.filter(
(item) => item.value !== CandidateStrategy.START_USER,
);
});
//
const changeCandidateStrategy = () => {
configForm.value.userIds = [];
configForm.value.deptIds = [];
configForm.value.roleIds = [];
configForm.value.postIds = [];
configForm.value.userGroups = [];
configForm.value.deptLevel = 1;
configForm.value.formUser = '';
configForm.value.formDept = '';
};
//
const saveConfig = async () => {
activeTabName.value = 'user';
if (!formRef.value) return false;
const valid = await formRef.value.validate();
if (!valid) return false;
const showText = getShowText();
if (!showText) return false;
currentNode.value.name = nodeName.value!;
currentNode.value.candidateParam = handleCandidateParam();
currentNode.value.candidateStrategy = configForm.value.candidateStrategy;
currentNode.value.showText = showText;
currentNode.value.fieldsPermission = fieldsPermissionConfig.value;
drawerApi.close();
return true;
};
//
const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name;
//
configForm.value.candidateStrategy = node.candidateStrategy!;
parseCandidateParam(node.candidateStrategy!, node?.candidateParam);
//
getNodeConfigFormFields(node.fieldsPermission);
drawerApi.open();
};
/** 批量更新权限 */
const updatePermission = (type: string) => {
fieldsPermissionConfig.value.forEach((field) => {
if (type === 'READ') {
field.permission = FieldPermissionType.READ;
} else if (type === 'WRITE') {
field.permission = FieldPermissionType.WRITE;
} else {
field.permission = FieldPermissionType.NONE;
}
});
};
//
onMounted(() => {
//
});
defineExpose({ showCopyTaskNodeConfig }); //
</script>
<template>
<Drawer class="w-[580px]">
<template #title>
<div class="config-header">
<Input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
</div>
</div>
</template>
<Tabs v-model:active-key="activeTabName">
<TabPane tab="抄送人" key="user">
<div>
<Form
ref="formRef"
:model="configForm"
:label-col="{ span: 24 }"
:wrapper-col="{ span: 24 }"
:rules="formRules"
>
<FormItem label="抄送人设置" name="candidateStrategy">
<RadioGroup
v-model:value="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<Row :gutter="[0, 8]">
<Col
v-for="(dict, index) in copyUserStrategies"
:key="index"
:span="8"
>
<Radio :value="dict.value" :label="dict.value">
{{ dict.label }}
</Radio>
</Col>
</Row>
</RadioGroup>
</FormItem>
<FormItem
v-if="configForm.candidateStrategy === CandidateStrategy.ROLE"
label="指定角色"
name="roleIds"
>
<Select
v-model:value="configForm.roleIds"
clearable
mode="multiple"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy ===
CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy ===
CandidateStrategy.DEPT_LEADER ||
configForm.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
name="deptIds"
>
<TreeSelect
v-model:value="configForm.deptIds"
:tree-data="deptTreeOptions"
:field-names="{
label: 'name',
value: 'id',
children: 'children',
}"
empty-text="加载中,请稍后"
multiple
:check-strictly="true"
allow-clear
tree-checkable
/>
</FormItem>
<FormItem
v-if="configForm.candidateStrategy === CandidateStrategy.POST"
label="指定岗位"
name="postIds"
>
<Select
v-model:value="configForm.postIds"
clearable
mode="multiple"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="configForm.candidateStrategy === CandidateStrategy.USER"
label="指定用户"
name="userIds"
>
<Select
v-model:value="configForm.userIds"
clearable
mode="multiple"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy === CandidateStrategy.USER_GROUP
"
label="指定用户组"
name="userGroups"
>
<Select
v-model:value="configForm.userGroups"
clearable
mode="multiple"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy === CandidateStrategy.FORM_USER
"
label="表单内用户字段"
name="formUser"
>
<Select v-model:value="configForm.formUser" clearable>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy ===
CandidateStrategy.FORM_DEPT_LEADER
"
label="表单内部门字段"
name="formDept"
>
<Select v-model:value="configForm.formDept" clearable>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy ===
CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy ===
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy ===
CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="configForm.deptLevel" clearable>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
configForm.candidateStrategy === CandidateStrategy.EXPRESSION
"
label="流程表达式"
name="expression"
>
<Textarea v-model:value="configForm.expression" clearable />
</FormItem>
</Form>
</div>
</TabPane>
<TabPane
tab="表单字段权限"
key="fields"
v-if="formType === BpmModelFormType.NORMAL"
>
<div class="p-1">
<div class="mb-4 text-[16px] font-bold">字段权限</div>
<!-- 表头 -->
<Row class="border border-gray-200 px-4 py-3">
<Col :span="8" class="font-bold">字段名称</Col>
<Col :span="16">
<Row>
<Col :span="8" class="flex items-center justify-center">
<span
class="cursor-pointer font-bold"
@click="updatePermission('READ')"
>
只读
</span>
</Col>
<Col :span="8" class="flex items-center justify-center">
<span
class="cursor-pointer font-bold"
@click="updatePermission('WRITE')"
>
可编辑
</span>
</Col>
<Col :span="8" class="flex items-center justify-center">
<span
class="cursor-pointer font-bold"
@click="updatePermission('NONE')"
>
隐藏
</span>
</Col>
</Row>
</Col>
</Row>
<!-- 表格内容 -->
<div v-for="(item, index) in fieldsPermissionConfig" :key="index">
<Row class="border border-t-0 border-gray-200 px-4 py-2">
<Col :span="8" class="flex items-center truncate">
{{ item.title }}
</Col>
<Col :span="16">
<RadioGroup v-model:value="item.permission" class="w-full">
<Row>
<Col :span="8" class="flex items-center justify-center">
<Radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
/>
</Col>
<Col :span="8" class="flex items-center justify-center">
<Radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
disabled
/>
</Col>
<Col :span="8" class="flex items-center justify-center">
<Radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
/>
</Col>
</Row>
</RadioGroup>
</Col>
</Row>
</div>
</div>
</TabPane>
</Tabs>
<template #footer>
<Button type="primary" @click="saveConfig"> </Button>
<Button @click="drawerApi.close()"> </Button>
</template>
</Drawer>
</template>
<style lang="scss" scoped>
.config-editable-input {
&:focus {
outline: 0;
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
</style>

View File

@ -34,6 +34,8 @@ import {
TypographyText,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils';
import {
APPROVE_METHODS,
APPROVE_TYPE,
@ -626,11 +628,9 @@ onMounted(() => {
name="roleIds"
>
<Select
filterable
v-model:value="configForm.roleIds"
clearable
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in roleOptions"
@ -664,10 +664,8 @@ onMounted(() => {
}"
empty-text="加载中,请稍后"
multiple
node-key="id"
:check-strictly="true"
allow-clear
style="width: 100%"
tree-checkable
/>
</FormItem>
@ -677,11 +675,9 @@ onMounted(() => {
name="postIds"
>
<Select
filterable
v-model:value="configForm.postIds"
clearable
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in postOptions"
@ -699,11 +695,9 @@ onMounted(() => {
name="userIds"
>
<Select
filterable
v-model:value="configForm.userIds"
clearable
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in userOptions"
@ -723,11 +717,9 @@ onMounted(() => {
name="userGroups"
>
<Select
filterable
v-model:value="configForm.userGroups"
clearable
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in userGroupOptions"
@ -746,12 +738,7 @@ onMounted(() => {
label="表单内用户字段"
name="formUser"
>
<Select
filterable
v-model:value="configForm.formUser"
clearable
style="width: 100%"
>
<Select v-model:value="configForm.formUser" clearable>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
@ -771,12 +758,7 @@ onMounted(() => {
label="表单内部门字段"
name="formDept"
>
<Select
filterable
v-model:value="configForm.formDept"
clearable
style="width: 100%"
>
<Select v-model:value="configForm.formDept" clearable>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
@ -802,7 +784,7 @@ onMounted(() => {
:label="deptLevelLabel!"
name="deptLevel"
>
<Select filterable v-model:value="configForm.deptLevel" clearable>
<Select v-model:value="configForm.deptLevel" clearable>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
@ -821,11 +803,7 @@ onMounted(() => {
label="流程表达式"
name="expression"
>
<Textarea
v-model:value="configForm.expression"
clearable
style="width: 100%"
/>
<Textarea v-model:value="configForm.expression" clearable />
</FormItem>
<!-- 多人审批/办理 方式 -->
<FormItem :label="`多人${nodeTypeName}方式`" name="approveMethod">
@ -890,12 +868,7 @@ onMounted(() => {
label="驳回节点"
name="returnNodeId"
>
<Select
filterable
v-model:value="configForm.returnNodeId"
clearable
style="width: 100%"
>
<Select v-model:value="configForm.returnNodeId" clearable>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
@ -963,8 +936,7 @@ onMounted(() => {
<Col>
<FormItem name="timeDuration">
<InputNumber
class="mr-2"
:style="{ width: '100px' }"
class="mr-2 mt-0.5"
v-model:value="configForm.timeDuration"
:min="1"
controls-position="right"
@ -973,7 +945,6 @@ onMounted(() => {
</Col>
<Col>
<Select
filterable
v-model:value="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@ -1040,11 +1011,9 @@ onMounted(() => {
name="assignEmptyHandlerUserIds"
>
<Select
filterable
v-model:value="configForm.assignEmptyHandlerUserIds"
clearable
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in userOptions"
@ -1151,7 +1120,11 @@ onMounted(() => {
</div>
</div>
</TabPane>
<TabPane tab="表单字段权限" key="fields" v-if="formType === 10">
<TabPane
tab="表单字段权限"
key="fields"
v-if="formType === BpmModelFormType.NORMAL"
>
<div class="p-1">
<div class="mb-4 text-[16px] font-bold">字段权限</div>

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../../consts';
import { inject, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import CopyTaskNodeConfig from '../nodes-config/copy-task-node-config.vue';
import NodeHandler from './node-handler.vue';
defineOptions({
name: 'CopyTaskNode',
});
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true,
},
});
//
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined];
}>();
//
const readonly = inject<Boolean>('readonly');
//
const currentNode = useWatchNode(props);
//
const { showInput, blurEvent, clickTitle } = useNodeName2(
currentNode,
NodeType.COPY_TASK_NODE,
);
const nodeSetting = ref();
//
const openNodeConfig = () => {
if (readonly) {
return;
}
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value);
nodeSetting.value.openDrawer();
};
//
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode);
};
</script>
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`,
]"
>
<div class="node-title-container">
<div class="node-title-icon copy-task">
<span class="iconfont icon-copy"></span>
</div>
<Input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div
class="node-text"
:title="currentNode.showText"
v-if="currentNode.showText"
>
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div>
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteNode"
/>
</div>
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<CopyTaskNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -3,6 +3,7 @@ import type { SimpleFlowNode } from '../consts';
import { NodeType } from '../consts';
import { useWatchNode } from '../helpers';
import CopyTaskNode from './nodes/copy-task-node.vue';
import EndEventNode from './nodes/end-event-node.vue';
import StartUserNode from './nodes/start-user-node.vue';
import UserTaskNode from './nodes/user-task-node.vue';
@ -77,11 +78,11 @@ const recursiveFindParentNode = (
@find-parent-node="findParentNode"
/>
<!-- 抄送节点 -->
<!-- <CopyTaskNode
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/> -->
/>
<!-- 条件节点 -->
<!-- <ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"