Merge pull request !172 from xingyu/dev
This commit is contained in:
@@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
||||
@@ -7,6 +7,8 @@ export namespace BpmProcessDefinitionApi {
|
||||
export interface ProcessDefinition {
|
||||
id: string;
|
||||
version: number;
|
||||
name: string;
|
||||
description: string;
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
modelType: number;
|
||||
@@ -15,6 +17,7 @@ export namespace BpmProcessDefinitionApi {
|
||||
bpmnXml?: string;
|
||||
simpleModel?: string;
|
||||
formFields?: string[];
|
||||
icon?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export namespace BpmProcessInstanceApi {
|
||||
candidateStrategy?: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: User[];
|
||||
endTime?: Date;
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
|
||||
@@ -186,6 +186,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
});
|
||||
|
||||
const buildSelect = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
|
||||
if (props.multiple) {
|
||||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
@@ -193,7 +199,9 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
mode="multiple"
|
||||
{...attrs}
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
// TODO: remote 对等实现
|
||||
// remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
@@ -212,7 +220,9 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
<Select
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
// TODO: @dhb52 remote 对等实现, 还是说没作用
|
||||
// remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
@@ -228,6 +238,11 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
);
|
||||
};
|
||||
const buildCheckbox = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
@@ -235,7 +250,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
];
|
||||
}
|
||||
return (
|
||||
<CheckboxGroup class="w-full" {...attrs}>
|
||||
<CheckboxGroup
|
||||
class="w-full"
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Checkbox key={index} value={item.value}>
|
||||
@@ -247,6 +267,11 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
);
|
||||
};
|
||||
const buildRadio = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
@@ -254,7 +279,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
];
|
||||
}
|
||||
return (
|
||||
<RadioGroup class="w-full" {...attrs}>
|
||||
<RadioGroup
|
||||
class="w-full"
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Radio key={index} value={item.value}>
|
||||
|
||||
@@ -22,7 +22,7 @@ const md = new MarkdownIt({
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`;
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`;
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
|
||||
} catch {}
|
||||
}
|
||||
return ``;
|
||||
|
||||
@@ -0,0 +1,875 @@
|
||||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import type { IOParameter, SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getFormDetail } from '#/api/bpm/form';
|
||||
import { getModelList } from '#/api/bpm/model';
|
||||
import { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import {
|
||||
CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE,
|
||||
CHILD_PROCESS_START_USER_EMPTY_TYPE,
|
||||
CHILD_PROCESS_START_USER_TYPE,
|
||||
ChildProcessMultiInstanceSourceTypeEnum,
|
||||
ChildProcessStartUserEmptyTypeEnum,
|
||||
ChildProcessStartUserTypeEnum,
|
||||
DELAY_TYPE,
|
||||
DelayTypeEnum,
|
||||
TIME_UNIT_TYPES,
|
||||
TimeUnitType,
|
||||
} from '../../consts';
|
||||
import {
|
||||
parseFormFields,
|
||||
useFormFields,
|
||||
useNodeName,
|
||||
useWatchNode,
|
||||
} from '../../helpers';
|
||||
import { convertTimeUnit } from './utils';
|
||||
|
||||
defineOptions({ name: 'ChildProcessNodeConfig' });
|
||||
|
||||
const props = defineProps<{
|
||||
flowNode: SimpleFlowNode;
|
||||
}>();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
header: true,
|
||||
closable: true,
|
||||
title: '',
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
},
|
||||
});
|
||||
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
/** 节点名称配置 */
|
||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||
useNodeName(BpmNodeTypeEnum.CHILD_PROCESS_NODE);
|
||||
// 激活的 Tab 标签页
|
||||
const activeTabName = ref('child');
|
||||
// 子流程表单配置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
async: [{ required: true, message: '是否异步不能为空', trigger: 'change' }],
|
||||
calledProcessDefinitionKey: [
|
||||
{ required: true, message: '子流程不能为空', trigger: 'change' },
|
||||
],
|
||||
skipStartUserNode: [
|
||||
{
|
||||
required: true,
|
||||
message: '是否自动跳过子流程发起节点不能为空',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
startUserType: [
|
||||
{ required: true, message: '子流程发起人不能为空', trigger: 'change' },
|
||||
],
|
||||
startUserEmptyType: [
|
||||
{
|
||||
required: true,
|
||||
message: '当子流程发起人为空时不能为空',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
startUserFormField: [
|
||||
{ required: true, message: '子流程发起人字段不能为空', trigger: 'change' },
|
||||
],
|
||||
timeoutEnable: [
|
||||
{ required: true, message: '超时设置是否开启不能为空', trigger: 'change' },
|
||||
],
|
||||
timeoutType: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
timeDuration: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
dateTime: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
multiInstanceEnable: [
|
||||
{ required: true, message: '多实例设置不能为空', trigger: 'change' },
|
||||
],
|
||||
sequential: [
|
||||
{ required: true, message: '是否串行不能为空', trigger: 'change' },
|
||||
],
|
||||
multiInstanceSourceType: [
|
||||
{ required: true, message: '实例数量不能为空', trigger: 'change' },
|
||||
],
|
||||
approveRatio: [
|
||||
{ required: true, message: '完成比例不能为空', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
type ChildProcessFormType = {
|
||||
approveRatio: number;
|
||||
async: boolean;
|
||||
calledProcessDefinitionKey: string;
|
||||
dateTime: string;
|
||||
inVariables?: IOParameter[];
|
||||
multiInstanceEnable: boolean;
|
||||
multiInstanceSource: string;
|
||||
multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum;
|
||||
outVariables?: IOParameter[];
|
||||
sequential: boolean;
|
||||
skipStartUserNode: boolean;
|
||||
startUserEmptyType: ChildProcessStartUserEmptyTypeEnum;
|
||||
startUserFormField: string;
|
||||
startUserType: ChildProcessStartUserTypeEnum;
|
||||
timeDuration: number;
|
||||
timeoutEnable: boolean;
|
||||
timeoutType: DelayTypeEnum;
|
||||
timeUnit: TimeUnitType;
|
||||
};
|
||||
|
||||
const configForm = ref<ChildProcessFormType>({
|
||||
async: false,
|
||||
calledProcessDefinitionKey: '',
|
||||
skipStartUserNode: false,
|
||||
inVariables: [],
|
||||
outVariables: [],
|
||||
startUserType: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
|
||||
startUserEmptyType:
|
||||
ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER,
|
||||
startUserFormField: '',
|
||||
timeoutEnable: false,
|
||||
timeoutType: DelayTypeEnum.FIXED_TIME_DURATION,
|
||||
timeDuration: 1,
|
||||
timeUnit: TimeUnitType.HOUR,
|
||||
dateTime: '',
|
||||
multiInstanceEnable: false,
|
||||
sequential: false,
|
||||
approveRatio: 100,
|
||||
multiInstanceSourceType:
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY,
|
||||
multiInstanceSource: '',
|
||||
});
|
||||
|
||||
const childProcessOptions = ref<any[]>([]);
|
||||
// 主流程表单字段选项
|
||||
const formFieldOptions = useFormFields();
|
||||
/** 子流程发起人表单可选项 : 只有用户选择组件字段才能被选择 */
|
||||
const startUserFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'UserSelect');
|
||||
});
|
||||
// 数字表单字段选项
|
||||
const digitalFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'inputNumber');
|
||||
});
|
||||
// 多选表单字段选项
|
||||
const multiFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter(
|
||||
(item) => item.type === 'select' || item.type === 'checkbox',
|
||||
);
|
||||
});
|
||||
const childFormFieldOptions = ref<any[]>([]);
|
||||
|
||||
/** 保存配置 */
|
||||
const saveConfig = async () => {
|
||||
activeTabName.value = 'child';
|
||||
if (!formRef.value) return false;
|
||||
|
||||
const valid = await formRef.value.validate().catch(() => false);
|
||||
if (!valid) return false;
|
||||
|
||||
const childInfo = childProcessOptions.value.find(
|
||||
(option) => option.key === configForm.value.calledProcessDefinitionKey,
|
||||
);
|
||||
|
||||
currentNode.value.name = nodeName.value!;
|
||||
if (currentNode.value.childProcessSetting) {
|
||||
// 1. 是否异步
|
||||
currentNode.value.childProcessSetting.async = configForm.value.async;
|
||||
// 2. 调用流程
|
||||
currentNode.value.childProcessSetting.calledProcessDefinitionKey =
|
||||
childInfo.key;
|
||||
currentNode.value.childProcessSetting.calledProcessDefinitionName =
|
||||
childInfo.name;
|
||||
// 3. 是否跳过发起人
|
||||
currentNode.value.childProcessSetting.skipStartUserNode =
|
||||
configForm.value.skipStartUserNode;
|
||||
// 4. 主->子变量
|
||||
currentNode.value.childProcessSetting.inVariables =
|
||||
configForm.value.inVariables;
|
||||
// 5. 子->主变量
|
||||
currentNode.value.childProcessSetting.outVariables =
|
||||
configForm.value.outVariables;
|
||||
// 6. 发起人设置
|
||||
currentNode.value.childProcessSetting.startUserSetting.type =
|
||||
configForm.value.startUserType;
|
||||
currentNode.value.childProcessSetting.startUserSetting.emptyType =
|
||||
configForm.value.startUserEmptyType;
|
||||
currentNode.value.childProcessSetting.startUserSetting.formField =
|
||||
configForm.value.startUserFormField;
|
||||
// 7. 超时设置
|
||||
currentNode.value.childProcessSetting.timeoutSetting = {
|
||||
enable: configForm.value.timeoutEnable,
|
||||
};
|
||||
if (configForm.value.timeoutEnable) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.type =
|
||||
configForm.value.timeoutType;
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
|
||||
getIsoTimeDuration();
|
||||
}
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
|
||||
configForm.value.dateTime;
|
||||
}
|
||||
}
|
||||
// 8. 多实例设置
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting = {
|
||||
enable: configForm.value.multiInstanceEnable,
|
||||
};
|
||||
if (configForm.value.multiInstanceEnable) {
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.sequential =
|
||||
configForm.value.sequential;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.approveRatio =
|
||||
configForm.value.approveRatio;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.sourceType =
|
||||
configForm.value.multiInstanceSourceType;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.source =
|
||||
configForm.value.multiInstanceSource;
|
||||
}
|
||||
}
|
||||
|
||||
currentNode.value.showText = `调用子流程:${childInfo.name}`;
|
||||
drawerApi.close();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 显示子流程节点配置, 由父组件传过来
|
||||
const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
|
||||
nodeName.value = node.name;
|
||||
if (node.childProcessSetting) {
|
||||
// 1. 是否异步
|
||||
configForm.value.async = node.childProcessSetting.async;
|
||||
// 2. 调用流程
|
||||
configForm.value.calledProcessDefinitionKey =
|
||||
node.childProcessSetting?.calledProcessDefinitionKey;
|
||||
// 3. 是否跳过发起人
|
||||
configForm.value.skipStartUserNode =
|
||||
node.childProcessSetting.skipStartUserNode;
|
||||
// 4. 主->子变量
|
||||
configForm.value.inVariables = node.childProcessSetting.inVariables ?? [];
|
||||
// 5. 子->主变量
|
||||
configForm.value.outVariables = node.childProcessSetting.outVariables ?? [];
|
||||
// 6. 发起人设置
|
||||
configForm.value.startUserType =
|
||||
node.childProcessSetting.startUserSetting.type;
|
||||
configForm.value.startUserEmptyType =
|
||||
node.childProcessSetting.startUserSetting.emptyType ??
|
||||
ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER;
|
||||
configForm.value.startUserFormField =
|
||||
node.childProcessSetting.startUserSetting.formField ?? '';
|
||||
// 7. 超时设置
|
||||
configForm.value.timeoutEnable =
|
||||
node.childProcessSetting.timeoutSetting.enable ?? false;
|
||||
if (configForm.value.timeoutEnable) {
|
||||
configForm.value.timeoutType =
|
||||
node.childProcessSetting.timeoutSetting.type ??
|
||||
DelayTypeEnum.FIXED_TIME_DURATION;
|
||||
// 固定时长
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
const strTimeDuration =
|
||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||
const parseTime = strTimeDuration.slice(2, -1);
|
||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||
}
|
||||
// 固定日期时间
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
configForm.value.dateTime =
|
||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||
}
|
||||
}
|
||||
// 8. 多实例设置
|
||||
configForm.value.multiInstanceEnable =
|
||||
node.childProcessSetting.multiInstanceSetting.enable ?? false;
|
||||
if (configForm.value.multiInstanceEnable) {
|
||||
configForm.value.sequential =
|
||||
node.childProcessSetting.multiInstanceSetting.sequential ?? false;
|
||||
configForm.value.approveRatio =
|
||||
node.childProcessSetting.multiInstanceSetting.approveRatio ?? 100;
|
||||
configForm.value.multiInstanceSourceType =
|
||||
node.childProcessSetting.multiInstanceSetting.sourceType ??
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY;
|
||||
configForm.value.multiInstanceSource =
|
||||
node.childProcessSetting.multiInstanceSetting.source ?? '';
|
||||
}
|
||||
}
|
||||
loadFormInfo();
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({ showChildProcessNodeConfig });
|
||||
|
||||
const addVariable = (arr?: IOParameter[]) => {
|
||||
arr?.push({
|
||||
source: '',
|
||||
target: '',
|
||||
});
|
||||
};
|
||||
|
||||
const deleteVariable = (index: number, arr?: IOParameter[]) => {
|
||||
arr?.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleCalledElementChange = () => {
|
||||
configForm.value.inVariables = [];
|
||||
configForm.value.outVariables = [];
|
||||
loadFormInfo();
|
||||
};
|
||||
|
||||
const loadFormInfo = async () => {
|
||||
const childInfo = childProcessOptions.value.find(
|
||||
(option) => option.key === configForm.value.calledProcessDefinitionKey,
|
||||
);
|
||||
if (!childInfo) return;
|
||||
|
||||
const formInfo = await getFormDetail(childInfo.formId);
|
||||
childFormFieldOptions.value = [];
|
||||
if (formInfo.fields) {
|
||||
formInfo.fields.forEach((fieldStr: string) => {
|
||||
parseFormFields(JSON.parse(fieldStr), childFormFieldOptions.value);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getIsoTimeDuration = () => {
|
||||
let strTimeDuration = 'PT';
|
||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||
}
|
||||
return strTimeDuration;
|
||||
};
|
||||
|
||||
const handleMultiInstanceSourceTypeChange = () => {
|
||||
configForm.value.multiInstanceSource = '';
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
childProcessOptions.value = await getModelList(undefined);
|
||||
} catch (error) {
|
||||
console.error('获取模型列表失败', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div v-else class="node-name">
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="lucide:edit-3" @click="clickIcon()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="configForm"
|
||||
:label-wrap="true"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:rules="formRules"
|
||||
>
|
||||
<FormItem
|
||||
label="是否异步执行"
|
||||
name="async"
|
||||
label-align="left"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.async"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="选择子流程" name="calledProcessDefinitionKey">
|
||||
<Select
|
||||
v-model:value="configForm.calledProcessDefinitionKey"
|
||||
allow-clear
|
||||
@change="handleCalledElementChange"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(item, index) in childProcessOptions"
|
||||
:key="index"
|
||||
:value="item.key"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="是否自动跳过子流程发起节点"
|
||||
name="skipStartUserNode"
|
||||
label-align="left"
|
||||
:label-col="{ span: 12 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.skipStartUserNode"
|
||||
checked-children="跳过"
|
||||
un-checked-children="不跳过"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="主→子变量传递" name="inVariables">
|
||||
<div
|
||||
class="flex"
|
||||
v-for="(item, index) in configForm.inVariables"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['inVariables', index, 'source']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '变量不能为空',
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<Select class="!w-40" v-model:value="item.source">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in formFieldOptions"
|
||||
:key="fIdx"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['inVariables', index, 'target']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '变量不能为空',
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<Select class="!w-40" v-model:value="item.target">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in childFormFieldOptions"
|
||||
:key="fIdx"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-1 flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
icon="lucide:trash-2"
|
||||
:size="18"
|
||||
class="cursor-pointer text-red-500"
|
||||
@click="deleteVariable(index, configForm.inVariables)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addVariable(configForm.inVariables)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.async === false"
|
||||
label="子→主变量传递"
|
||||
name="outVariables"
|
||||
>
|
||||
<div
|
||||
class="flex"
|
||||
v-for="(item, index) in configForm.outVariables"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['outVariables', index, 'source']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '变量不能为空',
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<Select class="!w-40" v-model:value="item.source">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in childFormFieldOptions"
|
||||
:key="fIdx"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['outVariables', index, 'target']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '变量不能为空',
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<Select class="!w-40" v-model:value="item.target">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in formFieldOptions"
|
||||
:key="fIdx"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-1 flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
icon="lucide:trash-2"
|
||||
:size="18"
|
||||
class="cursor-pointer text-red-500"
|
||||
@click="deleteVariable(index, configForm.outVariables)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addVariable(configForm.outVariables)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem label="子流程发起人" name="startUserType">
|
||||
<RadioGroup v-model:value="configForm.startUserType">
|
||||
<Radio
|
||||
v-for="item in CHILD_PROCESS_START_USER_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM
|
||||
"
|
||||
label="子流程发起人字段"
|
||||
name="startUserFormField"
|
||||
>
|
||||
<Select v-model:value="configForm.startUserFormField" allow-clear>
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in startUserFormFieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM
|
||||
"
|
||||
label="当子流程发起人为空时"
|
||||
name="startUserEmptyType"
|
||||
>
|
||||
<RadioGroup v-model:value="configForm.startUserEmptyType">
|
||||
<Radio
|
||||
v-for="item in CHILD_PROCESS_START_USER_EMPTY_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
|
||||
<Divider>超时设置</Divider>
|
||||
<FormItem
|
||||
label="启用开关"
|
||||
name="timeoutEnable"
|
||||
label-align="left"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.timeoutEnable"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
</FormItem>
|
||||
<div v-if="configForm.timeoutEnable">
|
||||
<FormItem name="timeoutType">
|
||||
<RadioGroup v-model:value="configForm.timeoutType">
|
||||
<RadioButton
|
||||
v-for="item in DELAY_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center"> 当超过 </span>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormItem name="timeDuration">
|
||||
<InputNumber
|
||||
class="w-24"
|
||||
v-model:value="configForm.timeDuration"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col>
|
||||
<Select v-model:value="configForm.timeUnit" class="w-24">
|
||||
<SelectOption
|
||||
v-for="item in TIME_UNIT_TYPES"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">后进入下一节点</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.timeoutType === DelayTypeEnum.FIXED_DATE_TIME"
|
||||
name="dateTime"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<DatePicker
|
||||
class="mr-2"
|
||||
v-model:value="configForm.dateTime"
|
||||
type="date"
|
||||
show-time
|
||||
placeholder="请选择日期和时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">
|
||||
后进入下一节点
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<Divider>多实例设置</Divider>
|
||||
<FormItem
|
||||
label="启用开关"
|
||||
label-align="left"
|
||||
name="multiInstanceEnable"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.multiInstanceEnable"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
</FormItem>
|
||||
<div v-if="configForm.multiInstanceEnable">
|
||||
<FormItem
|
||||
name="sequential"
|
||||
label="是否串行"
|
||||
label-align="left"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.sequential"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="approveRatio"
|
||||
label="完成比例(%)"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="configForm.approveRatio"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="10"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="multiInstanceSourceType"
|
||||
label="实例数量"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
>
|
||||
<Select
|
||||
v-model:value="configForm.multiInstanceSourceType"
|
||||
@change="handleMultiInstanceSourceTypeChange"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="固定数量"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '固定数量不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="configForm.multiInstanceSource"
|
||||
:min="1"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="数字表单"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '数字表单字段不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<Select v-model:value="configForm.multiInstanceSource">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in digitalFormFieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="多选表单"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '多选表单字段不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<Select v-model:value="configForm.multiInstanceSource">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in multiFormFieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -62,9 +62,15 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
|
||||
// 不用暴露啊,用 useVbenModal 就可以了
|
||||
defineExpose({ modalApi });
|
||||
/**
|
||||
* 打开条件配置弹窗,不暴露 modalApi 给父组件
|
||||
*/
|
||||
function openModal(conditionObj: any) {
|
||||
modalApi.setData(conditionObj).open();
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-1/2">
|
||||
|
||||
@@ -200,8 +200,8 @@ function addFormSettingCondition(
|
||||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
// 使用modalApi来打开模态框并传递数据
|
||||
conditionDialog.modalApi.setData(formSetting).open();
|
||||
// 打开模态框并传递数据
|
||||
conditionDialog.openModal(formSetting);
|
||||
}
|
||||
|
||||
/** 删除条件配置 */
|
||||
@@ -215,8 +215,8 @@ function openFormSettingCondition(
|
||||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
// 使用 modalApi 来打开模态框并传递数据
|
||||
conditionDialog.modalApi.setData(formSetting).open();
|
||||
// 打开模态框并传递数据
|
||||
conditionDialog.openModal(formSetting);
|
||||
}
|
||||
|
||||
/** 处理条件配置保存 */
|
||||
|
||||
@@ -601,7 +601,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-1/3">
|
||||
<Drawer class="w-2/5">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<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 { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import { NODE_DEFAULT_TEXT } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import ChildProcessNodeConfig from '../nodes-config/child-process-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'ChildProcessNode' });
|
||||
|
||||
const props = defineProps<{
|
||||
flowNode: SimpleFlowNode;
|
||||
}>();
|
||||
|
||||
/** 定义事件,更新父组件。 */
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
|
||||
/** 监控节点的变化 */
|
||||
const currentNode = useWatchNode(props);
|
||||
|
||||
/** 节点名称编辑 */
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.CHILD_PROCESS_NODE,
|
||||
);
|
||||
|
||||
// 节点配置 Ref
|
||||
const nodeConfigRef = ref();
|
||||
|
||||
/** 打开节点配置 */
|
||||
const openNodeConfig = () => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeConfigRef.value.showChildProcessNodeConfig(currentNode.value);
|
||||
};
|
||||
|
||||
/** 删除节点。更新当前节点为孩子节点 */
|
||||
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 ${currentNode.childProcessSetting?.async === true ? 'async-child-process' : 'child-process'}`"
|
||||
>
|
||||
<span
|
||||
:class="`iconfont ${currentNode.childProcessSetting?.async === true ? 'icon-async-child-process' : 'icon-child-process'}`"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
ref="inputRef"
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="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(BpmNodeTypeEnum.CHILD_PROCESS_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="lucide:circle-x"
|
||||
:size="18"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<ChildProcessNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeConfigRef"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -16,25 +16,6 @@ import { NODE_DEFAULT_TEXT } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue';
|
||||
import TaskListModal from './modules/task-list-modal.vue';
|
||||
// // 使用useVbenVxeGrid
|
||||
// const [Grid, gridApi] = useVbenVxeGrid({
|
||||
// gridOptions: {
|
||||
// columns: columns.value,
|
||||
// keepSource: true,
|
||||
// border: true,
|
||||
// height: 'auto',
|
||||
// data: selectTasks.value,
|
||||
// rowConfig: {
|
||||
// keyField: 'id',
|
||||
// },
|
||||
// pagerConfig: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// toolbarConfig: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// } as VxeTableGridOptions<any>,
|
||||
// });
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'UserTaskNode' });
|
||||
@@ -155,7 +136,7 @@ function findReturnTaskNodes(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<!-- 添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SimpleFlowNode } from '../consts';
|
||||
import { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import { useWatchNode } from '../helpers';
|
||||
import ChildProcessNode from './nodes/child-process-node.vue';
|
||||
import CopyTaskNode from './nodes/copy-task-node.vue';
|
||||
import DelayTimerNode from './nodes/delay-timer-node.vue';
|
||||
import EndEventNode from './nodes/end-event-node.vue';
|
||||
@@ -140,11 +141,13 @@ function recursiveFindParentNode(
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/>
|
||||
<!-- 子流程节点 -->
|
||||
<!-- <ChildProcessNode
|
||||
v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
|
||||
<ChildProcessNode
|
||||
v-if="
|
||||
currentNode && currentNode.type === BpmNodeTypeEnum.CHILD_PROCESS_NODE
|
||||
"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 递归显示孩子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="currentNode && currentNode.childNode"
|
||||
|
||||
@@ -126,7 +126,8 @@ function updateModel() {
|
||||
name: '发起人',
|
||||
type: BpmNodeTypeEnum.START_USER_NODE,
|
||||
id: NodeId.START_USER_NODE_ID,
|
||||
showText: '默认配置',
|
||||
// 默认为空,需要进行配置
|
||||
showText: '',
|
||||
childNode: {
|
||||
id: NodeId.END_EVENT_NODE_ID,
|
||||
name: '结束',
|
||||
|
||||
@@ -852,7 +852,7 @@ export const CHILD_PROCESS_START_USER_TYPE = [
|
||||
label: '同主流程发起人',
|
||||
value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
|
||||
},
|
||||
{ label: '表单', value: ChildProcessStartUserTypeEnum.FROM_FORM },
|
||||
{ label: '从表单中获取', value: ChildProcessStartUserTypeEnum.FROM_FORM },
|
||||
];
|
||||
|
||||
export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [
|
||||
|
||||
@@ -60,39 +60,14 @@ function isIfShow(action: ActionItem): boolean {
|
||||
|
||||
/** 处理按钮 actions */
|
||||
const getActions = computed(() => {
|
||||
return (props.actions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
type: action.type || 'link',
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
return (props.actions || []).filter((action: ActionItem) => isIfShow(action));
|
||||
});
|
||||
|
||||
/** 处理下拉菜单 actions */
|
||||
const getDropdownList = computed(() => {
|
||||
return (props.dropDownActions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem, index: number) => {
|
||||
const { label, popConfirm } = action;
|
||||
const processedAction = { ...action };
|
||||
delete processedAction.icon;
|
||||
return {
|
||||
...processedAction,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
text: label,
|
||||
divider:
|
||||
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||
};
|
||||
});
|
||||
return (props.dropDownActions || []).filter((action: ActionItem) =>
|
||||
isIfShow(action),
|
||||
);
|
||||
});
|
||||
|
||||
/** Space 组件的 size */
|
||||
@@ -103,18 +78,27 @@ const spaceSize = computed(() => {
|
||||
});
|
||||
|
||||
/** 获取 PopConfirm 属性 */
|
||||
function getPopConfirmProps(attrs: PopConfirm) {
|
||||
const originAttrs: any = { ...attrs };
|
||||
delete originAttrs.icon;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
delete originAttrs.confirm;
|
||||
function getPopConfirmProps(popConfirm: PopConfirm) {
|
||||
if (!popConfirm) return {};
|
||||
|
||||
const attrs: Record<string, any> = {};
|
||||
|
||||
// 复制基本属性,排除函数
|
||||
Object.keys(popConfirm).forEach((key) => {
|
||||
if (key !== 'confirm' && key !== 'cancel' && key !== 'icon') {
|
||||
attrs[key] = popConfirm[key as keyof PopConfirm];
|
||||
}
|
||||
});
|
||||
|
||||
// 单独处理事件函数
|
||||
if (popConfirm.confirm && isFunction(popConfirm.confirm)) {
|
||||
attrs.onConfirm = popConfirm.confirm;
|
||||
}
|
||||
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||
originAttrs.onCancel = attrs.cancel;
|
||||
delete originAttrs.cancel;
|
||||
if (popConfirm.cancel && isFunction(popConfirm.cancel)) {
|
||||
attrs.onCancel = popConfirm.cancel;
|
||||
}
|
||||
return originAttrs;
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/** 获取 Button 属性 */
|
||||
@@ -146,6 +130,13 @@ function handleMenuClick(e: any) {
|
||||
function getActionKey(action: ActionItem, index: number) {
|
||||
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||
}
|
||||
|
||||
/** 处理按钮点击 */
|
||||
function handleButtonClick(action: ActionItem) {
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -172,7 +163,10 @@ function getActionKey(action: ActionItem, index: number) {
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<Button
|
||||
v-bind="getButtonProps(action)"
|
||||
@click="handleButtonClick(action)"
|
||||
>
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
@@ -184,7 +178,7 @@ function getActionKey(action: ActionItem, index: number) {
|
||||
|
||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||
<slot name="more">
|
||||
<Button :type="getDropdownList[0]?.type">
|
||||
<Button type="link">
|
||||
<template #icon>
|
||||
{{ $t('page.action.more') }}
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
@@ -213,7 +207,7 @@ function getActionKey(action: ActionItem, index: number) {
|
||||
>
|
||||
<IconifyIcon v-if="action.icon" :icon="action.icon" />
|
||||
<span :class="action.icon ? 'ml-1' : ''">
|
||||
{{ action.text }}
|
||||
{{ action.label }}
|
||||
</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
@@ -124,12 +125,6 @@ async function handleVerifySuccess({ captchaVerification }: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
||||
@@ -115,7 +115,7 @@ async function handelUpload({
|
||||
所属岗位
|
||||
</div>
|
||||
</template>
|
||||
{{ profile.posts.map((post) => post.name).join(',') }}
|
||||
{{ profile.posts && profile.posts.length > 0 ? profile.posts.map(post => post.name).join(',') : '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem>
|
||||
<template #label>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import { Button, Card, Image, message } from 'ant-design-vue';
|
||||
|
||||
@@ -149,13 +150,6 @@ async function bindSocial() {
|
||||
window.history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续搞到 util 里;
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
bindSocial();
|
||||
|
||||
@@ -2,7 +2,12 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
@@ -97,7 +102,15 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
// TODO 创建时间 等通用方法完善后加
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ defineExpose({ validate });
|
||||
<div
|
||||
v-for="user in selectedStartUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
@@ -346,7 +346,9 @@ defineExpose({ validate });
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@@ -371,10 +373,12 @@ defineExpose({ validate });
|
||||
<div
|
||||
v-for="dept in selectedStartDepts"
|
||||
:key="dept.id"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 shadow-sm hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 shadow-sm hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<IconifyIcon icon="lucide:building" class="size-6 px-1" />
|
||||
{{ dept.name }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ dept.name }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@@ -398,7 +402,7 @@ defineExpose({ validate });
|
||||
<div
|
||||
v-for="user in selectedManagerUsers"
|
||||
:key="user.id"
|
||||
class="hover:bg-primary-500 relative flex h-9 items-center rounded-full pr-2"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
@@ -409,7 +413,9 @@ defineExpose({ validate });
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@@ -432,6 +438,7 @@ defineExpose({ validate });
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModalComp
|
||||
class="w-3/5"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
@@ -441,6 +448,7 @@ defineExpose({ validate });
|
||||
/>
|
||||
<!-- 部门选择对话框 -->
|
||||
<DeptSelectModalComp
|
||||
class="w-3/5"
|
||||
title="发起人部门选择"
|
||||
:check-strictly="true"
|
||||
@confirm="handleDeptSelectConfirm"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
@@ -146,6 +146,11 @@ function handleQuery() {
|
||||
// 如果没有搜索关键字,恢复所有数据
|
||||
isSearching.value = false;
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value;
|
||||
|
||||
// 恢复到第一个可用分类
|
||||
if (availableCategories.value.length > 0) {
|
||||
activeCategory.value = availableCategories.value[0].code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +183,8 @@ const processDefinitionGroup = computed(() => {
|
||||
});
|
||||
|
||||
/** 通过分类 code 获取对应的名称 */
|
||||
function getCategoryName(categoryCode: string) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function _getCategoryName(categoryCode: string) {
|
||||
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)
|
||||
?.name;
|
||||
}
|
||||
@@ -215,11 +221,28 @@ const availableCategories = computed(() => {
|
||||
});
|
||||
|
||||
/** 获取 tab 的位置 */
|
||||
|
||||
const tabPosition = computed(() => {
|
||||
return window.innerWidth < 768 ? 'top' : 'left';
|
||||
});
|
||||
|
||||
/** 监听可用分类变化,自动设置正确的活动分类 */
|
||||
watch(
|
||||
availableCategories,
|
||||
(newCategories) => {
|
||||
if (newCategories.length > 0) {
|
||||
// 如果当前活动分类不在可用分类中,切换到第一个可用分类
|
||||
const currentCategoryExists = newCategories.some(
|
||||
(category: BpmCategoryApi.Category) =>
|
||||
category.code === activeCategory.value,
|
||||
);
|
||||
if (!currentCategoryExists) {
|
||||
activeCategory.value = newCategories[0].code;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
@@ -240,10 +263,10 @@ onMounted(() => {
|
||||
:loading="loading"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="flex items-end">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<InputSearch
|
||||
v-model:value="searchName"
|
||||
class="!w-50% mb-4"
|
||||
class="!w-50%"
|
||||
placeholder="请输入流程名称检索"
|
||||
allow-clear
|
||||
@input="handleQuery"
|
||||
@@ -259,15 +282,15 @@ onMounted(() => {
|
||||
:key="category.code"
|
||||
:tab="category.name"
|
||||
>
|
||||
<Row :gutter="[16, 16]">
|
||||
<Row :gutter="[16, 16]" :wrap="true">
|
||||
<Col
|
||||
v-for="definition in processDefinitionGroup[category.code]"
|
||||
:key="definition.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="4"
|
||||
:lg="8"
|
||||
:xl="6"
|
||||
@click="handleSelect(definition)"
|
||||
>
|
||||
<Card
|
||||
@@ -278,10 +301,10 @@ onMounted(() => {
|
||||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<!-- TODO @ziye:icon、name 会告警~~ -->
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
@@ -290,16 +313,14 @@ onMounted(() => {
|
||||
/>
|
||||
|
||||
<div v-else class="flow-icon flex-shrink-0">
|
||||
<Tooltip :title="definition.name">
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="ml-3 flex-1 truncate text-base">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
:title="`${definition.name}`"
|
||||
:title="`${definition.description}`"
|
||||
>
|
||||
{{ definition.name }}
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ApiAttrs } from '@form-create/ant-design-vue/types/config';
|
||||
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
BpmModelFormType,
|
||||
BpmModelType,
|
||||
BpmNodeIdEnum,
|
||||
BpmNodeTypeEnum,
|
||||
decodeFields,
|
||||
setConfAndFields2,
|
||||
} from '#/utils';
|
||||
@@ -41,22 +39,6 @@ interface UserTask {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ApprovalNodeInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
candidateStrategy: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: Array<{
|
||||
avatar: string;
|
||||
id: number;
|
||||
nickname: string;
|
||||
}>;
|
||||
endTime?: Date;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
status: number;
|
||||
tasks: any[];
|
||||
}
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCreateForm' });
|
||||
|
||||
const props = defineProps({
|
||||
@@ -80,7 +62,7 @@ const detailForm = ref<ProcessFormData>({
|
||||
value: {},
|
||||
});
|
||||
|
||||
const fApi = ref<ApiAttrs>();
|
||||
const fApi = ref<any>();
|
||||
const startUserSelectTasks = ref<UserTask[]>([]);
|
||||
const startUserSelectAssignees = ref<Record<string, string[]>>({});
|
||||
const tempStartUserSelectAssignees = ref<Record<string, string[]>>({});
|
||||
@@ -88,9 +70,8 @@ const bpmnXML = ref<string | undefined>(undefined);
|
||||
const simpleJson = ref<string | undefined>(undefined);
|
||||
const timelineRef = ref<any>();
|
||||
const activeTab = ref('form');
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]);
|
||||
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]);
|
||||
const processInstanceStartLoading = ref(false);
|
||||
|
||||
/** 提交按钮 */
|
||||
async function submitForm() {
|
||||
if (!fApi.value || !props.selectProcessDefinition) {
|
||||
@@ -127,7 +108,6 @@ async function submitForm() {
|
||||
|
||||
await router.push({ path: '/bpm/task/my' });
|
||||
} catch (error) {
|
||||
message.error('发起流程失败');
|
||||
console.error('发起流程失败:', error);
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
@@ -219,7 +199,7 @@ async function getApprovalDetail(row: {
|
||||
}
|
||||
|
||||
// 获取审批节点
|
||||
activityNodes.value = data.activityNodes as unknown as ApprovalNodeInfo[];
|
||||
activityNodes.value = data.activityNodes;
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = (data.activityNodes?.filter(
|
||||
@@ -330,7 +310,12 @@ defineExpose({ initProcessInfo });
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane tab="流程图" key="flow" class="flex flex-1 overflow-hidden">
|
||||
<Tabs.TabPane
|
||||
tab="流程图"
|
||||
key="flow"
|
||||
class="flex flex-1 overflow-hidden"
|
||||
:force-render="true"
|
||||
>
|
||||
<div class="w-full">
|
||||
<ProcessInstanceSimpleViewer
|
||||
:simple-json="simpleJson"
|
||||
@@ -343,7 +328,12 @@ defineExpose({ initProcessInfo });
|
||||
<template #actions>
|
||||
<template v-if="activeTab === 'form'">
|
||||
<Space wrap class="flex w-full justify-center">
|
||||
<Button plain type="primary" @click="submitForm">
|
||||
<Button
|
||||
plain
|
||||
type="primary"
|
||||
@click="submitForm"
|
||||
:loading="processInstanceStartLoading"
|
||||
>
|
||||
<IconifyIcon icon="lucide:check" />
|
||||
发起
|
||||
</Button>
|
||||
|
||||
@@ -657,8 +657,7 @@ async function validateNormalForm() {
|
||||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
if (field && variables[field])
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
return variables;
|
||||
}
|
||||
@@ -736,6 +735,7 @@ defineExpose({ loadTodoTask });
|
||||
<ProcessInstanceTimeline
|
||||
:activity-nodes="nextAssigneesActivityNode"
|
||||
:show-status-icon="false"
|
||||
:use-next-assignees="true"
|
||||
@select-user-confirm="selectNextAssigneesConfirm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
|
||||
@@ -19,13 +20,15 @@ import {
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activityNodes: BpmProcessInstanceApi.ApprovalNodeInfo[]; // 审批节点信息
|
||||
showStatusIcon?: boolean; // 是否显示头像右下角状态图标
|
||||
useNextAssignees?: boolean; // 是否用于下一个节点审批人选择
|
||||
}>(),
|
||||
{
|
||||
showStatusIcon: true, // 默认值为 true
|
||||
useNextAssignees: false, // 默认值为 false
|
||||
},
|
||||
);
|
||||
|
||||
@@ -102,7 +105,7 @@ const nodeTypeSvgMap = {
|
||||
color: '#14bb83',
|
||||
icon: 'icon-park-outline:tree-diagram',
|
||||
},
|
||||
};
|
||||
} as Record<BpmNodeTypeEnum, { color: string; icon: string }>;
|
||||
|
||||
// 只有状态是 -1、0、1 才展示头像右小角状态小icon
|
||||
const onlyStatusIconShow = [-1, 0, 1];
|
||||
@@ -150,21 +153,27 @@ function getApprovalNodeTime(node: BpmProcessInstanceApi.ApprovalNodeInfo) {
|
||||
}
|
||||
|
||||
// 选择自定义审批人
|
||||
const userSelectFormRef = ref();
|
||||
const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
||||
connectedComponent: UserSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const selectedActivityNodeId = ref<string>();
|
||||
const customApproveUsers = ref<Record<string, any[]>>({}); // key:activityId,value:用户列表
|
||||
|
||||
// 打开选择用户弹窗
|
||||
const handleSelectUser = (activityId: string, selectedList: any[]) => {
|
||||
selectedActivityNodeId.value = activityId;
|
||||
userSelectFormRef.value.open(
|
||||
selectedList?.length ? selectedList.map((item) => item.id) : [],
|
||||
);
|
||||
userSelectModalApi
|
||||
.setData({ userIds: selectedList.map((item) => item.id) })
|
||||
.open();
|
||||
};
|
||||
|
||||
// 选择用户完成
|
||||
const selectedUsers = ref<number[]>([]);
|
||||
function handleUserSelectConfirm(userList: any[]) {
|
||||
if (!selectedActivityNodeId.value) {
|
||||
return;
|
||||
}
|
||||
customApproveUsers.value[selectedActivityNodeId.value] = userList || [];
|
||||
|
||||
emit('selectUserConfirm', selectedActivityNodeId.value, userList);
|
||||
@@ -189,8 +198,9 @@ function shouldShowCustomUserSelect(
|
||||
isEmpty(activity.candidateUsers) &&
|
||||
(BpmCandidateStrategyEnum.START_USER_SELECT ===
|
||||
activity.candidateStrategy ||
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy)
|
||||
(BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy &&
|
||||
props.useNextAssignees))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,8 +299,12 @@ function handleUserSelectCancel() {
|
||||
type="primary"
|
||||
size="middle"
|
||||
ghost
|
||||
class="flex items-center justify-center"
|
||||
@click="
|
||||
handleSelectUser(activity.id, customApproveUsers[activity.id])
|
||||
handleSelectUser(
|
||||
activity.id,
|
||||
customApproveUsers[activity.id] ?? [],
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -330,9 +344,7 @@ function handleUserSelectCancel() {
|
||||
v-if="task.assigneeUser || task.ownerUser"
|
||||
>
|
||||
<!-- 信息:头像昵称 -->
|
||||
<div
|
||||
class="relative flex h-8 items-center rounded-3xl bg-gray-100 pr-2 dark:bg-gray-600"
|
||||
>
|
||||
<div class="relative flex h-8 items-center rounded-3xl pr-2">
|
||||
<template
|
||||
v-if="
|
||||
task.assigneeUser?.avatar || task.assigneeUser?.nickname
|
||||
@@ -414,7 +426,7 @@ function handleUserSelectCancel() {
|
||||
<div
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="relative flex h-8 items-center rounded-3xl bg-gray-100 pr-2 dark:bg-gray-600"
|
||||
class="relative flex h-8 items-center rounded-3xl pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-1"
|
||||
@@ -447,8 +459,8 @@ function handleUserSelectCancel() {
|
||||
</Timeline>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
ref="userSelectFormRef"
|
||||
<UserSelectModalComp
|
||||
class="w-3/5"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { ref } from 'vue';
|
||||
@@ -21,7 +21,7 @@ import { useAssignMenuFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const menuTree = ref<SystemDeptApi.Dept[]>([]); // 菜单树
|
||||
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||
const menuLoading = ref(false); // 加载菜单列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
@@ -90,7 +90,7 @@ async function loadMenuTree() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuList();
|
||||
menuTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
@@ -27,7 +27,7 @@ const getTitle = computed(() => {
|
||||
? $t('ui.actionTitle.edit', ['套餐'])
|
||||
: $t('ui.actionTitle.create', ['套餐']);
|
||||
});
|
||||
const menuTree = ref<SystemDeptApi.Dept[]>([]); // 菜单树
|
||||
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||
const menuLoading = ref(false); // 加载菜单列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
@@ -95,7 +95,7 @@ async function loadMenuTree() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuList();
|
||||
menuTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
@@ -134,7 +134,6 @@ function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-6">
|
||||
<template #menuIds="slotProps">
|
||||
<!-- TODO @芋艿:可优化,使用 antd 的 tree?原因是,更原生 -->
|
||||
<VbenTree
|
||||
class="max-h-96 overflow-y-auto"
|
||||
:loading="menuLoading"
|
||||
|
||||
Reference in New Issue
Block a user