fix: 修复表单校验回调弃用与 BPMN 告警

- 将 web-antd、web-antdv-next、web-ele 中的表单 validator 从 callback 改为 Promise

- 为 BPMN viewer 的 xml 增加空值兜底,避免 undefined prop warning

- 移除流程详情用户选择弹窗的无效 title 透传

- 调整 simple-bpm-viewer 函数声明顺序,避免 immediate watch 初始化时报错
This commit is contained in:
YunaiV
2026-07-05 01:02:49 +08:00
parent 9f990e0ab3
commit d75e4ac32d
28 changed files with 313 additions and 372 deletions

View File

@@ -112,41 +112,37 @@ export namespace ThingModelApi {
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
return (_rule: any, value: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
return Promise.reject(new Error(`${label}必须是数字`));
}
callback();
return Promise.resolve();
};
}
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
return (_rule: any, value: string) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
return Promise.reject(
new Error(`${label}必须以中文、英文字母或数字开头`),
);
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
return Promise.reject(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
return Promise.reject(new Error(`${label}长度不能超过 20 个字符`));
}
callback();
return Promise.resolve();
};
}
@@ -170,7 +166,7 @@ export const ThingModelFormRules: Record<string, Rule[]> = {
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
validator: (_rule: any, value: string) => {
const reservedKeywords = [
'set',
'get',
@@ -181,18 +177,16 @@ export const ThingModelFormRules: Record<string, Rule[]> = {
'value',
];
if (reservedKeywords.includes(value)) {
callback(
return Promise.reject(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
return Promise.reject(new Error('标识符不能是纯数字'));
}
callback();
return Promise.resolve();
},
trigger: 'blur',
},

View File

@@ -15,8 +15,8 @@ import '../theme/index.scss';
const props = defineProps({
xml: {
default: '',
type: String,
required: true,
},
view: {
type: Object,
@@ -185,12 +185,12 @@ const onSelectElement = (element: any) => {
};
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
const importXML = async (xml?: string) => {
// 清空流程图
clearViewer();
// 初始化流程图
if (xml !== null && xml !== '') {
if (xml) {
try {
bpmnViewer.value = new BpmnViewer({
additionalModules: [MoveCanvasModule],
@@ -303,7 +303,7 @@ const setProcessStatus = (view: any) => {
watch(
() => props.xml,
(newXml) => {
importXML(newXml);
importXML(newXml || '');
},
{ immediate: true },
);
@@ -318,7 +318,7 @@ watch(
/** mounted初始化 */
onMounted(() => {
importXML(props.xml);
importXML(props.xml || '');
setProcessStatus(props.view);
});

View File

@@ -68,20 +68,18 @@ const rules: Record<string, Rule[]> = {
key: [
{ required: true, message: '流程标识不能为空', trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
validator: (_rule: any, value: string) => {
if (!value) {
callback();
return;
return Promise.resolve();
}
if (!/^[a-z_][-\w.$]*$/i.test(value)) {
callback(
return Promise.reject(
new Error(
'只能包含字母、数字、下划线、连字符和点号,且必须以字母或下划线开头',
),
);
return;
}
callback();
return Promise.resolve();
},
trigger: 'blur',
},

View File

@@ -67,7 +67,7 @@ const startUserSelectTasks = ref<UserTask[]>([]);
const startUserSelectAssignees = ref<Record<string, number[]>>({});
const tempStartUserSelectAssignees = ref<Record<string, number[]>>({});
const bpmnXML = ref<string | undefined>(undefined);
const bpmnXML = ref('');
const simpleJson = ref<string | undefined>(undefined);
const activeTab = ref('form');
@@ -158,7 +158,7 @@ async function initProcessInfo(row: any, formVariables?: any) {
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinition =
await getProcessDefinition(row.id);
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml;
bpmnXML.value = processDefinitionDetail.bpmnXml || '';
simpleJson.value = processDefinitionDetail.simpleModel;
}
// 情况二:业务表单

View File

@@ -9,7 +9,7 @@ const props = withDefaults(
defineProps<{
bpmnXml?: string;
loading?: boolean; // 是否加载中
modelView?: object;
modelView?: Record<string, any>;
}>(),
{
loading: false,
@@ -29,8 +29,10 @@ watch(
async (newModelView) => {
// 加载最新
if (newModelView) {
// @ts-expect-error: viewer instance type is broader than local ref typing
view.value = newModelView;
view.value = {
...newModelView,
bpmnXml: newModelView.bpmnXml || '',
};
}
},
{ immediate: true },
@@ -40,7 +42,7 @@ watch(
watch(
() => props.bpmnXml,
(value) => {
view.value.bpmnXml = value;
view.value.bpmnXml = value || '';
},
);
</script>
@@ -52,7 +54,7 @@ watch(
>
<MyProcessViewer
key="processViewer"
:xml="view.bpmnXml"
:xml="view.bpmnXml || ''"
:view="view"
class="h-full min-h-[500px] w-full"
/>

View File

@@ -26,57 +26,14 @@ const simpleModel = ref<any>({});
const tasks = ref([]); // 用户任务
const processInstance = ref(); // 流程实例
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
const setSimpleModelNodeTaskStatus = (
function setSimpleModelNodeTaskStatus(
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[],
) => {
) {
if (!simpleModel) {
return;
}
@@ -166,7 +123,50 @@ const setSimpleModelNodeTaskStatus = (
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
};
}
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
</script>
<template>
<div v-loading="loading">

View File

@@ -482,7 +482,6 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
class="w-3/5"
v-model:value="selectedUsers"
:multiple="true"
title="选择用户"
@confirm="handleUserSelectConfirm"
@closed="handleUserSelectClosed"
@cancel="handleUserSelectCancel"

View File

@@ -122,23 +122,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule {
}
/** 触发器校验 */
function validateTriggers(_rule: any, value: any, callback: any) {
function validateTriggers(_rule: any, value: any) {
const error = validateSceneRuleTriggers(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
/** 执行器校验 */
function validateActions(_rule: any, value: any, callback: any) {
function validateActions(_rule: any, value: any) {
const error = validateSceneRuleActions(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
const formRules = reactive({

View File

@@ -35,51 +35,44 @@ function deleteEnum(index: number) {
}
/** 校验单项枚举值:必填、数字、不重复 */
function validateEnumValue(_rule: any, value: any, callback: any) {
function validateEnumValue(_rule: any, value: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
return Promise.reject(new Error('枚举值不能为空'));
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
return Promise.reject(new Error('枚举值必须是数字'));
}
const sameCount = dataSpecsList.value.filter(
(it) => it.value === value,
).length;
if (sameCount > 1) {
callback(new Error('枚举值不能重复'));
return;
return Promise.reject(new Error('枚举值不能重复'));
}
callback();
return Promise.resolve();
}
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
function validateEnumList(_rule: any, _value: any, callback: any) {
function validateEnumList(_rule: any, _value: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
return Promise.reject(new Error('请至少添加一个枚举项'));
}
const hasEmpty = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmpty) {
callback(new Error('存在未填写的枚举值或描述'));
return;
return Promise.reject(new Error('存在未填写的枚举值或描述'));
}
const hasInvalidNumber = dataSpecsList.value.some((item) =>
Number.isNaN(Number(item.value)),
);
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'));
return;
return Promise.reject(new Error('存在非数字的枚举值'));
}
const values = dataSpecsList.value.map((item) => item.value);
if (new Set(values).size !== values.length) {
callback(new Error('存在重复的枚举值'));
return;
return Promise.reject(new Error('存在重复的枚举值'));
}
callback();
return Promise.resolve();
}
</script>

View File

@@ -36,53 +36,46 @@ function unitChange(unitSpecs: any) {
}
/** 校验最小值 */
function validateMin(_rule: any, _value: any, callback: any) {
function validateMin(_rule: any, _value: any) {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(min)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(max) && min >= max) {
callback(new Error('最小值必须小于最大值'));
return;
return Promise.reject(new Error('最小值必须小于最大值'));
}
callback();
return Promise.resolve();
}
/** 校验最大值 */
function validateMax(_rule: any, _value: any, callback: any) {
function validateMax(_rule: any, _value: any) {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(max)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'));
return;
return Promise.reject(new Error('最大值必须大于最小值'));
}
callback();
return Promise.resolve();
}
/** 校验步长 */
function validateStep(_rule: any, _value: any, callback: any) {
function validateStep(_rule: any, _value: any) {
const step = Number(dataSpecs.value.step);
if (Number.isNaN(step)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (step <= 0) {
callback(new Error('步长必须大于 0'));
return;
return Promise.reject(new Error('步长必须大于 0'));
}
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值与最小值的差值'));
return;
return Promise.reject(new Error('步长不能大于最大值与最小值的差值'));
}
callback();
return Promise.resolve();
}
</script>

View File

@@ -33,12 +33,11 @@ const structFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
/** 校验结构体属性对象非空 */
function validateStructSpecsList(_rule: any, _value: any, callback: any) {
function validateStructSpecsList(_rule: any, _value: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个结构体属性对象'));
return;
return Promise.reject(new Error('请至少添加一个结构体属性对象'));
}
callback();
return Promise.resolve();
}
const [Modal, modalApi] = useVbenModal({

View File

@@ -114,41 +114,37 @@ export namespace ThingModelApi {
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
return (_rule: any, value: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
return Promise.reject(new Error(`${label}必须是数字`));
}
callback();
return Promise.resolve();
};
}
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
return (_rule: any, value: string) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
return Promise.reject(
new Error(`${label}必须以中文、英文字母或数字开头`),
);
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
return Promise.reject(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
return Promise.reject(new Error(`${label}长度不能超过 20 个字符`));
}
callback();
return Promise.resolve();
};
}
@@ -172,7 +168,7 @@ export const ThingModelFormRules: Record<string, FormItemRule[]> = {
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
validator: (_rule: any, value: string) => {
const reservedKeywords = [
'set',
'get',
@@ -183,18 +179,16 @@ export const ThingModelFormRules: Record<string, FormItemRule[]> = {
'value',
];
if (reservedKeywords.includes(value)) {
callback(
return Promise.reject(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
return Promise.reject(new Error('标识符不能是纯数字'));
}
callback();
return Promise.resolve();
},
trigger: 'blur',
},

View File

@@ -25,8 +25,8 @@ import '../theme/index.scss';
const props = defineProps({
xml: {
default: '',
type: String,
required: true,
},
view: {
type: Object,
@@ -269,12 +269,12 @@ const onSelectElement = (element: any) => {
};
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
const importXML = async (xml?: string) => {
// 清空流程图
clearViewer();
// 初始化流程图
if (xml !== null && xml !== '') {
if (xml) {
try {
bpmnViewer.value = new BpmnViewer({
additionalModules: [MoveCanvasModule],
@@ -387,7 +387,7 @@ const setProcessStatus = (view: any) => {
watch(
() => props.xml,
(newXml) => {
importXML(newXml);
importXML(newXml || '');
},
{ immediate: true },
);
@@ -402,7 +402,7 @@ watch(
/** mounted初始化 */
onMounted(() => {
importXML(props.xml);
importXML(props.xml || '');
setProcessStatus(props.view);
});

View File

@@ -76,7 +76,7 @@ const startUserSelectTasks = ref<UserTask[]>([]);
const startUserSelectAssignees = ref<Record<string, number[]>>({});
const tempStartUserSelectAssignees = ref<Record<string, number[]>>({});
const bpmnXML = ref<string | undefined>(undefined);
const bpmnXML = ref('');
const simpleJson = ref<string | undefined>(undefined);
const activeTab = ref('form');
@@ -167,7 +167,7 @@ async function initProcessInfo(row: any, formVariables?: any) {
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinition =
await getProcessDefinition(row.id);
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml;
bpmnXML.value = processDefinitionDetail.bpmnXml || '';
simpleJson.value = processDefinitionDetail.simpleModel;
}
// 情况二:业务表单

View File

@@ -9,7 +9,7 @@ const props = withDefaults(
defineProps<{
bpmnXml?: string;
loading?: boolean; // 是否加载中
modelView?: object;
modelView?: Record<string, any>;
}>(),
{
loading: false,
@@ -29,8 +29,10 @@ watch(
async (newModelView) => {
// 加载最新
if (newModelView) {
// @ts-expect-error: viewer instance type is broader than local ref typing
view.value = newModelView;
view.value = {
...newModelView,
bpmnXml: newModelView.bpmnXml || '',
};
}
},
{ immediate: true },
@@ -40,7 +42,7 @@ watch(
watch(
() => props.bpmnXml,
(value) => {
view.value.bpmnXml = value;
view.value.bpmnXml = value || '';
},
);
</script>
@@ -52,7 +54,7 @@ watch(
>
<MyProcessViewer
key="processViewer"
:xml="view.bpmnXml"
:xml="view.bpmnXml || ''"
:view="view"
class="h-full min-h-[500px] w-full"
/>

View File

@@ -26,57 +26,14 @@ const simpleModel = ref<any>({});
const tasks = ref([]); // 用户任务
const processInstance = ref(); // 流程实例
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
const setSimpleModelNodeTaskStatus = (
function setSimpleModelNodeTaskStatus(
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[],
) => {
) {
if (!simpleModel) {
return;
}
@@ -166,7 +123,50 @@ const setSimpleModelNodeTaskStatus = (
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
};
}
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
</script>
<template>
<div v-loading="loading">

View File

@@ -14,13 +14,7 @@ import {
import { IconifyIcon } from '@vben/icons';
import { formatDateTime, isEmpty } from '@vben/utils';
import {
Avatar,
Button,
Timeline,
TimelineItem,
Tooltip,
} from 'antdv-next';
import { Avatar, Button, Timeline, TimelineItem, Tooltip } from 'antdv-next';
import { UserSelectModal } from '#/views/system/user/components';
@@ -488,7 +482,6 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
class="w-3/5"
v-model:value="selectedUsers"
:multiple="true"
title="选择用户"
@confirm="handleUserSelectConfirm"
@closed="handleUserSelectClosed"
@cancel="handleUserSelectCancel"

View File

@@ -111,42 +111,38 @@ export namespace ThingModelApi {
}
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
function buildRequiredNumberValidator(label: string): any {
return (_rule: any, value: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
return Promise.reject(new Error(`${label}必须是数字`));
}
callback();
return Promise.resolve();
};
}
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
export function buildIdentifierLikeNameValidator(label: string): any {
return (_rule: any, value: string) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
return Promise.reject(
new Error(`${label}必须以中文、英文字母或数字开头`),
);
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
return Promise.reject(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
return Promise.reject(new Error(`${label}长度不能超过 20 个字符`));
}
callback();
return Promise.resolve();
};
}
@@ -170,7 +166,7 @@ export const ThingModelFormRules: Record<string, FormItemRule[]> = {
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
validator: ((_rule: any, value: string) => {
const reservedKeywords = [
'set',
'get',
@@ -181,19 +177,17 @@ export const ThingModelFormRules: Record<string, FormItemRule[]> = {
'value',
];
if (reservedKeywords.includes(value)) {
callback(
return Promise.reject(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
return Promise.reject(new Error('标识符不能是纯数字'));
}
callback();
},
return Promise.resolve();
}) as any,
trigger: 'blur',
},
],

View File

@@ -22,8 +22,8 @@ import '../theme/index.scss';
const props = defineProps({
xml: {
default: '',
type: String,
required: true,
},
view: {
type: Object,
@@ -192,12 +192,12 @@ const onSelectElement = (element: any) => {
};
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
const importXML = async (xml?: string) => {
// 清空流程图
clearViewer();
// 初始化流程图
if (xml !== null && xml !== '') {
if (xml) {
try {
bpmnViewer.value = new BpmnViewer({
additionalModules: [MoveCanvasModule],
@@ -310,7 +310,7 @@ const setProcessStatus = (view: any) => {
watch(
() => props.xml,
(newXml) => {
importXML(newXml);
importXML(newXml || '');
},
{ immediate: true },
);
@@ -325,7 +325,7 @@ watch(
/** mounted初始化 */
onMounted(() => {
importXML(props.xml);
importXML(props.xml || '');
setProcessStatus(props.view);
});

View File

@@ -70,21 +70,19 @@ const rules: Record<string, FormItemRule[]> = {
key: [
{ required: true, message: '流程标识不能为空', trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
validator: ((_rule: any, value: string) => {
if (!value) {
callback();
return;
return Promise.resolve();
}
if (!/^[a-z_][-\w.$]*$/i.test(value)) {
callback(
return Promise.reject(
new Error(
'只能包含字母、数字、下划线、连字符和点号,且必须以字母或下划线开头',
),
);
return;
}
callback();
},
return Promise.resolve();
}) as any,
trigger: 'blur',
},
],

View File

@@ -75,7 +75,7 @@ const startUserSelectTasks = ref<UserTask[]>([]);
const startUserSelectAssignees = ref<Record<string, number[]>>({});
const tempStartUserSelectAssignees = ref<Record<string, number[]>>({});
const bpmnXML = ref<string | undefined>(undefined);
const bpmnXML = ref('');
const simpleJson = ref<string | undefined>(undefined);
// @ts-expect-error unused
@@ -168,7 +168,7 @@ async function initProcessInfo(row: any, formVariables?: any) {
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinition =
await getProcessDefinition(row.id);
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml;
bpmnXML.value = processDefinitionDetail.bpmnXml || '';
simpleJson.value = processDefinitionDetail.simpleModel;
}
// 情况二:业务表单

View File

@@ -9,7 +9,7 @@ const props = withDefaults(
defineProps<{
bpmnXml?: string;
loading?: boolean; // 是否加载中
modelView?: object;
modelView?: Record<string, any>;
}>(),
{
loading: false,
@@ -29,8 +29,10 @@ watch(
async (newModelView) => {
// 加载最新
if (newModelView) {
// @ts-expect-error: viewer instance type is broader than local ref typing
view.value = newModelView;
view.value = {
...newModelView,
bpmnXml: newModelView.bpmnXml || '',
};
}
},
{ immediate: true },
@@ -40,7 +42,7 @@ watch(
watch(
() => props.bpmnXml,
(value) => {
view.value.bpmnXml = value;
view.value.bpmnXml = value || '';
},
);
</script>
@@ -52,7 +54,7 @@ watch(
>
<MyProcessViewer
key="processViewer"
:xml="view.bpmnXml"
:xml="view.bpmnXml || ''"
:view="view"
class="h-full min-h-[500px] w-full"
/>

View File

@@ -26,57 +26,14 @@ const simpleModel = ref<any>({});
const tasks = ref([]); // 用户任务
const processInstance = ref(); // 流程实例
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
const setSimpleModelNodeTaskStatus = (
function setSimpleModelNodeTaskStatus(
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[],
) => {
) {
if (!simpleModel) {
return;
}
@@ -166,7 +123,50 @@ const setSimpleModelNodeTaskStatus = (
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
};
}
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
{ immediate: true },
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
</script>
<template>
<div v-loading="loading">

View File

@@ -486,7 +486,6 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
class="w-3/5"
v-model="selectedUsers"
:multiple="true"
title="选择用户"
@confirm="handleUserSelectConfirm"
@closed="handleUserSelectClosed"
@cancel="handleUserSelectCancel"

View File

@@ -122,23 +122,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule {
}
/** 触发器校验 */
function validateTriggers(_rule: any, value: any, callback: any) {
function validateTriggers(_rule: any, value: any) {
const error = validateSceneRuleTriggers(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
/** 执行器校验 */
function validateActions(_rule: any, value: any, callback: any) {
function validateActions(_rule: any, value: any) {
const error = validateSceneRuleActions(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
const formRules = reactive({

View File

@@ -35,52 +35,45 @@ function deleteEnum(index: number) {
}
/** 校验单项枚举值:必填、数字、不重复 */
function validateEnumValue(_rule: any, value: any, callback: any) {
const validateEnumValue: any = (_rule: any, value: any) => {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
return Promise.reject(new Error('枚举值不能为空'));
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
return Promise.reject(new Error('枚举值必须是数字'));
}
const sameCount = dataSpecsList.value.filter(
(it) => it.value === value,
).length;
if (sameCount > 1) {
callback(new Error('枚举值不能重复'));
return;
return Promise.reject(new Error('枚举值不能重复'));
}
callback();
}
return Promise.resolve();
};
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
function validateEnumList(_rule: any, _value: any, callback: any) {
const validateEnumList: any = (_rule: any, _value: any) => {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
return Promise.reject(new Error('请至少添加一个枚举项'));
}
const hasEmpty = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmpty) {
callback(new Error('存在未填写的枚举值或描述'));
return;
return Promise.reject(new Error('存在未填写的枚举值或描述'));
}
const hasInvalidNumber = dataSpecsList.value.some((item) =>
Number.isNaN(Number(item.value)),
);
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'));
return;
return Promise.reject(new Error('存在非数字的枚举值'));
}
const values = dataSpecsList.value.map((item) => item.value);
if (new Set(values).size !== values.length) {
callback(new Error('存在重复的枚举值'));
return;
return Promise.reject(new Error('存在重复的枚举值'));
}
callback();
}
return Promise.resolve();
};
</script>
<template>

View File

@@ -29,54 +29,47 @@ function unitChange(unitSpecs: any) {
}
/** 校验最小值 */
function validateMin(_rule: any, _value: any, callback: any) {
const validateMin: any = (_rule: any, _value: any) => {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(min)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(max) && min >= max) {
callback(new Error('最小值必须小于最大值'));
return;
return Promise.reject(new Error('最小值必须小于最大值'));
}
callback();
}
return Promise.resolve();
};
/** 校验最大值 */
function validateMax(_rule: any, _value: any, callback: any) {
const validateMax: any = (_rule: any, _value: any) => {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(max)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'));
return;
return Promise.reject(new Error('最大值必须大于最小值'));
}
callback();
}
return Promise.resolve();
};
/** 校验步长 */
function validateStep(_rule: any, _value: any, callback: any) {
const validateStep: any = (_rule: any, _value: any) => {
const step = Number(dataSpecs.value.step);
if (Number.isNaN(step)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (step <= 0) {
callback(new Error('步长必须大于 0'));
return;
return Promise.reject(new Error('步长必须大于 0'));
}
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值与最小值的差值'));
return;
return Promise.reject(new Error('步长不能大于最大值与最小值的差值'));
}
callback();
}
return Promise.resolve();
};
</script>
<template>

View File

@@ -33,13 +33,12 @@ const structFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
/** 校验结构体属性对象非空 */
function validateStructSpecsList(_rule: any, _value: any, callback: any) {
const validateStructSpecsList: any = (_rule: any, _value: any) => {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个结构体属性对象'));
return;
return Promise.reject(new Error('请至少添加一个结构体属性对象'));
}
callback();
}
return Promise.resolve();
};
const [Modal, modalApi] = useVbenModal({
async onConfirm() {