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

View File

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

View File

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