!172 !170 Merge remote-tracking branch 'yudao/dev' into dev

Merge pull request !172 from xingyu/dev
This commit is contained in:
xingyu
2025-07-14 03:27:17 +00:00
committed by Gitee
75 changed files with 5090 additions and 3133 deletions

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -35,7 +35,7 @@ export namespace BpmProcessInstanceApi {
candidateStrategy?: BpmCandidateStrategyEnum;
candidateUsers?: User[];
endTime?: Date;
id: number;
id: string;
name: string;
nodeType: BpmNodeTypeEnum;
startTime?: Date;

View File

@@ -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}>

View File

@@ -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 ``;

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);
}
/** 处理条件配置保存 */

View File

@@ -601,7 +601,7 @@ onMounted(() => {
});
</script>
<template>
<Drawer class="w-1/3">
<Drawer class="w-2/5">
<template #title>
<div class="config-header">
<Input

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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: '结束',

View File

@@ -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 = [

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -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();

View File

@@ -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,
},
},
];
}

View File

@@ -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"

View File

@@ -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 @ziyeiconname 会告警~~ -->
<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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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[]>>({}); // keyactivityIdvalue用户列表
// 打开选择用户弹窗
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="选择用户"

View File

@@ -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;
}

View File

@@ -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"