fix(vben): 修复多端迁移差异与组件适配问题

- 同步 IoT 物模型 Event dataType 类型,并修复 antdv-next 模拟器事件类型展示
- 修复 antdv-next 物模型选项模板字符串和校验规则写法
- 补齐 antdv-next BPM 审批附件上传限制与流程打印业务表单渲染
- 修复 antdv-next 缺失的组件导入、垂直按钮组导出,并删除未引用的部门树遗留文件
- 对齐 BPM 流程配置中 antd/antdv-next 的组件属性写法
- 修复 ele 流程创建跳转、好友拒绝文本框、客服回车发送、公告栏颜色表单项和部门选择弹窗选中状态
- 调整 BPM 流程实例 API 字段顺序,减少三端无意义差异
This commit is contained in:
YunaiV
2026-06-25 22:35:58 -07:00
parent 94a5d9c8a1
commit 4affc95db4
29 changed files with 139 additions and 148 deletions

View File

@@ -52,6 +52,7 @@ export namespace ThingModelApi {
name?: string;
required?: boolean;
type?: string;
dataType?: string;
description?: string;
outputParams?: Param[];
method?: string;

View File

@@ -264,7 +264,6 @@ watch(
class="mr-2 mt-0.5"
v-model:value="timeDuration"
:min="1"
controls-position="right"
@change="
() => {
updateTimeModdle();

View File

@@ -687,7 +687,6 @@ onMounted(async () => {
class="w-24"
v-model:value="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</FormItem>
</Col>

View File

@@ -264,7 +264,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.roleIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -311,7 +311,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.postIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -331,7 +331,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.userIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -353,7 +353,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.userGroups"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -373,7 +373,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="表单内用户字段"
name="formUser"
>
<Select v-model:value="configForm.formUser" clearable>
<Select v-model:value="configForm.formUser" allow-clear>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
@@ -393,7 +393,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="表单内部门字段"
name="formDept"
>
<Select v-model:value="configForm.formDept" clearable>
<Select v-model:value="configForm.formDept" allow-clear>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
@@ -419,7 +419,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="configForm.deptLevel" clearable>
<Select v-model:value="configForm.deptLevel" allow-clear>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
@@ -437,7 +437,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="流程表达式"
name="expression"
>
<Textarea v-model:value="configForm.expression" clearable />
<Textarea v-model:value="configForm.expression" allow-clear />
</FormItem>
</Form>
</div>

View File

@@ -698,7 +698,7 @@ onMounted(() => {
>
<Select
v-model:value="configForm.roleIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -745,7 +745,7 @@ onMounted(() => {
>
<Select
v-model:value="configForm.postIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -765,7 +765,7 @@ onMounted(() => {
>
<Select
v-model:value="configForm.userIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -787,7 +787,7 @@ onMounted(() => {
>
<Select
v-model:value="configForm.userGroups"
clearable
allow-clear
mode="multiple"
>
<SelectOption
@@ -807,7 +807,7 @@ onMounted(() => {
label="表单内用户字段"
name="formUser"
>
<Select v-model:value="configForm.formUser" clearable>
<Select v-model:value="configForm.formUser" allow-clear>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
@@ -827,7 +827,7 @@ onMounted(() => {
label="表单内部门字段"
name="formDept"
>
<Select v-model:value="configForm.formDept" clearable>
<Select v-model:value="configForm.formDept" allow-clear>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
@@ -853,7 +853,7 @@ onMounted(() => {
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="configForm.deptLevel" clearable>
<Select v-model:value="configForm.deptLevel" allow-clear>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
@@ -944,7 +944,7 @@ onMounted(() => {
label="驳回节点"
name="returnNodeId"
>
<Select v-model:value="configForm.returnNodeId" clearable>
<Select v-model:value="configForm.returnNodeId" allow-clear>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
@@ -1015,7 +1015,6 @@ onMounted(() => {
class="mr-2 mt-0.5"
v-model:value="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</FormItem>
</Col>
@@ -1088,7 +1087,7 @@ onMounted(() => {
>
<Select
v-model:value="configForm.assignEmptyHandlerUserIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption

View File

@@ -79,11 +79,11 @@ export namespace BpmProcessInstanceApi {
export interface ApprovalTaskInfo {
id: number;
assigneeUser: User;
attachments?: string[];
ownerUser: User;
reason: string;
signPicUrl: string;
status: number;
attachments?: string[];
}
/** 抄送流程实例 */

View File

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

View File

@@ -263,7 +263,6 @@ watch(
class="mr-2 mt-0.5"
v-model:value="timeDuration"
:min="1"
controls-position="right"
@change="
() => {
updateTimeModdle();

View File

@@ -673,7 +673,6 @@ onMounted(async () => {
class="w-24"
v-model:value="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</FormItem>
</Col>

View File

@@ -402,7 +402,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="流程表达式"
name="expression"
>
<TextArea v-model:value="configForm.expression" clearable />
<TextArea v-model:value="configForm.expression" allow-clear />
</FormItem>
</Form>
</div>

View File

@@ -980,7 +980,6 @@ onMounted(() => {
class="mr-2 mt-0.5"
v-model:value="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</FormItem>
</Col>

View File

@@ -94,6 +94,24 @@ const popOverVisible: any = ref({
deleteSign: false,
}); // 气泡卡是否展示
const returnList = ref([] as any); // 退回节点
const APPROVAL_ATTACHMENT_FILE_TYPES = [
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'txt',
'pdf',
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
];
const APPROVAL_ATTACHMENT_FILE_SIZE = 5;
const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment';
/** 创建流程表达式 */
function openSignatureModal() {
@@ -897,8 +915,12 @@ defineExpose({ loadTodoTask });
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="approveReasonForm.attachments"
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
:max-number="10"
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
:multiple="true"
:show-description="true"
:show-download-icon="false"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
@@ -964,8 +986,12 @@ defineExpose({ loadTodoTask });
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="rejectReasonForm.attachments"
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
:max-number="10"
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
:multiple="true"
:show-description="true"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>

View File

@@ -2,7 +2,7 @@
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import type { SystemAreaApi } from '#/api/system/area';
import { computed, ref } from 'vue';
import { computed, ref, shallowRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
@@ -19,6 +19,7 @@ import { getAreaTree } from '#/api/system/area';
import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
import { decodeFields } from '#/components/form-create';
import { registerComponent } from '#/utils';
const userStore = useUserStore();
@@ -54,6 +55,7 @@ const userName = computed(() => userStore.userInfo?.nickname ?? '');
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'));
const formFields = ref<FormFieldItem[]>([]);
const printDataMap = ref<Record<string, string>>({});
const BusinessFormComponent = shallowRef<any>();
/** 打印配置 */
const printObj = ref({
@@ -93,6 +95,17 @@ async function fetchPrintData(id: string) {
printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm');
initPrintDataMap();
await parseFormFields();
initBusinessFormComponent();
}
/** 初始化业务表单组件 */
function initBusinessFormComponent() {
const businessFormPath =
printData.value?.processInstance.processDefinition?.formCustomViewPath ||
'';
BusinessFormComponent.value = businessFormPath
? registerComponent(businessFormPath)
: undefined;
}
/** 解析表单字段 */
@@ -595,6 +608,22 @@ function getPrintTemplateHTML() {
<div v-html="item.html"></div>
</td>
</tr>
</tbody>
</table>
<!-- 业务表单独立成块渲染不嵌入表格单元格避免宽度与分页受限 -->
<div
v-if="BusinessFormComponent && formFields.length === 0"
class="mt-3"
>
<component
:is="BusinessFormComponent"
:id="printData.processInstance.businessKey"
:readonly="true"
:print-mode="true"
/>
</div>
<table class="mt-3 w-full border-collapse">
<tbody>
<tr>
<td
class="w-full border border-black p-1.5 text-center"

View File

@@ -587,7 +587,7 @@ watch([activeTab, upstreamTab, downstreamTab], () => {
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.dataType ?? '-' }}
{{ record.event?.dataType ?? '-' }}
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />

View File

@@ -128,7 +128,7 @@ function validateStep(_rule: any, _value: any) {
@change="unitChange"
:options="[
...unitOptions.map((item) => ({
label: `{{ \`${item.label}-${item.value}\` }}`,
label: `${item.label}-${item.value}`,
value: `${item.label}-${item.value}`,
})),
]"

View File

@@ -113,7 +113,7 @@ if (!props.isStructDataSpecs && !props.isParams) {
@change="handleChange"
:options="[
...dataTypeOptions.map((option) => ({
label: `{{ \`${option.value}(${option.label})\` }}`,
label: `${option.value}(${option.label})`,
value: option.value,
})),
]"

View File

@@ -7,7 +7,7 @@ import { IconifyIcon } from '@vben/icons';
import { Button } from 'antdv-next';
import { VerticalSpace } from '#/views/mall/promotion/components';
import { VerticalButtonGroup } from '#/views/mall/promotion/components';
import { components } from './mobile';
@@ -109,7 +109,7 @@ const handleDeleteComponent = () => {
class="component-toolbar"
v-if="showToolbar && component.name && active"
>
<VerticalSpace size="small">
<VerticalButtonGroup size="small">
<Button
:disabled="!canMoveUp"
type="primary"
@@ -164,7 +164,7 @@ const handleDeleteComponent = () => {
>
<IconifyIcon icon="lucide:trash-2" />
</Button>
</VerticalSpace>
</VerticalButtonGroup>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep, isEmpty, isString } from '@vben/utils';
import { Button, Card, Col, QRCode, Row, Tag, Tooltip } from 'antdv-next';
import { Button, Card, Col, QRCode, Row, Space, Tag, Tooltip } from 'antdv-next';
import draggable from 'vuedraggable';
import statusBarImg from '#/assets/imgs/diy/statusBar.png';

View File

@@ -7,4 +7,4 @@ export { default as Draggable } from './draggable/index.vue';
export { default as InputWithColor } from './input-with-color/index.vue';
export { default as MagicCubeEditor } from './magic-cube-editor/index.vue';
export { default as VerticalSpace } from './vertical-button-group/index.vue';
export { default as VerticalButtonGroup } from './vertical-button-group/index.vue';

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { SpaceCompact } from 'antdv-next';
import { Space, SpaceCompact } from 'antdv-next';
/**
* 垂直按钮组
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局
*/
defineOptions({ name: 'VerticalSpace' });
defineOptions({ name: 'VerticalButtonGroup' });
</script>
<template>

View File

@@ -6,7 +6,16 @@ import { computed, ref } from 'vue';
import { confirm } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, Col, Input, Layout, Row, TextArea } from 'antdv-next';
import {
Button,
Col,
Input,
Layout,
LayoutContent,
LayoutSider,
Row,
TextArea,
} from 'antdv-next';
import { createEmptyNewsItem } from '#/api/mp/draft';
import { Tinymce as RichTextArea } from '#/components/tinymce';

View File

@@ -13,7 +13,15 @@ import {
import { useTabs } from '@vben/hooks';
import { fenToYuan, formatDate } from '@vben/utils';
import { Button, Card, Descriptions, Input, message, QRCode } from 'antdv-next';
import {
Button,
Card,
Descriptions,
DescriptionsItem,
Input,
message,
QRCode,
} from 'antdv-next';
import { getOrder, submitOrder } from '#/api/pay/order';

View File

@@ -1,85 +0,0 @@
<script lang="ts" setup>
import type { SystemDeptApi } from '#/api/system/dept';
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { handleTree } from '@vben/utils';
import { Input, Spin, Tree } from 'antdv-next';
import { getSimpleDeptList } from '#/api/system/dept';
const emit = defineEmits(['select']);
const deptList = ref<SystemDeptApi.Dept[]>([]); // 部门列表
const deptTree = ref<any[]>([]); // 部门树
const expandedKeys = ref<number[]>([]); // 展开的节点
const loading = ref(false); // 加载状态
const searchValue = ref(''); // 搜索值
/** 处理搜索逻辑 */
function handleSearch(e: any) {
const value = e.target.value;
searchValue.value = value;
const filteredList = value
? deptList.value.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase()),
)
: deptList.value;
deptTree.value = handleTree(filteredList);
// 展开所有节点
expandedKeys.value = deptTree.value.map((node) => node.id!);
}
/** 选中部门 */
function handleSelect(selectedNodeKeys: any[], info: any) {
const selectedKey = selectedNodeKeys[0] ?? info.node?.id ?? info.node?.key;
const dept = info.selected
? deptList.value.find((item) => String(item.id) === String(selectedKey))
: undefined;
emit('select', dept);
}
/** 初始化 */
onMounted(async () => {
try {
loading.value = true;
const data = await getSimpleDeptList();
deptList.value = data;
deptTree.value = handleTree(data);
} catch (error) {
console.error('获取部门数据失败', error);
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<Input
placeholder="搜索部门"
allow-clear
v-model:value="searchValue"
@change="handleSearch"
class="w-full"
>
<template #prefix>
<IconifyIcon icon="lucide:search" class="size-4" />
</template>
</Input>
<Spin :spinning="loading" :classes="{ root: 'w-full' }">
<Tree
@select="handleSelect"
v-if="deptTree.length > 0"
class="pt-2"
:tree-data="deptTree"
:default-expand-all="true"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
/>
<div v-else-if="!loading" class="py-4 text-center text-gray-500">
暂无数据
</div>
</Spin>
</div>
</template>

View File

@@ -52,6 +52,7 @@ export namespace ThingModelApi {
name?: string;
required?: boolean;
type?: string;
dataType?: string;
description?: string;
outputParams?: Param[];
method?: string;

View File

@@ -177,8 +177,6 @@ async function initProcessInfo(row: any, formVariables?: any) {
await router.push({
path: row.formCustomCreatePath,
});
// 返回选择流程
emit('cancel');
}
}

View File

@@ -115,7 +115,8 @@ async function handleRefuse() {
clearable: true,
maxlength: 255,
placeholder: '不填则不告知对方原因',
rows: 3
rows: 3,
type: 'textarea'
},
content: '',
defaultValue: '',

View File

@@ -41,7 +41,7 @@ const rules = {
<ElFormItem label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</ElFormItem>
<ElFormItem label="文字颜色" prop="文字颜色">
<ElFormItem label="文字颜色" prop="textColor">
<ColorInput v-model="formData.textColor" />
</ElFormItem>
<ElCard header="公告内容" class="property-group" shadow="never">

View File

@@ -415,7 +415,7 @@ function showTime(item: MallKefuMessageApi.Message, index: number) {
:rows="4"
class="border-none p-2"
placeholder="输入消息Enter发送Shift+Enter换行"
@keyup.enter="handleSendMessage"
@keydown.enter.exact.prevent="handleSendMessage"
:input-style="{ boxShadow: 'none' }"
/>
</div>

View File

@@ -40,10 +40,9 @@ const treeRef = ref(); // Tree 组件引用
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
// 获取选中的部门ID
const selectedIds: number[] = props.checkStrictly
? treeRef.value?.getCheckedKeys() || []
: selectedDeptIds.value;
// 获取选中的部门IDselectedDeptIds 由 handleCheck 实时维护,
// 严格/非严格模式下均与树勾选状态一致,无需依赖 treeRef 是否已挂载)
const selectedIds: number[] = selectedDeptIds.value;
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
@@ -102,6 +101,9 @@ function handleCheck(
if (lastSelectedId) {
selectedDeptIds.value = [lastSelectedId];
treeRef.value?.setCheckedKeys([lastSelectedId]);
} else {
selectedDeptIds.value = [];
treeRef.value?.setCheckedKeys([]);
}
}
}