2025-09-05 15:38:09 +08:00
|
|
|
|
<script lang="ts" setup>
|
2025-09-14 18:16:02 +08:00
|
|
|
|
import type { Ref } from 'vue';
|
|
|
|
|
|
|
|
|
|
|
|
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
|
|
|
|
|
|
2025-09-25 11:52:26 +08:00
|
|
|
|
import { IconifyIcon } from '@vben/icons';
|
2025-09-14 18:16:02 +08:00
|
|
|
|
|
|
|
|
|
|
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
|
|
|
|
|
|
|
2025-09-05 15:38:09 +08:00
|
|
|
|
import CycleConfig from './CycleConfig.vue';
|
2025-09-14 18:16:02 +08:00
|
|
|
|
import DurationConfig from './DurationConfig.vue';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
businessObject: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({}),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-05 15:38:09 +08:00
|
|
|
|
const bpmnInstances = () => (window as any).bpmnInstances;
|
2025-09-14 18:16:02 +08:00
|
|
|
|
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);
|
2025-09-05 15:38:09 +08:00
|
|
|
|
|
2025-09-14 18:16:02 +08:00
|
|
|
|
const placeholder = computed<string>(() => {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (type.value === 'time') return '请输入时间';
|
|
|
|
|
|
if (type.value === 'duration') return '请输入持续时长';
|
|
|
|
|
|
if (type.value === 'cycle') return '请输入循环表达式';
|
|
|
|
|
|
return '';
|
|
|
|
|
|
});
|
2025-09-14 18:16:02 +08:00
|
|
|
|
const helpText = computed<string>(() => {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (type.value === 'time') return '选择具体时间';
|
|
|
|
|
|
if (type.value === 'duration') return 'ISO 8601格式,如PT1H';
|
|
|
|
|
|
if (type.value === 'cycle') return 'CRON表达式或ISO 8601周期';
|
|
|
|
|
|
return '';
|
|
|
|
|
|
});
|
2025-09-14 18:16:02 +08:00
|
|
|
|
const helpHtml = computed<string>(() => {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (type.value === 'duration') {
|
|
|
|
|
|
return `指定定时器之前要等待多长时间。S表示秒,M表示分,D表示天;P表示时间段,T表示精确到时间的时间段。<br>
|
|
|
|
|
|
时间格式依然为ISO 8601格式,一年两个月三天四小时五分六秒内,可以写成P1Y2M3DT4H5M6S。<br>
|
|
|
|
|
|
P是开始标记,T是时间和日期分割标记,没有日期只有时间T是不能省去的,比如1小时执行一次应写成PT1H。`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type.value === 'cycle') {
|
|
|
|
|
|
return `支持CRON表达式(如0 0/30 * * * ?)或ISO 8601周期(如R3/PT10M)。`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化和监听
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function syncFromBusinessObject(): void {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (props.businessObject) {
|
|
|
|
|
|
const timerDef = (props.businessObject.eventDefinitions || [])[0];
|
|
|
|
|
|
if (timerDef) {
|
|
|
|
|
|
if (timerDef.timeDate) {
|
|
|
|
|
|
type.value = 'time';
|
|
|
|
|
|
condition.value = timerDef.timeDate.body;
|
|
|
|
|
|
} else if (timerDef.timeDuration) {
|
|
|
|
|
|
type.value = 'duration';
|
|
|
|
|
|
condition.value = timerDef.timeDuration.body;
|
|
|
|
|
|
} else if (timerDef.timeCycle) {
|
|
|
|
|
|
type.value = 'cycle';
|
|
|
|
|
|
condition.value = timerDef.timeCycle.body;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
onMounted(syncFromBusinessObject);
|
|
|
|
|
|
|
|
|
|
|
|
// 切换类型
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function setType(t: string) {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
type.value = t;
|
|
|
|
|
|
condition.value = '';
|
|
|
|
|
|
updateNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 输入校验
|
|
|
|
|
|
watch([type, condition], () => {
|
|
|
|
|
|
valid.value = validate();
|
|
|
|
|
|
// updateNode() // 可以注释掉,避免频繁触发
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function validate(): boolean {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (type.value === 'time') {
|
2025-09-14 18:16:02 +08:00
|
|
|
|
return !!condition.value && !Number.isNaN(Date.parse(condition.value));
|
2025-09-05 15:38:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (type.value === 'duration') {
|
|
|
|
|
|
return /^P.*$/.test(condition.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type.value === 'cycle') {
|
2025-09-14 18:16:02 +08:00
|
|
|
|
return /^(?:[0-9*/?, ]+|R\d*\/P.*)$/.test(condition.value);
|
2025-09-05 15:38:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选择时间
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onDateChange(val: any) {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
dateValue.value = val;
|
|
|
|
|
|
}
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onDateConfirm(): void {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (dateValue.value) {
|
|
|
|
|
|
condition.value = new Date(dateValue.value).toISOString();
|
|
|
|
|
|
showDatePicker.value = false;
|
|
|
|
|
|
updateNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 持续时长
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onDurationChange(val: string) {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
condition.value = val;
|
|
|
|
|
|
}
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onDurationConfirm(): void {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
showDurationDialog.value = false;
|
|
|
|
|
|
updateNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 循环
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onCycleChange(val: string) {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
condition.value = val;
|
|
|
|
|
|
}
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function onCycleConfirm(): void {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
showCycleDialog.value = false;
|
|
|
|
|
|
updateNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 输入框聚焦时弹窗(可选)
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function handleInputFocus(): void {
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (type.value === 'time') showDatePicker.value = true;
|
|
|
|
|
|
if (type.value === 'duration') showDurationDialog.value = true;
|
|
|
|
|
|
if (type.value === 'cycle') showCycleDialog.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同步到节点
|
2025-09-14 18:16:02 +08:00
|
|
|
|
function updateNode(): void {
|
|
|
|
|
|
const moddle = (window.bpmnInstances as any)?.moddle;
|
|
|
|
|
|
const modeling = (window.bpmnInstances as any)?.modeling;
|
|
|
|
|
|
const elementRegistry = (window.bpmnInstances as any)?.elementRegistry;
|
2025-09-05 15:38:09 +08:00
|
|
|
|
if (!moddle || !modeling || !elementRegistry) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取元素
|
|
|
|
|
|
if (!props.businessObject || !props.businessObject.id) return;
|
|
|
|
|
|
const element = elementRegistry.get(props.businessObject.id);
|
|
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 复用原有 timerDef,或新建
|
|
|
|
|
|
let timerDef =
|
|
|
|
|
|
element.businessObject.eventDefinitions &&
|
|
|
|
|
|
element.businessObject.eventDefinitions[0];
|
|
|
|
|
|
if (!timerDef) {
|
|
|
|
|
|
timerDef = bpmnInstances().bpmnFactory.create(
|
|
|
|
|
|
'bpmn:TimerEventDefinition',
|
|
|
|
|
|
{},
|
|
|
|
|
|
);
|
|
|
|
|
|
modeling.updateProperties(element, {
|
|
|
|
|
|
eventDefinitions: [timerDef],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 清空原有
|
|
|
|
|
|
delete timerDef.timeDate;
|
|
|
|
|
|
delete timerDef.timeDuration;
|
|
|
|
|
|
delete timerDef.timeCycle;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 设置新的
|
|
|
|
|
|
if (type.value === 'time' && condition.value) {
|
|
|
|
|
|
timerDef.timeDate = bpmnInstances().bpmnFactory.create(
|
|
|
|
|
|
'bpmn:FormalExpression',
|
|
|
|
|
|
{
|
|
|
|
|
|
body: condition.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (type.value === 'duration' && condition.value) {
|
|
|
|
|
|
timerDef.timeDuration = bpmnInstances().bpmnFactory.create(
|
|
|
|
|
|
'bpmn:FormalExpression',
|
|
|
|
|
|
{
|
|
|
|
|
|
body: condition.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (type.value === 'cycle' && condition.value) {
|
|
|
|
|
|
timerDef.timeCycle = bpmnInstances().bpmnFactory.create(
|
|
|
|
|
|
'bpmn:FormalExpression',
|
|
|
|
|
|
{
|
|
|
|
|
|
body: condition.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bpmnInstances().modeling.updateProperties(toRaw(element), {
|
|
|
|
|
|
eventDefinitions: [timerDef],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.businessObject,
|
|
|
|
|
|
(val) => {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
syncFromBusinessObject();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-09-14 18:16:02 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="panel-tab__content">
|
|
|
|
|
|
<div style="margin-top: 10px">
|
|
|
|
|
|
<span>类型:</span>
|
|
|
|
|
|
<Button.Group>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="type === 'time' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="setType('time')"
|
|
|
|
|
|
>
|
|
|
|
|
|
时间
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="type === 'duration' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="setType('duration')"
|
|
|
|
|
|
>
|
|
|
|
|
|
持续
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="type === 'cycle' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="setType('cycle')"
|
|
|
|
|
|
>
|
|
|
|
|
|
循环
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Button.Group>
|
2025-09-25 11:52:26 +08:00
|
|
|
|
<IconifyIcon
|
|
|
|
|
|
icon="ant-design:check-circle-filled"
|
|
|
|
|
|
v-if="valid"
|
|
|
|
|
|
style="color: green; margin-left: 8px"
|
|
|
|
|
|
/>
|
2025-09-14 18:16:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style="display: flex; align-items: center; margin-top: 10px">
|
|
|
|
|
|
<span>条件:</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model:value="condition"
|
|
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
|
style="width: calc(100% - 100px)"
|
|
|
|
|
|
:readonly="type !== 'duration' && type !== 'cycle'"
|
|
|
|
|
|
@focus="handleInputFocus"
|
|
|
|
|
|
@blur="updateNode"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #suffix>
|
|
|
|
|
|
<Tooltip v-if="!valid" title="格式错误" placement="top">
|
2025-09-25 11:52:26 +08:00
|
|
|
|
<IconifyIcon
|
|
|
|
|
|
icon="ant-design:exclamation-circle-filled"
|
|
|
|
|
|
class="text-orange-400"
|
|
|
|
|
|
/>
|
2025-09-14 18:16:02 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip :title="helpText" placement="top">
|
2025-09-25 11:52:26 +08:00
|
|
|
|
<IconifyIcon
|
|
|
|
|
|
icon="ant-design:question-circle-filled"
|
|
|
|
|
|
class="cursor-pointer text-[#409eff]"
|
2025-09-14 18:16:02 +08:00
|
|
|
|
@click="showHelp = true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="type === 'time'"
|
|
|
|
|
|
@click="showDatePicker = true"
|
|
|
|
|
|
style="margin-left: 4px"
|
|
|
|
|
|
shape="circle"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconifyIcon icon="ep:calendar" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="type === 'duration'"
|
|
|
|
|
|
@click="showDurationDialog = true"
|
|
|
|
|
|
style="margin-left: 4px"
|
|
|
|
|
|
shape="circle"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconifyIcon icon="ep:timer" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="type === 'cycle'"
|
|
|
|
|
|
@click="showCycleDialog = true"
|
|
|
|
|
|
style="margin-left: 4px"
|
|
|
|
|
|
shape="circle"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconifyIcon icon="ep:setting" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Input>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 时间选择器 -->
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
v-model:open="showDatePicker"
|
|
|
|
|
|
title="选择时间"
|
|
|
|
|
|
width="400px"
|
|
|
|
|
|
@cancel="showDatePicker = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DatePicker
|
|
|
|
|
|
v-model:value="dateValue"
|
|
|
|
|
|
show-time
|
|
|
|
|
|
placeholder="选择日期时间"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@change="onDateChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button @click="showDatePicker = false">取消</Button>
|
|
|
|
|
|
<Button type="primary" @click="onDateConfirm">确定</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
<!-- 持续时长选择器 -->
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
v-model:open="showDurationDialog"
|
|
|
|
|
|
title="时间配置"
|
|
|
|
|
|
width="600px"
|
|
|
|
|
|
@cancel="showDurationDialog = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DurationConfig :value="condition" @change="onDurationChange" />
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button @click="showDurationDialog = false">取消</Button>
|
|
|
|
|
|
<Button type="primary" @click="onDurationConfirm">确定</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
<!-- 循环配置器 -->
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
v-model:open="showCycleDialog"
|
|
|
|
|
|
title="时间配置"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
@cancel="showCycleDialog = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<CycleConfig :value="condition" @change="onCycleChange" />
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button @click="showCycleDialog = false">取消</Button>
|
|
|
|
|
|
<Button type="primary" @click="onCycleConfirm">确定</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
<!-- 帮助说明 -->
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
v-model:open="showHelp"
|
|
|
|
|
|
title="格式说明"
|
|
|
|
|
|
width="600px"
|
|
|
|
|
|
@cancel="showHelp = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-html="helpHtml"></div>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button @click="showHelp = false">关闭</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|