feat: [bpm][antd] bpmn 设计器时间事件定义优化

This commit is contained in:
jason
2025-12-13 00:39:00 +08:00
parent d50b9fae60
commit 8df5fbc843
3 changed files with 211 additions and 207 deletions

View File

@@ -8,8 +8,10 @@ import {
Input, Input,
InputNumber, InputNumber,
Radio, Radio,
TabPane,
Tabs, Tabs,
} from 'ant-design-vue'; } from 'ant-design-vue';
import dayjs from 'dayjs';
const props = defineProps({ const props = defineProps({
value: { value: {
@@ -41,7 +43,7 @@ const cronFieldList = [
]; ];
const activeField = ref('second'); const activeField = ref('second');
const cronMode = ref({ const cronMode = ref({
second: 'appoint', second: 'every',
minute: 'every', minute: 'every',
hour: 'every', hour: 'every',
day: 'every', day: 'every',
@@ -50,7 +52,7 @@ const cronMode = ref({
year: 'every', year: 'every',
}); });
const cronAppoint = ref({ const cronAppoint = ref({
second: ['00', '01'], second: [],
minute: [], minute: [],
hour: [], hour: [],
day: [], day: [],
@@ -107,103 +109,156 @@ watch(
const isoStr = ref(''); const isoStr = ref('');
const repeat = ref(1); const repeat = ref(1);
const isoDate = ref(''); const isoDate = ref('');
const durationUnits = [
{ key: 'Y', label: '年', presets: [1, 2, 3, 4] },
{ key: 'M', label: '月', presets: [1, 2, 3, 4] },
{ key: 'D', label: '天', presets: [1, 2, 3, 4] },
{ key: 'H', label: '时', presets: [4, 8, 12, 24] },
{ key: 'm', label: '分', presets: [5, 10, 30, 50] },
{ key: 'S', label: '秒', presets: [5, 10, 30, 50] },
];
const durationCustom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' });
const isoDuration = ref(''); const isoDuration = ref('');
function setDuration(type, val) {
// 组装ISO 8601字符串 function setDuration(key, val) {
let d = isoDuration.value; durationCustom.value[key] = !val || Number.isNaN(val) ? '' : val;
if (d.includes(type)) { updateDurationStr();
d = d.replace(new RegExp(String.raw`\d+${type}`), val + type); }
} else {
d += val + type; function updateDurationStr() {
} let str = 'P';
isoDuration.value = d; str += durationCustom.value.Y ? `${durationCustom.value.Y}Y` : '';
str += durationCustom.value.M ? `${durationCustom.value.M}M` : '';
str += durationCustom.value.D ? `${durationCustom.value.D}D` : '';
str +=
durationCustom.value.H || durationCustom.value.m || durationCustom.value.S
? 'T'
: '';
str += durationCustom.value.H ? `${durationCustom.value.H}H` : '';
str += durationCustom.value.m ? `${durationCustom.value.m}M` : '';
str += durationCustom.value.S ? `${durationCustom.value.S}S` : '';
isoDuration.value = str === 'P' ? '' : str;
updateIsoStr(); updateIsoStr();
} }
function updateIsoStr() { function updateIsoStr() {
let str = `R${repeat.value}`; let str = `R${repeat.value}`;
if (isoDate.value) if (isoDate.value) {
str += `/${ const dateStr =
typeof isoDate.value === 'string' typeof isoDate.value === 'string'
? isoDate.value ? isoDate.value
: new Date(isoDate.value).toISOString() : isoDate.value.toISOString();
}`; str += `/${dateStr}`;
}
if (isoDuration.value) str += `/${isoDuration.value}`; if (isoDuration.value) str += `/${isoDuration.value}`;
isoStr.value = str; isoStr.value = str;
if (tab.value === 'iso') emit('change', isoStr.value); if (tab.value === 'iso') emit('change', isoStr.value);
} }
watch([repeat, isoDate, isoDuration], updateIsoStr); watch([repeat, isoDate], updateIsoStr);
watch(durationCustom, updateDurationStr, { deep: true });
watch( watch(
() => props.value, () => props.value,
(val) => { (val) => {
if (!val) return; if (!val) return;
if (tab.value === 'cron') cronStr.value = val; // 自动检测格式以R开头的是ISO 8601格式否则是CRON表达式
if (tab.value === 'iso') isoStr.value = val; if (val.startsWith('R')) {
tab.value = 'iso';
isoStr.value = val;
// 解析ISO格式R{repeat}/{date}/{duration}
const parts = val.split('/');
if (parts[0]) {
const repeatMatch = parts[0].match(/^R(\d+)$/);
if (repeatMatch) repeat.value = Number.parseInt(repeatMatch[1], 10);
}
// 解析date部分ISO 8601日期时间格式
const datePart = parts.find(
(p) => p.includes('T') && !p.startsWith('P') && !p.startsWith('R'),
);
if (datePart) {
isoDate.value = dayjs(datePart);
}
// 解析duration部分
const durationPart = parts.find((p) => p.startsWith('P'));
if (durationPart) {
const match = durationPart.match(
/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/,
);
if (match) {
durationCustom.value.Y = match[1] || '';
durationCustom.value.M = match[2] || '';
durationCustom.value.D = match[3] || '';
durationCustom.value.H = match[4] || '';
durationCustom.value.m = match[5] || '';
durationCustom.value.S = match[6] || '';
isoDuration.value = durationPart;
}
}
} else {
tab.value = 'cron';
cronStr.value = val;
}
}, },
{ immediate: true }, { immediate: true },
); );
</script> </script>
<template> <template>
<Tabs v-model:active-key="tab"> <Tabs v-model:active-key="tab">
<Tabs.TabPane key="cron" tab="CRON表达式"> <TabPane key="cron" tab="CRON表达式">
<div style="margin-bottom: 10px"> <div class="mb-2.5">
<Input <Input
v-model:value="cronStr" v-model:value="cronStr"
readonly readonly
style="width: 400px; font-weight: bold" class="w-[400px] font-bold"
key="cronStr" key="cronStr"
/> />
</div> </div>
<div style="display: flex; gap: 8px; margin-bottom: 8px"> <div class="mb-2 flex gap-2">
<Input <Input
v-model:value="fields.second" v-model:value="fields.second"
placeholder="秒" placeholder="秒"
style="width: 80px" class="w-20"
key="second" key="second"
/> />
<Input <Input
v-model:value="fields.minute" v-model:value="fields.minute"
placeholder="分" placeholder="分"
style="width: 80px" class="w-20"
key="minute" key="minute"
/> />
<Input <Input
v-model:value="fields.hour" v-model:value="fields.hour"
placeholder="时" placeholder="时"
style="width: 80px" class="w-20"
key="hour" key="hour"
/> />
<Input <Input
v-model:value="fields.day" v-model:value="fields.day"
placeholder="天" placeholder="天"
style="width: 80px" class="w-20"
key="day" key="day"
/> />
<Input <Input
v-model:value="fields.month" v-model:value="fields.month"
placeholder="月" placeholder="月"
style="width: 80px" class="w-20"
key="month" key="month"
/> />
<Input <Input
v-model:value="fields.week" v-model:value="fields.week"
placeholder="周" placeholder="周"
style="width: 80px" class="w-20"
key="week" key="week"
/> />
<Input <Input
v-model:value="fields.year" v-model:value="fields.year"
placeholder="年" placeholder="年"
style="width: 80px" class="w-20"
key="year" key="year"
/> />
</div> </div>
<Tabs <Tabs v-model:active-key="activeField" type="card" class="mb-2">
v-model:active-key="activeField"
type="card"
style="margin-bottom: 8px"
>
<Tabs.TabPane v-for="f in cronFieldList" :key="f.key" :tab="f.label"> <Tabs.TabPane v-for="f in cronFieldList" :key="f.key" :tab="f.label">
<div style="margin-bottom: 8px"> <div class="mb-2">
<Radio.Group <Radio.Group
v-model:value="cronMode[f.key]" v-model:value="cronMode[f.key]"
:key="`radio-${f.key}`" :key="`radio-${f.key}`"
@@ -218,7 +273,7 @@ watch(
:min="f.min" :min="f.min"
:max="f.max" :max="f.max"
size="small" size="small"
style="width: 60px" class="w-[60px]"
:key="`range0-${f.key}`" :key="`range0-${f.key}`"
/> />
@@ -227,7 +282,7 @@ watch(
:min="f.min" :min="f.min"
:max="f.max" :max="f.max"
size="small" size="small"
style="width: 60px" class="w-[60px]"
:key="`range1-${f.key}`" :key="`range1-${f.key}`"
/> />
之间每{{ f.label }} 之间每{{ f.label }}
@@ -239,7 +294,7 @@ watch(
:min="f.min" :min="f.min"
:max="f.max" :max="f.max"
size="small" size="small"
style="width: 60px" class="w-[60px]"
:key="`step0-${f.key}`" :key="`step0-${f.key}`"
/> />
开始每 开始每
@@ -248,7 +303,7 @@ watch(
:min="1" :min="1"
:max="f.max" :max="f.max"
size="small" size="small"
style="width: 60px" class="w-[60px]"
:key="`step1-${f.key}`" :key="`step1-${f.key}`"
/> />
{{ f.label }} {{ f.label }}
@@ -272,109 +327,64 @@ watch(
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
</Tabs.TabPane> </TabPane>
<Tabs.TabPane key="iso" title="标准格式" tab="iso-tab"> <TabPane key="iso" tab="标准格式">
<div style="margin-bottom: 10px"> <div class="mb-2.5">
<Input <Input
v-model:value="isoStr" v-model:value="isoStr"
placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S" placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
style="width: 400px; font-weight: bold" class="w-[400px] font-bold"
key="isoStr" key="isoStr"
/> />
</div> </div>
<div style="margin-bottom: 10px"> <div class="mb-2.5">
循环次数<InputNumber 循环次数<InputNumber
v-model:value="repeat" v-model:value="repeat"
:min="1" :min="1"
style="width: 100px" class="w-[100px]"
key="repeat" key="repeat"
/> />
</div> </div>
<div style="margin-bottom: 10px"> <div class="mb-2.5">
日期时间<DatePicker 开始时间<DatePicker
v-model:value="isoDate" v-model:value="isoDate"
show-time show-time
placeholder="选择日期时间" placeholder="选择开始时间"
style="width: 200px" class="w-[200px]"
key="isoDate" key="isoDate"
/> />
</div> </div>
<div style="margin-bottom: 10px"> <div class="mb-2.5">
当前时长<Input 间隔时长<Input
v-model:value="isoDuration" v-model:value="isoDuration"
readonly
placeholder="如P3DT30M30S" placeholder="如P3DT30M30S"
style="width: 200px" class="w-[200px]"
key="isoDuration" key="isoDuration"
/> />
</div> </div>
<div> <div>
<div> <div v-for="unit in durationUnits" :key="unit.key" class="mb-2">
<span>{{ unit.label }}</span>
<Button <Button.Group>
v-for="s in [5, 10, 30, 50]" <Button
@click="setDuration('S', s)" v-for="val in unit.presets"
:key="`sec-${s}`" :key="val"
> size="small"
{{ s }} @click="setDuration(unit.key, val)"
</Button> >
自定义 {{ val }}
</div> </Button>
<div> <Input
v-model:value="durationCustom[unit.key]"
<Button size="small"
v-for="m in [5, 10, 30, 50]" class="ml-2 w-[60px]"
@click="setDuration('M', m)" placeholder="自定义"
:key="`min-${m}`" @change="setDuration(unit.key, durationCustom[unit.key])"
> />
{{ m }} </Button.Group>
</Button>
自定义
</div>
<div>
小时
<Button
v-for="h in [4, 8, 12, 24]"
@click="setDuration('H', h)"
:key="`hour-${h}`"
>
{{ h }}
</Button>
自定义
</div>
<div>
<Button
v-for="d in [1, 2, 3, 4]"
@click="setDuration('D', d)"
:key="`day-${d}`"
>
{{ d }}
</Button>
自定义
</div>
<div>
<Button
v-for="mo in [1, 2, 3, 4]"
@click="setDuration('M', mo)"
:key="`mon-${mo}`"
>
{{ mo }}
</Button>
自定义
</div>
<div>
<Button
v-for="y in [1, 2, 3, 4]"
@click="setDuration('Y', y)"
:key="`year-${y}`"
>
{{ y }}
</Button>
自定义
</div> </div>
</div> </div>
</Tabs.TabPane> </TabPane>
</Tabs> </Tabs>
</template> </template>

View File

@@ -68,14 +68,10 @@ watch(
<template> <template>
<div> <div>
<div style="margin-bottom: 10px"> <div class="mb-2.5">
当前选择<Input 当前选择<Input v-model:value="isoString" readonly class="w-[300px]" />
v-model:value="isoString"
readonly
style="width: 300px"
/>
</div> </div>
<div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px"> <div v-for="unit in units" :key="unit.key" class="mb-2">
<span>{{ unit.label }}</span> <span>{{ unit.label }}</span>
<Button.Group> <Button.Group>
<Button <Button
@@ -89,7 +85,7 @@ watch(
<Input <Input
v-model:value="custom[unit.key]" v-model:value="custom[unit.key]"
size="small" size="small"
style="width: 60px; margin-left: 8px" class="ml-2 w-[60px]"
placeholder="自定义" placeholder="自定义"
@change="setUnit(unit.key, custom[unit.key])" @change="setUnit(unit.key, custom[unit.key])"
/> />

View File

@@ -1,11 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue'; import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue'; import { Button, DatePicker, Input, Tooltip } from 'ant-design-vue';
import CycleConfig from './CycleConfig.vue'; import CycleConfig from './CycleConfig.vue';
import DurationConfig from './DurationConfig.vue'; import DurationConfig from './DurationConfig.vue';
@@ -20,13 +23,8 @@ const props = defineProps({
const bpmnInstances = () => (window as any).bpmnInstances; const bpmnInstances = () => (window as any).bpmnInstances;
const type: Ref<string> = ref('time'); const type: Ref<string> = ref('time');
const condition: Ref<string> = ref(''); const condition: Ref<string> = ref('');
const valid: Ref<boolean> = ref(true); const valid: Ref<boolean> = ref(false);
const showDatePicker: Ref<boolean> = ref(false); const dateValue = ref<Dayjs>();
const showDurationDialog: Ref<boolean> = ref(false);
const showCycleDialog: Ref<boolean> = ref(false);
const showHelp: Ref<boolean> = ref(false);
const dateValue: Ref<Date | null> = ref(null);
// const bpmnElement = ref(null);
const placeholder = computed<string>(() => { const placeholder = computed<string>(() => {
if (type.value === 'time') return '请输入时间'; if (type.value === 'time') return '请输入时间';
@@ -49,6 +47,9 @@ const helpHtml = computed<string>(() => {
if (type.value === 'cycle') { if (type.value === 'cycle') {
return `支持CRON表达式如0 0/30 * * * ?或ISO 8601周期如R3/PT10M`; return `支持CRON表达式如0 0/30 * * * ?或ISO 8601周期如R3/PT10M`;
} }
if (type.value === 'time') {
return `支持ISO 8601格式的时间如2024-12-12T12:12:12`;
}
return ''; return '';
}); });
@@ -82,7 +83,6 @@ function setType(t: string) {
// 输入校验 // 输入校验
watch([type, condition], () => { watch([type, condition], () => {
valid.value = validate(); valid.value = validate();
// updateNode() // 可以注释掉,避免频繁触发
}); });
function validate(): boolean { function validate(): boolean {
@@ -93,46 +93,74 @@ function validate(): boolean {
return /^P.*$/.test(condition.value); return /^P.*$/.test(condition.value);
} }
if (type.value === 'cycle') { if (type.value === 'cycle') {
return /^(?:[0-9*/?, ]+|R\d*\/P.*)$/.test(condition.value); // 支持CRON表达式或ISO 8601周期格式R{n}/P... 或 R{n}/{date}/P...
return /^(?:[0-9*/?, ]+|R\d+(?:\/[^/]+)*\/P.*)$/.test(condition.value);
} }
return true; return true;
} }
// 选择时间 // 选择时间 Modal
const [DateModal, dateModalApi] = useVbenModal({
title: '选择时间',
class: 'w-[400px]',
onConfirm: onDateConfirm,
});
function onDateChange(val: any) { function onDateChange(val: any) {
dateValue.value = val; dateValue.value = val || undefined;
} }
function onDateConfirm(): void { function onDateConfirm(): void {
if (dateValue.value) { if (dateValue.value) {
condition.value = new Date(dateValue.value).toISOString(); condition.value = dateValue.value.toISOString();
showDatePicker.value = false; dateModalApi.close();
updateNode(); updateNode();
} }
} }
// 持续时长 // 持续时长 Modal
const [DurationModal, durationModalApi] = useVbenModal({
title: '时间配置',
class: 'w-[600px]',
onConfirm: onDurationConfirm,
});
function onDurationChange(val: string) { function onDurationChange(val: string) {
condition.value = val; condition.value = val;
} }
function onDurationConfirm(): void { function onDurationConfirm(): void {
showDurationDialog.value = false; durationModalApi.close();
updateNode(); updateNode();
} }
// 循环 // 循环配置 Modal
const [CycleModal, cycleModalApi] = useVbenModal({
title: '时间配置',
class: 'w-[800px]',
onConfirm: onCycleConfirm,
});
function onCycleChange(val: string) { function onCycleChange(val: string) {
condition.value = val; condition.value = val;
} }
function onCycleConfirm(): void { function onCycleConfirm(): void {
showCycleDialog.value = false; cycleModalApi.close();
updateNode(); updateNode();
} }
// 输入框聚焦时弹窗(可选) // 帮助说明 Modal
function handleInputFocus(): void { const [HelpModal, helpModalApi] = useVbenModal({
if (type.value === 'time') showDatePicker.value = true; class: 'w-[600px]',
if (type.value === 'duration') showDurationDialog.value = true; title: '格式说明',
if (type.value === 'cycle') showCycleDialog.value = true; showCancelButton: false,
confirmText: '关闭',
onConfirm: () => helpModalApi.close(),
});
// 点击输入框时弹窗
function handleInputClick(): void {
if (type.value === 'time') dateModalApi.open();
if (type.value === 'duration') durationModalApi.open();
if (type.value === 'cycle') cycleModalApi.open();
} }
// 同步到节点 // 同步到节点
@@ -210,8 +238,8 @@ watch(
<template> <template>
<div class="panel-tab__content"> <div class="panel-tab__content">
<div style="margin-top: 10px"> <div class="mt-2 flex items-center">
<span>类型</span> <span class="w-14">类型</span>
<Button.Group> <Button.Group>
<Button <Button
size="small" size="small"
@@ -238,17 +266,17 @@ watch(
<IconifyIcon <IconifyIcon
icon="ant-design:check-circle-filled" icon="ant-design:check-circle-filled"
v-if="valid" v-if="valid"
style="margin-left: 8px; color: green" class="ml-2 text-green-500"
/> />
</div> </div>
<div style="display: flex; align-items: center; margin-top: 10px"> <div class="mt-2 flex items-center gap-1">
<span>条件</span> <span class="w-14">条件</span>
<Input <Input
v-model:value="condition" v-model:value="condition"
:placeholder="placeholder" :placeholder="placeholder"
class="w-[calc(100vw-25%)]" class="w-full"
:readonly="type !== 'duration' && type !== 'cycle'" :readonly="type !== 'duration' && type !== 'cycle'"
@focus="handleInputFocus" @click="handleInputClick"
@blur="updateNode" @blur="updateNode"
> >
<template #suffix> <template #suffix>
@@ -262,13 +290,13 @@ watch(
<IconifyIcon <IconifyIcon
icon="ant-design:question-circle-filled" icon="ant-design:question-circle-filled"
class="cursor-pointer text-[#409eff]" class="cursor-pointer text-[#409eff]"
@click="showHelp = true" @click="helpModalApi.open()"
/> />
</Tooltip> </Tooltip>
<Button <Button
v-if="type === 'time'" v-if="type === 'time'"
@click="showDatePicker = true" @click="dateModalApi.open()"
style="margin-left: 4px" class="ml-1 flex items-center justify-center"
shape="circle" shape="circle"
size="small" size="small"
> >
@@ -276,8 +304,8 @@ watch(
</Button> </Button>
<Button <Button
v-if="type === 'duration'" v-if="type === 'duration'"
@click="showDurationDialog = true" @click="durationModalApi.open()"
style="margin-left: 4px" class="ml-1 flex items-center justify-center"
shape="circle" shape="circle"
size="small" size="small"
> >
@@ -285,8 +313,8 @@ watch(
</Button> </Button>
<Button <Button
v-if="type === 'cycle'" v-if="type === 'cycle'"
@click="showCycleDialog = true" @click="cycleModalApi.open()"
style="margin-left: 4px" class="ml-1 flex items-center justify-center"
shape="circle" shape="circle"
size="small" size="small"
> >
@@ -295,62 +323,32 @@ watch(
</template> </template>
</Input> </Input>
</div> </div>
<!-- 时间选择器 --> <!-- 时间选择器 -->
<Modal <DateModal>
v-model:open="showDatePicker"
title="选择时间"
width="400px"
@cancel="showDatePicker = false"
>
<DatePicker <DatePicker
v-model:value="dateValue" v-model:value="dateValue"
show-time show-time
placeholder="选择日期时间" placeholder="选择日期时间"
style="width: 100%" class="w-full"
@change="onDateChange" @change="onDateChange"
/> />
<template #footer> </DateModal>
<Button @click="showDatePicker = false">取消</Button>
<Button type="primary" @click="onDateConfirm">确定</Button>
</template>
</Modal>
<!-- 持续时长选择器 --> <!-- 持续时长选择器 -->
<Modal <DurationModal>
v-model:open="showDurationDialog"
title="时间配置"
width="600px"
@cancel="showDurationDialog = false"
>
<DurationConfig :value="condition" @change="onDurationChange" /> <DurationConfig :value="condition" @change="onDurationChange" />
<template #footer> </DurationModal>
<Button @click="showDurationDialog = false">取消</Button>
<Button type="primary" @click="onDurationConfirm">确定</Button>
</template>
</Modal>
<!-- 循环配置器 --> <!-- 循环配置器 -->
<Modal <CycleModal>
v-model:open="showCycleDialog"
title="时间配置"
width="800px"
@cancel="showCycleDialog = false"
>
<CycleConfig :value="condition" @change="onCycleChange" /> <CycleConfig :value="condition" @change="onCycleChange" />
<template #footer> </CycleModal>
<Button @click="showCycleDialog = false">取消</Button>
<Button type="primary" @click="onCycleConfirm">确定</Button>
</template>
</Modal>
<!-- 帮助说明 --> <!-- 帮助说明 -->
<Modal <HelpModal>
v-model:open="showHelp" <!-- eslint-disable-next-line vue/no-v-html -->
title="格式说明"
width="600px"
@cancel="showHelp = false"
>
<div v-html="helpHtml"></div> <div v-html="helpHtml"></div>
<template #footer> </HelpModal>
<Button @click="showHelp = false">关闭</Button>
</template>
</Modal>
</div> </div>
</template> </template>