From b3154ef87a34017f6e866d3d0457dd9eb2752e32 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 30 May 2026 18:10:14 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat(mes):=20=E5=A2=9E=E5=8A=A0=20componn?= =?UTF-8?q?ents=20=E8=AF=84=E5=AE=A1=E7=9A=84=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/views/mes/dv/checkplan/components/select.vue | 2 ++ apps/web-antd/src/views/mes/dv/machinery/components/select.vue | 1 + .../src/views/mes/md/client/components/select-dialog.vue | 1 + apps/web-antd/src/views/mes/md/client/components/select.vue | 1 + apps/web-antd/src/views/mes/pro/workrecord/components/index.ts | 1 - apps/web-antd/src/views/mes/pro/workrecord/index.vue | 2 +- .../mes/pro/workrecord/{components => modules}/status-bar.vue | 0 apps/web-ele/src/views/mes/pro/workrecord/components/index.ts | 1 - apps/web-ele/src/views/mes/pro/workrecord/index.vue | 2 +- .../mes/pro/workrecord/{components => modules}/status-bar.vue | 0 10 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 apps/web-antd/src/views/mes/pro/workrecord/components/index.ts rename apps/web-antd/src/views/mes/pro/workrecord/{components => modules}/status-bar.vue (100%) delete mode 100644 apps/web-ele/src/views/mes/pro/workrecord/components/index.ts rename apps/web-ele/src/views/mes/pro/workrecord/{components => modules}/status-bar.vue (100%) diff --git a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue index 1c3ab50e8..2dddc5a78 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue @@ -48,12 +48,14 @@ async function getList() { function handleChange(value: SelectValue) { const planId = typeof value === 'number' ? value : undefined; emit('update:modelValue', planId); + // TODO @AI:可以简化,不换行么? emit( 'change', list.value.find((item) => item.id === planId), ); } +// TODO @AI:下面,2 个,需要有空行么? watch(() => [props.status, props.type], getList); onMounted(getList); diff --git a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue index ea9b543c8..e37d8f430 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue @@ -39,6 +39,7 @@ function handleChange(value: SelectValue) { ); } +// TODO @AI:按照目前项目的规则,会希望 /** 初始化么 */ ?如果喜欢,是不是加到 style.vue 里? onMounted(getList); diff --git a/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue b/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue index 49b652901..80efd7181 100644 --- a/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue +++ b/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue @@ -28,6 +28,7 @@ const latestQueryRows = ref([]); // 最近一次查询 const queryFinished = ref(false); // 最近一次查询是否完成 // TODO @芋艿:是否有必要搞的这么复杂???后续测试看看。 +// TODO @AI:需要简化下么?好像只有它这里有。。。 const MAX_TABLE_READY_FRAMES = 60; /** 等待下一帧 */ diff --git a/apps/web-antd/src/views/mes/md/client/components/select.vue b/apps/web-antd/src/views/mes/md/client/components/select.vue index fe70ff96b..8a85e1919 100644 --- a/apps/web-antd/src/views/mes/md/client/components/select.vue +++ b/apps/web-antd/src/views/mes/md/client/components/select.vue @@ -38,6 +38,7 @@ const selectedItem = ref(); // 当前选中客户 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( + // TODO @AI:这种,是不是应该放到 // computed( 后面? // 是否显示清空图标 () => props.allowClear && diff --git a/apps/web-antd/src/views/mes/pro/workrecord/components/index.ts b/apps/web-antd/src/views/mes/pro/workrecord/components/index.ts deleted file mode 100644 index 5b9734865..000000000 --- a/apps/web-antd/src/views/mes/pro/workrecord/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as WorkRecordStatusBar } from './status-bar.vue'; diff --git a/apps/web-antd/src/views/mes/pro/workrecord/index.vue b/apps/web-antd/src/views/mes/pro/workrecord/index.vue index 7f08c5413..65cfb1547 100644 --- a/apps/web-antd/src/views/mes/pro/workrecord/index.vue +++ b/apps/web-antd/src/views/mes/pro/workrecord/index.vue @@ -12,8 +12,8 @@ import { } from '#/api/mes/pro/workrecord'; import { $t } from '#/locales'; -import { WorkRecordStatusBar } from './components'; import { useGridColumns, useGridFormSchema } from './data'; +import WorkRecordStatusBar from './modules/status-bar.vue'; /** 刷新表格 */ function handleRefresh() { diff --git a/apps/web-antd/src/views/mes/pro/workrecord/components/status-bar.vue b/apps/web-antd/src/views/mes/pro/workrecord/modules/status-bar.vue similarity index 100% rename from apps/web-antd/src/views/mes/pro/workrecord/components/status-bar.vue rename to apps/web-antd/src/views/mes/pro/workrecord/modules/status-bar.vue diff --git a/apps/web-ele/src/views/mes/pro/workrecord/components/index.ts b/apps/web-ele/src/views/mes/pro/workrecord/components/index.ts deleted file mode 100644 index 5b9734865..000000000 --- a/apps/web-ele/src/views/mes/pro/workrecord/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as WorkRecordStatusBar } from './status-bar.vue'; diff --git a/apps/web-ele/src/views/mes/pro/workrecord/index.vue b/apps/web-ele/src/views/mes/pro/workrecord/index.vue index 7f08c5413..65cfb1547 100644 --- a/apps/web-ele/src/views/mes/pro/workrecord/index.vue +++ b/apps/web-ele/src/views/mes/pro/workrecord/index.vue @@ -12,8 +12,8 @@ import { } from '#/api/mes/pro/workrecord'; import { $t } from '#/locales'; -import { WorkRecordStatusBar } from './components'; import { useGridColumns, useGridFormSchema } from './data'; +import WorkRecordStatusBar from './modules/status-bar.vue'; /** 刷新表格 */ function handleRefresh() { diff --git a/apps/web-ele/src/views/mes/pro/workrecord/components/status-bar.vue b/apps/web-ele/src/views/mes/pro/workrecord/modules/status-bar.vue similarity index 100% rename from apps/web-ele/src/views/mes/pro/workrecord/components/status-bar.vue rename to apps/web-ele/src/views/mes/pro/workrecord/modules/status-bar.vue From 22e9081a45839c421e159fffecb5b13c3dd30d44 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 30 May 2026 19:03:57 +0800 Subject: [PATCH 02/19] =?UTF-8?q?refactor(mes):=20=E7=BB=9F=E4=B8=80=20ant?= =?UTF-8?q?d=20=E5=92=8C=20ele=20=E9=80=89=E6=8B=A9=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 规范 select 空值判断和回显逻辑 - 统一物料、供应商、客户选择弹窗的单选/多选行为 - 清理 components 内 TODO 并修复相关 DICT_TYPE 导入 --- .../mes/dv/checkplan/components/select.vue | 10 +- .../mes/dv/machinery/components/select.vue | 15 +- .../src/views/mes/dv/machinery/type/data.ts | 4 +- .../mes/dv/subject/components/select.vue | 9 +- .../web-antd/src/views/mes/dv/subject/data.ts | 9 +- .../md/client/components/select-dialog.vue | 50 +------ .../views/mes/md/client/components/select.vue | 12 +- apps/web-antd/src/views/mes/md/client/data.ts | 4 +- .../components/product-bom-select-dialog.vue | 2 +- .../md/item/components/product-bom-select.vue | 11 +- .../mes/md/item/components/select-dialog.vue | 126 ++++++++++------- .../views/mes/md/item/components/select.vue | 11 +- apps/web-antd/src/views/mes/md/item/data.ts | 10 +- .../src/views/mes/md/item/type/data.ts | 9 +- .../md/vendor/components/select-dialog.vue | 128 +++++++++++------- .../views/mes/md/vendor/components/select.vue | 11 +- apps/web-antd/src/views/mes/md/vendor/data.ts | 10 +- .../mes/md/workstation/components/select.vue | 3 +- .../src/views/mes/md/workstation/data.ts | 4 +- .../views/mes/md/workstation/workshop/data.ts | 4 +- .../mes/dv/checkplan/components/select.vue | 15 +- .../mes/dv/machinery/components/select.vue | 21 ++- .../src/views/mes/dv/machinery/type/data.ts | 4 +- .../mes/dv/subject/components/select.vue | 16 ++- apps/web-ele/src/views/mes/dv/subject/data.ts | 9 +- .../md/client/components/select-dialog.vue | 52 ++----- .../views/mes/md/client/components/select.vue | 11 +- apps/web-ele/src/views/mes/md/client/data.ts | 4 +- .../components/product-bom-select-dialog.vue | 2 +- .../md/item/components/product-bom-select.vue | 11 +- .../mes/md/item/components/select-dialog.vue | 126 ++++++++++------- .../views/mes/md/item/components/select.vue | 11 +- apps/web-ele/src/views/mes/md/item/data.ts | 10 +- .../src/views/mes/md/item/type/data.ts | 9 +- .../md/vendor/components/select-dialog.vue | 128 +++++++++++------- .../views/mes/md/vendor/components/select.vue | 11 +- apps/web-ele/src/views/mes/md/vendor/data.ts | 10 +- .../mes/md/workstation/components/select.vue | 3 +- .../src/views/mes/md/workstation/data.ts | 4 +- .../views/mes/md/workstation/workshop/data.ts | 4 +- 40 files changed, 484 insertions(+), 419 deletions(-) diff --git a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue index 2dddc5a78..4c9a2c3d1 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue @@ -9,6 +9,8 @@ import { Select } from 'ant-design-vue'; import { getCheckPlanPage } from '#/api/mes/dv/checkplan'; +defineOptions({ name: 'DvCheckPlanSelect' }); + const props = withDefaults( defineProps<{ allowClear?: boolean; @@ -48,15 +50,11 @@ async function getList() { function handleChange(value: SelectValue) { const planId = typeof value === 'number' ? value : undefined; emit('update:modelValue', planId); - // TODO @AI:可以简化,不换行么? - emit( - 'change', - list.value.find((item) => item.id === planId), - ); + emit('change', list.value.find((item) => item.id === planId)); } -// TODO @AI:下面,2 个,需要有空行么? watch(() => [props.status, props.type], getList); + onMounted(getList); diff --git a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue index e37d8f430..f0b6a7481 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue @@ -9,6 +9,8 @@ import { Select } from 'ant-design-vue'; import { getMachinerySimpleList } from '#/api/mes/dv/machinery'; +defineOptions({ name: 'DvMachinerySelect' }); + withDefaults( defineProps<{ allowClear?: boolean; @@ -16,7 +18,12 @@ withDefaults( modelValue?: number; placeholder?: string; }>(), - { allowClear: true, disabled: false, modelValue: undefined, placeholder: '请选择设备' }, + { + allowClear: true, + disabled: false, + modelValue: undefined, + placeholder: '请选择设备', + }, ); const emit = defineEmits<{ change: [row?: MesDvMachineryApi.Machinery]; @@ -33,13 +40,9 @@ async function getList() { function handleChange(value: SelectValue) { const machineryId = typeof value === 'number' ? value : undefined; emit('update:modelValue', machineryId); - emit( - 'change', - list.value.find((item) => item.id === machineryId), - ); + emit('change', list.value.find((item) => item.id === machineryId)); } -// TODO @AI:按照目前项目的规则,会希望 /** 初始化么 */ ?如果喜欢,是不是加到 style.vue 里? onMounted(getList); diff --git a/apps/web-antd/src/views/mes/dv/machinery/type/data.ts b/apps/web-antd/src/views/mes/dv/machinery/type/data.ts index e817fcea1..604ea3054 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/type/data.ts +++ b/apps/web-antd/src/views/mes/dv/machinery/type/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvMachineryTypeApi } from '#/api/mes/dv/machinery/type'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { handleTree } from '@vben/utils'; diff --git a/apps/web-antd/src/views/mes/dv/subject/components/select.vue b/apps/web-antd/src/views/mes/dv/subject/components/select.vue index f11e16a6b..8c54234d4 100644 --- a/apps/web-antd/src/views/mes/dv/subject/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/subject/components/select.vue @@ -9,6 +9,8 @@ import { Select } from 'ant-design-vue'; import { getSubjectSimpleList } from '#/api/mes/dv/subject'; +defineOptions({ name: 'DvSubjectSelect' }); + const props = withDefaults( defineProps<{ allowClear?: boolean; @@ -30,7 +32,7 @@ const emit = defineEmits<{ 'update:modelValue': [value?: number]; }>(); const list = ref([]); // 项目列表 -const filteredList = computed( // 筛选后的项目列表 +const filteredList = computed( () => list.value.filter((item) => !props.type || item.type === props.type), ); @@ -43,10 +45,7 @@ async function getList() { function handleChange(value: SelectValue) { const subjectId = typeof value === 'number' ? value : undefined; emit('update:modelValue', subjectId); - emit( - 'change', - list.value.find((item) => item.id === subjectId), - ); + emit('change', list.value.find((item) => item.id === subjectId)); } onMounted(getList); diff --git a/apps/web-antd/src/views/mes/dv/subject/data.ts b/apps/web-antd/src/views/mes/dv/subject/data.ts index 3748e9c42..7ebc009be 100644 --- a/apps/web-antd/src/views/mes/dv/subject/data.ts +++ b/apps/web-antd/src/views/mes/dv/subject/data.ts @@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvSubjectApi } from '#/api/mes/dv/subject'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode, MesDvSubjectTypeEnum } from '@vben/constants'; +import { + CommonStatusEnum, + DICT_TYPE, + MesAutoCodeRuleCode, + MesDvSubjectTypeEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue b/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue index 80efd7181..844552a4f 100644 --- a/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue +++ b/apps/web-antd/src/views/mes/md/client/components/select-dialog.vue @@ -16,6 +16,8 @@ import { useClientSelectGridFormSchema, } from '../data'; +defineOptions({ name: 'MdClientSelectDialog' }); + const emit = defineEmits<{ selected: [rows: MesMdClientApi.Client[]]; }>(); @@ -24,44 +26,12 @@ const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 const selectedRows = ref([]); // 已选客户列表 const preSelectedIds = ref([]); // 预选客户编号列表 -const latestQueryRows = ref([]); // 最近一次查询返回的客户列表 -const queryFinished = ref(false); // 最近一次查询是否完成 - -// TODO @芋艿:是否有必要搞的这么复杂???后续测试看看。 -// TODO @AI:需要简化下么?好像只有它这里有。。。 -const MAX_TABLE_READY_FRAMES = 60; - -/** 等待下一帧 */ -function waitNextFrame(): Promise { - return new Promise((resolve) => { - requestAnimationFrame(() => resolve()); - }); -} /** 获取当前表格数据 */ function getTableRows() { return gridApi.grid.getTableData().fullData as MesMdClientApi.Client[]; } -/** 等待 VXE 将当前查询结果写入表格数据后再回显选中 */ -async function waitTableReady(): Promise { - if (preSelectedIds.value.length === 0) { - return; - } - for (let index = 0; index < MAX_TABLE_READY_FRAMES; index += 1) { - if (queryFinished.value) { - const rows = getTableRows(); - if (latestQueryRows.value.length === 0 && rows.length === 0) { - return; - } - if (latestQueryRows.value.length > 0 && rows.length > 0) { - return; - } - } - await waitNextFrame(); - } -} - /** 获取多选记录,包含 VXE reserve 跨页记录 */ function getMultipleSelectedRows() { const selectedMap = new Map(); @@ -71,7 +41,7 @@ function getMultipleSelectedRows() { ] as MesMdClientApi.Client[]; records.forEach((row) => { const rowId = row.id; - if (rowId !== null && rowId !== undefined) { + if (rowId != null) { selectedMap.set(rowId, row); } }); @@ -111,10 +81,9 @@ async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - // proxy 表格回显选中时要读取 fullData,否则首次打开可能读不到刚查询出的数据。 const rows = getTableRows(); for (const row of rows) { - if (row.id === null || !preSelectedIds.value.includes(row.id as number)) { + if (row.id == null || !preSelectedIds.value.includes(row.id)) { continue; } if (multiple.value) { @@ -150,15 +119,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ proxyConfig: { ajax: { query: async ({ page }, formValues) => { - const data = await getClientPage({ + return await getClientPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues, status: CommonStatusEnum.ENABLE, }); - latestQueryRows.value = data.list || []; - queryFinished.value = true; - return data; }, }, }, @@ -198,8 +164,6 @@ async function openModal( open.value = true; multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; - latestQueryRows.value = []; - queryFinished.value = false; await nextTick(); gridApi.setGridOptions({ columns: useClientSelectGridColumns(multiple.value), @@ -207,13 +171,13 @@ async function openModal( await resetQueryState(); await gridApi.query(); await nextTick(); - await waitTableReady(); await applyPreSelection(); } /** 关闭客户选择弹窗 */ -function closeModal() { +async function closeModal() { open.value = false; + await resetQueryState(); } /** 确认选择客户 */ diff --git a/apps/web-antd/src/views/mes/md/client/components/select.vue b/apps/web-antd/src/views/mes/md/client/components/select.vue index 8a85e1919..bbe599651 100644 --- a/apps/web-antd/src/views/mes/md/client/components/select.vue +++ b/apps/web-antd/src/views/mes/md/client/components/select.vue @@ -38,18 +38,16 @@ const selectedItem = ref(); // 当前选中客户 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // TODO @AI:这种,是不是应该放到 // computed( 后面? - // 是否显示清空图标 () => props.allowClear && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据客户编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -57,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getClient(id as number); + selectedItem.value = await getClient(id); } catch (error) { console.error('[MdClientSelect] resolveItemById failed:', error); } @@ -89,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的客户 */ diff --git a/apps/web-antd/src/views/mes/md/client/data.ts b/apps/web-antd/src/views/mes/md/client/data.ts index 8b03d6e84..3c14dc147 100644 --- a/apps/web-antd/src/views/mes/md/client/data.ts +++ b/apps/web-antd/src/views/mes/md/client/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdClientApi } from '#/api/mes/md/client'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/md/item/components/product-bom-select-dialog.vue b/apps/web-antd/src/views/mes/md/item/components/product-bom-select-dialog.vue index 9718d3255..90ae85a32 100644 --- a/apps/web-antd/src/views/mes/md/item/components/product-bom-select-dialog.vue +++ b/apps/web-antd/src/views/mes/md/item/components/product-bom-select-dialog.vue @@ -68,7 +68,7 @@ async function openModal(itemId: number, selectedBomItemId?: number) { list.value = await getProductBomListByItemId(itemId); gridApi.setGridOptions({ data: list.value }); await nextTick(); - if (selectedBomItemId !== null) { + if (selectedBomItemId != null) { const match = list.value.find( (row) => row.bomItemId === selectedBomItemId, ); diff --git a/apps/web-antd/src/views/mes/md/item/components/product-bom-select.vue b/apps/web-antd/src/views/mes/md/item/components/product-bom-select.vue index ac4964f94..a5085f356 100644 --- a/apps/web-antd/src/views/mes/md/item/components/product-bom-select.vue +++ b/apps/web-antd/src/views/mes/md/item/components/product-bom-select.vue @@ -40,17 +40,16 @@ const selectedBom = ref(); // 当前选中的 BOM const displayLabel = computed(() => selectedBom.value?.bomItemName ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.allowClear && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据 BOM 子物料编号回显选择器 */ async function resolveBomById(bomItemId: number | undefined) { - if (bomItemId === null || props.itemId === null) { + if (bomItemId == null || props.itemId == null) { selectedBom.value = undefined; return; } @@ -58,7 +57,7 @@ async function resolveBomById(bomItemId: number | undefined) { return; } try { - const list = await getProductBomListByItemId(props.itemId as number); + const list = await getProductBomListByItemId(props.itemId); selectedBom.value = list.find((item) => item.bomItemId === bomItemId); } catch (error) { console.error('[MdProductBomSelect] resolveBomById failed:', error); @@ -91,7 +90,7 @@ function clearSelected() { /** 打开 BOM 物料选择弹窗 */ function handleClick(event: MouseEvent) { - if (props.disabled || props.itemId === null) { + if (props.disabled || props.itemId == null) { return; } const target = event.target as HTMLElement; @@ -100,7 +99,7 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - dialogRef.value?.open(props.itemId as number, props.modelValue); + dialogRef.value?.open(props.itemId, props.modelValue); } /** 回填选中的 BOM 物料 */ diff --git a/apps/web-antd/src/views/mes/md/item/components/select-dialog.vue b/apps/web-antd/src/views/mes/md/item/components/select-dialog.vue index 18e3ffa99..2af087579 100644 --- a/apps/web-antd/src/views/mes/md/item/components/select-dialog.vue +++ b/apps/web-antd/src/views/mes/md/item/components/select-dialog.vue @@ -23,52 +23,58 @@ const emit = defineEmits<{ const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 -const syncingSingleSelection = ref(false); // 是否同步单选勾选状态 const selectedRows = ref([]); // 已选物料列表 const selectedItemTypeId = ref(); // 当前筛选分类编号 const preSelectedIds = ref([]); // 预选物料编号列表 const typeTreeRef = ref>(); // 物料分类树 -/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */ -async function syncSingleSelection(row?: MesMdItemApi.Item) { - syncingSingleSelection.value = true; - await nextTick(); - await gridApi.grid.clearCheckboxRow(); - if (row) { - await gridApi.grid.setCheckboxRow(row, true); - } - await nextTick(); - syncingSingleSelection.value = false; +/** 获取当前表格数据 */ +function getTableRows() { + return gridApi.grid.getTableData().fullData as MesMdItemApi.Item[]; } -/** 处理勾选变化,单选模式只保留最后一条 */ -async function handleCheckboxChange({ - checked, - records, - row, -}: { - checked: boolean; - records: MesMdItemApi.Item[]; - row?: MesMdItemApi.Item; -}) { - if (syncingSingleSelection.value) { - return; - } - if (!multiple.value) { - const selected = checked && row ? [row] : []; - selectedRows.value = selected; - await syncSingleSelection(selected[0]); - return; - } - selectedRows.value = records; +/** 获取多选记录,包含 VXE reserve 跨页记录 */ +function getMultipleSelectedRows() { + const selectedMap = new Map(); + const records = [ + ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []), + ...(gridApi.grid.getCheckboxRecords?.() ?? []), + ] as MesMdItemApi.Item[]; + records.forEach((row) => { + const rowId = row.id; + if (rowId != null) { + selectedMap.set(rowId, row); + } + }); + return [...selectedMap.values()]; } -/** 处理全选变化 */ -function handleCheckboxAll({ records }: { records: MesMdItemApi.Item[] }) { - if (syncingSingleSelection.value) { +/** 处理勾选变化 */ +function handleCheckboxSelectChange() { + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理单选变化 */ +function handleRadioChange(row: MesMdItemApi.Item) { + selectedRows.value = [row]; +} + +/** 多选模式下切换行勾选 */ +async function toggleMultipleRow(row: MesMdItemApi.Item) { + const selected = gridApi.grid.isCheckedByCheckboxRow(row); + await gridApi.grid.setCheckboxRow(row, !selected); + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理行双击 */ +async function handleCellDblclick({ row }: { row: MesMdItemApi.Item }) { + if (multiple.value) { + await toggleMultipleRow(row); return; } - selectedRows.value = records; + selectedRows.value = [row]; + await gridApi.grid.setRadioRow(row); + handleConfirm(); } /** 按分类筛选物料 */ @@ -78,18 +84,25 @@ function handleItemTypeNodeClick(row: MesMdItemTypeApi.ItemType | undefined) { } /** 回显预选物料 */ -function applyPreSelection() { +async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - const rows = gridApi.grid.getData() as MesMdItemApi.Item[]; + const rows = getTableRows(); for (const row of rows) { - if (row.id && preSelectedIds.value.includes(row.id)) { - gridApi.grid.setCheckboxRow(row, true); - if (!multiple.value) { - selectedRows.value = [row]; - } + if (row.id == null || !preSelectedIds.value.includes(row.id)) { + continue; } + if (multiple.value) { + await gridApi.grid.setCheckboxRow(row, true); + } else { + await gridApi.grid.setRadioRow(row); + selectedRows.value = [row]; + return; + } + } + if (multiple.value) { + selectedRows.value = getMultipleSelectedRows(); } } @@ -98,7 +111,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ schema: useItemSelectGridFormSchema(), }, gridOptions: { - columns: useItemSelectGridColumns(), + columns: useItemSelectGridColumns(true), height: 560, keepSource: true, checkboxConfig: { @@ -106,6 +119,10 @@ const [Grid, gridApi] = useVbenVxeGrid({ range: true, reserve: true, }, + radioConfig: { + highlight: true, + trigger: 'row', + }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { @@ -129,8 +146,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, } as VxeTableGridOptions, gridEvents: { - checkboxAll: handleCheckboxAll, - checkboxChange: handleCheckboxChange, + cellDblclick: handleCellDblclick, + checkboxAll: handleCheckboxSelectChange, + checkboxChange: handleCheckboxSelectChange, + radioChange: ({ row }: { row: MesMdItemApi.Item }) => { + handleRadioChange(row); + }, }, }); @@ -140,6 +161,8 @@ async function resetQueryState() { selectedRows.value = []; typeTreeRef.value?.reset(); await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.clearRadioRow(); await gridApi.formApi.resetForm(); } @@ -152,10 +175,13 @@ async function openModal( multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; await nextTick(); + gridApi.setGridOptions({ + columns: useItemSelectGridColumns(multiple.value), + }); await resetQueryState(); await gridApi.query(); await nextTick(); - applyPreSelection(); + await applyPreSelection(); } /** 关闭物料选择弹窗 */ @@ -166,14 +192,12 @@ async function closeModal() { /** 确认选择物料 */ function handleConfirm() { - if (selectedRows.value.length === 0) { + const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value; + if (rows.length === 0) { message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); return; } - emit( - 'selected', - multiple.value ? selectedRows.value : [selectedRows.value[0]!], - ); + emit('selected', multiple.value ? rows : [rows[0]!]); open.value = false; } diff --git a/apps/web-antd/src/views/mes/md/item/components/select.vue b/apps/web-antd/src/views/mes/md/item/components/select.vue index 22bd15384..bb62ec9a4 100644 --- a/apps/web-antd/src/views/mes/md/item/components/select.vue +++ b/apps/web-antd/src/views/mes/md/item/components/select.vue @@ -38,17 +38,16 @@ const selectedItem = ref(); // 当前选中物料 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.allowClear && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据物料编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -56,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getItem(id as number); + selectedItem.value = await getItem(id); } catch (error) { console.error('[MdItemSelect] resolveItemById failed:', error); } @@ -88,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的物料 */ diff --git a/apps/web-antd/src/views/mes/md/item/data.ts b/apps/web-antd/src/views/mes/md/item/data.ts index 21e55f602..d2915d6fd 100644 --- a/apps/web-antd/src/views/mes/md/item/data.ts +++ b/apps/web-antd/src/views/mes/md/item/data.ts @@ -3,9 +3,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdItemApi } from '#/api/mes/md/item'; import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom'; -import { DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; @@ -356,9 +356,11 @@ export function useItemSelectGridFormSchema(): VbenFormSchema[] { } /** 物料选择弹窗列表字段 */ -export function useItemSelectGridColumns(): VxeTableGridOptions['columns'] { +export function useItemSelectGridColumns( + multiple = true, +): VxeTableGridOptions['columns'] { return [ - { type: 'checkbox', width: 50 }, + { type: multiple ? 'checkbox' : 'radio', width: 50 }, { field: 'code', title: '物料编码', diff --git a/apps/web-antd/src/views/mes/md/item/type/data.ts b/apps/web-antd/src/views/mes/md/item/type/data.ts index a5a8c7549..ebab1d063 100644 --- a/apps/web-antd/src/views/mes/md/item/type/data.ts +++ b/apps/web-antd/src/views/mes/md/item/type/data.ts @@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdItemTypeApi } from '#/api/mes/md/item/type'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode, MesItemOrProductEnum } from '@vben/constants'; +import { + CommonStatusEnum, + DICT_TYPE, + MesAutoCodeRuleCode, + MesItemOrProductEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { handleTree } from '@vben/utils'; diff --git a/apps/web-antd/src/views/mes/md/vendor/components/select-dialog.vue b/apps/web-antd/src/views/mes/md/vendor/components/select-dialog.vue index 7a3acd77d..9837b5d7a 100644 --- a/apps/web-antd/src/views/mes/md/vendor/components/select-dialog.vue +++ b/apps/web-antd/src/views/mes/md/vendor/components/select-dialog.vue @@ -16,71 +16,86 @@ import { useVendorSelectGridFormSchema, } from '../data'; +defineOptions({ name: 'MdVendorSelectDialog' }); + const emit = defineEmits<{ selected: [rows: MesMdVendorApi.Vendor[]]; }>(); const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 -const syncingSingleSelection = ref(false); // 是否同步单选勾选状态 const selectedRows = ref([]); // 已选供应商列表 const preSelectedIds = ref([]); // 预选供应商编号列表 -/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */ -async function syncSingleSelection(row?: MesMdVendorApi.Vendor) { - syncingSingleSelection.value = true; - await nextTick(); - await gridApi.grid.clearCheckboxRow(); - if (row) { - await gridApi.grid.setCheckboxRow(row, true); - } - await nextTick(); - syncingSingleSelection.value = false; +/** 获取当前表格数据 */ +function getTableRows() { + return gridApi.grid.getTableData().fullData as MesMdVendorApi.Vendor[]; } -/** 处理勾选变化,单选模式只保留最后一条 */ -async function handleCheckboxChange({ - checked, - records, - row, -}: { - checked: boolean; - records: MesMdVendorApi.Vendor[]; - row?: MesMdVendorApi.Vendor; -}) { - if (syncingSingleSelection.value) { - return; - } - if (!multiple.value) { - const selected = checked && row ? [row] : []; - selectedRows.value = selected; - await syncSingleSelection(selected[0]); - return; - } - selectedRows.value = records; +/** 获取多选记录,包含 VXE reserve 跨页记录 */ +function getMultipleSelectedRows() { + const selectedMap = new Map(); + const records = [ + ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []), + ...(gridApi.grid.getCheckboxRecords?.() ?? []), + ] as MesMdVendorApi.Vendor[]; + records.forEach((row) => { + const rowId = row.id; + if (rowId != null) { + selectedMap.set(rowId, row); + } + }); + return [...selectedMap.values()]; } -/** 处理全选变化 */ -function handleCheckboxAll({ records }: { records: MesMdVendorApi.Vendor[] }) { - if (syncingSingleSelection.value) { +/** 处理勾选变化 */ +function handleCheckboxSelectChange() { + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理单选变化 */ +function handleRadioChange(row: MesMdVendorApi.Vendor) { + selectedRows.value = [row]; +} + +/** 多选模式下切换行勾选 */ +async function toggleMultipleRow(row: MesMdVendorApi.Vendor) { + const selected = gridApi.grid.isCheckedByCheckboxRow(row); + await gridApi.grid.setCheckboxRow(row, !selected); + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理行双击 */ +async function handleCellDblclick({ row }: { row: MesMdVendorApi.Vendor }) { + if (multiple.value) { + await toggleMultipleRow(row); return; } - selectedRows.value = records; + selectedRows.value = [row]; + await gridApi.grid.setRadioRow(row); + handleConfirm(); } /** 回显预选供应商 */ -function applyPreSelection() { +async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - const rows = gridApi.grid.getData() as MesMdVendorApi.Vendor[]; + const rows = getTableRows(); for (const row of rows) { - if (row.id && preSelectedIds.value.includes(row.id)) { - gridApi.grid.setCheckboxRow(row, true); - if (!multiple.value) { - selectedRows.value = [row]; - } + if (row.id == null || !preSelectedIds.value.includes(row.id)) { + continue; } + if (multiple.value) { + await gridApi.grid.setCheckboxRow(row, true); + } else { + await gridApi.grid.setRadioRow(row); + selectedRows.value = [row]; + return; + } + } + if (multiple.value) { + selectedRows.value = getMultipleSelectedRows(); } } @@ -89,7 +104,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ schema: useVendorSelectGridFormSchema(), }, gridOptions: { - columns: useVendorSelectGridColumns(), + columns: useVendorSelectGridColumns(true), height: 520, keepSource: true, checkboxConfig: { @@ -97,6 +112,10 @@ const [Grid, gridApi] = useVbenVxeGrid({ range: true, reserve: true, }, + radioConfig: { + highlight: true, + trigger: 'row', + }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { @@ -119,8 +138,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, } as VxeTableGridOptions, gridEvents: { - checkboxAll: handleCheckboxAll, - checkboxChange: handleCheckboxChange, + cellDblclick: handleCellDblclick, + checkboxAll: handleCheckboxSelectChange, + checkboxChange: handleCheckboxSelectChange, + radioChange: ({ row }: { row: MesMdVendorApi.Vendor }) => { + handleRadioChange(row); + }, }, }); @@ -128,6 +151,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ async function resetQueryState() { selectedRows.value = []; await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.clearRadioRow(); await gridApi.formApi.resetForm(); } @@ -140,10 +165,13 @@ async function openModal( multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; await nextTick(); + gridApi.setGridOptions({ + columns: useVendorSelectGridColumns(multiple.value), + }); await resetQueryState(); await gridApi.query(); await nextTick(); - applyPreSelection(); + await applyPreSelection(); } /** 关闭供应商选择弹窗 */ @@ -154,14 +182,12 @@ async function closeModal() { /** 确认选择供应商 */ function handleConfirm() { - if (selectedRows.value.length === 0) { + const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value; + if (rows.length === 0) { message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); return; } - emit( - 'selected', - multiple.value ? selectedRows.value : [selectedRows.value[0]!], - ); + emit('selected', multiple.value ? rows : [rows[0]!]); open.value = false; } diff --git a/apps/web-antd/src/views/mes/md/vendor/components/select.vue b/apps/web-antd/src/views/mes/md/vendor/components/select.vue index a202650f6..cc6e756b9 100644 --- a/apps/web-antd/src/views/mes/md/vendor/components/select.vue +++ b/apps/web-antd/src/views/mes/md/vendor/components/select.vue @@ -38,17 +38,16 @@ const selectedItem = ref(); // 当前选中供应商 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.allowClear && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据供应商编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -56,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getVendor(id as number); + selectedItem.value = await getVendor(id); } catch (error) { console.error('[MdVendorSelect] resolveItemById failed:', error); } @@ -88,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的供应商 */ diff --git a/apps/web-antd/src/views/mes/md/vendor/data.ts b/apps/web-antd/src/views/mes/md/vendor/data.ts index 1395328b8..c9accb2c8 100644 --- a/apps/web-antd/src/views/mes/md/vendor/data.ts +++ b/apps/web-antd/src/views/mes/md/vendor/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdVendorApi } from '#/api/mes/md/vendor'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; @@ -416,9 +416,11 @@ export function useVendorSelectGridFormSchema(): VbenFormSchema[] { } /** 供应商选择弹窗的字段 */ -export function useVendorSelectGridColumns(): VxeTableGridOptions['columns'] { +export function useVendorSelectGridColumns( + multiple = true, +): VxeTableGridOptions['columns'] { return [ - { type: 'checkbox', width: 50 }, + { type: multiple ? 'checkbox' : 'radio', width: 50 }, { field: 'code', title: '供应商编码', diff --git a/apps/web-antd/src/views/mes/md/workstation/components/select.vue b/apps/web-antd/src/views/mes/md/workstation/components/select.vue index 4f65aedea..fd4d6ecfd 100644 --- a/apps/web-antd/src/views/mes/md/workstation/components/select.vue +++ b/apps/web-antd/src/views/mes/md/workstation/components/select.vue @@ -40,12 +40,11 @@ const selectedItem = ref(); // 选中的工作 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.allowClear && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据工作站编号回显选择器 */ diff --git a/apps/web-antd/src/views/mes/md/workstation/data.ts b/apps/web-antd/src/views/mes/md/workstation/data.ts index 3807d5db7..f5201219c 100644 --- a/apps/web-antd/src/views/mes/md/workstation/data.ts +++ b/apps/web-antd/src/views/mes/md/workstation/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; -import { DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/md/workstation/workshop/data.ts b/apps/web-antd/src/views/mes/md/workstation/workshop/data.ts index 1db3673b4..eb0937316 100644 --- a/apps/web-antd/src/views/mes/md/workstation/workshop/data.ts +++ b/apps/web-antd/src/views/mes/md/workstation/workshop/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdWorkshopApi } from '#/api/mes/md/workstation/workshop'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-ele/src/views/mes/dv/checkplan/components/select.vue b/apps/web-ele/src/views/mes/dv/checkplan/components/select.vue index 81d905dc3..12009bbc8 100644 --- a/apps/web-ele/src/views/mes/dv/checkplan/components/select.vue +++ b/apps/web-ele/src/views/mes/dv/checkplan/components/select.vue @@ -7,6 +7,8 @@ import { ElOption, ElSelect } from 'element-plus'; import { getCheckPlanPage } from '#/api/mes/dv/checkplan'; +defineOptions({ name: 'DvCheckPlanSelect' }); + const props = withDefaults( defineProps<{ clearable?: boolean; @@ -46,13 +48,11 @@ async function getList() { function handleChange(value: number | string | undefined) { const planId = typeof value === 'number' ? value : undefined; emit('update:modelValue', planId); - emit( - 'change', - list.value.find((item) => item.id === planId), - ); + emit('change', list.value.find((item) => item.id === planId)); } watch(() => [props.status, props.type], getList); + onMounted(getList); @@ -66,6 +66,11 @@ onMounted(getList); filterable @change="handleChange" > - + diff --git a/apps/web-ele/src/views/mes/dv/machinery/components/select.vue b/apps/web-ele/src/views/mes/dv/machinery/components/select.vue index 09f55a66b..f16f10e0e 100644 --- a/apps/web-ele/src/views/mes/dv/machinery/components/select.vue +++ b/apps/web-ele/src/views/mes/dv/machinery/components/select.vue @@ -7,6 +7,8 @@ import { ElOption, ElSelect } from 'element-plus'; import { getMachinerySimpleList } from '#/api/mes/dv/machinery'; +defineOptions({ name: 'DvMachinerySelect' }); + withDefaults( defineProps<{ clearable?: boolean; @@ -14,7 +16,12 @@ withDefaults( modelValue?: number; placeholder?: string; }>(), - { clearable: true, disabled: false, modelValue: undefined, placeholder: '请选择设备' }, + { + clearable: true, + disabled: false, + modelValue: undefined, + placeholder: '请选择设备', + }, ); const emit = defineEmits<{ change: [row?: MesDvMachineryApi.Machinery]; @@ -31,10 +38,7 @@ async function getList() { function handleChange(value: number | string | undefined) { const machineryId = typeof value === 'number' ? value : undefined; emit('update:modelValue', machineryId); - emit( - 'change', - list.value.find((item) => item.id === machineryId), - ); + emit('change', list.value.find((item) => item.id === machineryId)); } onMounted(getList); @@ -50,6 +54,11 @@ onMounted(getList); filterable @change="handleChange" > - + diff --git a/apps/web-ele/src/views/mes/dv/machinery/type/data.ts b/apps/web-ele/src/views/mes/dv/machinery/type/data.ts index e5a20b1c7..8ec5ebb5d 100644 --- a/apps/web-ele/src/views/mes/dv/machinery/type/data.ts +++ b/apps/web-ele/src/views/mes/dv/machinery/type/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvMachineryTypeApi } from '#/api/mes/dv/machinery/type'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { handleTree } from '@vben/utils'; diff --git a/apps/web-ele/src/views/mes/dv/subject/components/select.vue b/apps/web-ele/src/views/mes/dv/subject/components/select.vue index 36e75fb68..918efe5ee 100644 --- a/apps/web-ele/src/views/mes/dv/subject/components/select.vue +++ b/apps/web-ele/src/views/mes/dv/subject/components/select.vue @@ -7,6 +7,8 @@ import { ElOption, ElSelect } from 'element-plus'; import { getSubjectSimpleList } from '#/api/mes/dv/subject'; +defineOptions({ name: 'DvSubjectSelect' }); + const props = withDefaults( defineProps<{ clearable?: boolean; @@ -28,7 +30,7 @@ const emit = defineEmits<{ 'update:modelValue': [value?: number]; }>(); const list = ref([]); // 项目列表 -const filteredList = computed( // 筛选后的项目列表 +const filteredList = computed( () => list.value.filter((item) => !props.type || item.type === props.type), ); @@ -41,10 +43,7 @@ async function getList() { function handleChange(value: number | string | undefined) { const subjectId = typeof value === 'number' ? value : undefined; emit('update:modelValue', subjectId); - emit( - 'change', - list.value.find((item) => item.id === subjectId), - ); + emit('change', list.value.find((item) => item.id === subjectId)); } onMounted(getList); @@ -60,6 +59,11 @@ onMounted(getList); filterable @change="handleChange" > - + diff --git a/apps/web-ele/src/views/mes/dv/subject/data.ts b/apps/web-ele/src/views/mes/dv/subject/data.ts index 5a778f118..f35bffd20 100644 --- a/apps/web-ele/src/views/mes/dv/subject/data.ts +++ b/apps/web-ele/src/views/mes/dv/subject/data.ts @@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvSubjectApi } from '#/api/mes/dv/subject'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode, MesDvSubjectTypeEnum } from '@vben/constants'; +import { + CommonStatusEnum, + DICT_TYPE, + MesAutoCodeRuleCode, + MesDvSubjectTypeEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; diff --git a/apps/web-ele/src/views/mes/md/client/components/select-dialog.vue b/apps/web-ele/src/views/mes/md/client/components/select-dialog.vue index 570b5bdff..8076bdee5 100644 --- a/apps/web-ele/src/views/mes/md/client/components/select-dialog.vue +++ b/apps/web-ele/src/views/mes/md/client/components/select-dialog.vue @@ -16,6 +16,8 @@ import { useClientSelectGridFormSchema, } from '../data'; +defineOptions({ name: 'MdClientSelectDialog' }); + const emit = defineEmits<{ selected: [rows: MesMdClientApi.Client[]]; }>(); @@ -24,43 +26,12 @@ const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 const selectedRows = ref([]); // 已选客户列表 const preSelectedIds = ref([]); // 预选客户编号列表 -const latestQueryRows = ref([]); // 最近一次查询返回的客户列表 -const queryFinished = ref(false); // 最近一次查询是否完成 - -// TODO @芋艿:是否有必要搞的这么复杂???后续测试看看。 -const MAX_TABLE_READY_FRAMES = 60; - -/** 等待下一帧 */ -function waitNextFrame(): Promise { - return new Promise((resolve) => { - requestAnimationFrame(() => resolve()); - }); -} /** 获取当前表格数据 */ function getTableRows() { return gridApi.grid.getTableData().fullData as MesMdClientApi.Client[]; } -/** 等待 VXE 将当前查询结果写入表格数据后再回显选中 */ -async function waitTableReady(): Promise { - if (preSelectedIds.value.length === 0) { - return; - } - for (let index = 0; index < MAX_TABLE_READY_FRAMES; index += 1) { - if (queryFinished.value) { - const rows = getTableRows(); - if (latestQueryRows.value.length === 0 && rows.length === 0) { - return; - } - if (latestQueryRows.value.length > 0 && rows.length > 0) { - return; - } - } - await waitNextFrame(); - } -} - /** 获取多选记录,包含 VXE reserve 跨页记录 */ function getMultipleSelectedRows() { const selectedMap = new Map(); @@ -69,8 +40,9 @@ function getMultipleSelectedRows() { ...(gridApi.grid.getCheckboxRecords?.() ?? []), ] as MesMdClientApi.Client[]; records.forEach((row) => { - if (row.id !== null) { - selectedMap.set(row.id as number, row); + const rowId = row.id; + if (rowId != null) { + selectedMap.set(rowId, row); } }); return [...selectedMap.values()]; @@ -109,10 +81,9 @@ async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - // proxy 表格回显选中时要读取 fullData,否则首次打开可能读不到刚查询出的数据。 const rows = getTableRows(); for (const row of rows) { - if (row.id === null || !preSelectedIds.value.includes(row.id as number)) { + if (row.id == null || !preSelectedIds.value.includes(row.id)) { continue; } if (multiple.value) { @@ -148,15 +119,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ proxyConfig: { ajax: { query: async ({ page }, formValues) => { - const data = await getClientPage({ + return await getClientPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues, status: CommonStatusEnum.ENABLE, }); - latestQueryRows.value = data.list || []; - queryFinished.value = true; - return data; }, }, }, @@ -196,8 +164,6 @@ async function openModal( open.value = true; multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; - latestQueryRows.value = []; - queryFinished.value = false; await nextTick(); gridApi.setGridOptions({ columns: useClientSelectGridColumns(multiple.value), @@ -205,13 +171,13 @@ async function openModal( await resetQueryState(); await gridApi.query(); await nextTick(); - await waitTableReady(); await applyPreSelection(); } /** 关闭客户选择弹窗 */ -function closeModal() { +async function closeModal() { open.value = false; + await resetQueryState(); } /** 确认选择客户 */ diff --git a/apps/web-ele/src/views/mes/md/client/components/select.vue b/apps/web-ele/src/views/mes/md/client/components/select.vue index eda669a31..6fe4417f0 100644 --- a/apps/web-ele/src/views/mes/md/client/components/select.vue +++ b/apps/web-ele/src/views/mes/md/client/components/select.vue @@ -38,17 +38,16 @@ const selectedItem = ref(); // 当前选中客户 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.clearable && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据客户编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -56,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getClient(id as number); + selectedItem.value = await getClient(id); } catch (error) { console.error('[MdClientSelect] resolveItemById failed:', error); } @@ -88,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的客户 */ diff --git a/apps/web-ele/src/views/mes/md/client/data.ts b/apps/web-ele/src/views/mes/md/client/data.ts index e254e0576..97363e232 100644 --- a/apps/web-ele/src/views/mes/md/client/data.ts +++ b/apps/web-ele/src/views/mes/md/client/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdClientApi } from '#/api/mes/md/client'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; diff --git a/apps/web-ele/src/views/mes/md/item/components/product-bom-select-dialog.vue b/apps/web-ele/src/views/mes/md/item/components/product-bom-select-dialog.vue index ebde34dcd..0938dc898 100644 --- a/apps/web-ele/src/views/mes/md/item/components/product-bom-select-dialog.vue +++ b/apps/web-ele/src/views/mes/md/item/components/product-bom-select-dialog.vue @@ -68,7 +68,7 @@ async function openModal(itemId: number, selectedBomItemId?: number) { list.value = await getProductBomListByItemId(itemId); gridApi.setGridOptions({ data: list.value }); await nextTick(); - if (selectedBomItemId !== null) { + if (selectedBomItemId != null) { const match = list.value.find( (row) => row.bomItemId === selectedBomItemId, ); diff --git a/apps/web-ele/src/views/mes/md/item/components/product-bom-select.vue b/apps/web-ele/src/views/mes/md/item/components/product-bom-select.vue index 2a8ce7e1e..9661df112 100644 --- a/apps/web-ele/src/views/mes/md/item/components/product-bom-select.vue +++ b/apps/web-ele/src/views/mes/md/item/components/product-bom-select.vue @@ -40,17 +40,16 @@ const selectedBom = ref(); // 当前选中的 BOM const displayLabel = computed(() => selectedBom.value?.bomItemName ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.clearable && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据 BOM 子物料编号回显选择器 */ async function resolveBomById(bomItemId: number | undefined) { - if (bomItemId === null || props.itemId === null) { + if (bomItemId == null || props.itemId == null) { selectedBom.value = undefined; return; } @@ -58,7 +57,7 @@ async function resolveBomById(bomItemId: number | undefined) { return; } try { - const list = await getProductBomListByItemId(props.itemId as number); + const list = await getProductBomListByItemId(props.itemId); selectedBom.value = list.find((item) => item.bomItemId === bomItemId); } catch (error) { console.error('[MdProductBomSelect] resolveBomById failed:', error); @@ -91,7 +90,7 @@ function clearSelected() { /** 打开 BOM 物料选择弹窗 */ function handleClick(event: MouseEvent) { - if (props.disabled || props.itemId === null) { + if (props.disabled || props.itemId == null) { return; } const target = event.target as HTMLElement; @@ -100,7 +99,7 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - dialogRef.value?.open(props.itemId as number, props.modelValue); + dialogRef.value?.open(props.itemId, props.modelValue); } /** 回填选中的 BOM 物料 */ diff --git a/apps/web-ele/src/views/mes/md/item/components/select-dialog.vue b/apps/web-ele/src/views/mes/md/item/components/select-dialog.vue index 51f1cc0a5..e0985673c 100644 --- a/apps/web-ele/src/views/mes/md/item/components/select-dialog.vue +++ b/apps/web-ele/src/views/mes/md/item/components/select-dialog.vue @@ -23,52 +23,58 @@ const emit = defineEmits<{ const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 -const syncingSingleSelection = ref(false); // 是否同步单选勾选状态 const selectedRows = ref([]); // 已选物料列表 const selectedItemTypeId = ref(); // 当前筛选分类编号 const preSelectedIds = ref([]); // 预选物料编号列表 const typeTreeRef = ref>(); // 物料分类树 -/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */ -async function syncSingleSelection(row?: MesMdItemApi.Item) { - syncingSingleSelection.value = true; - await nextTick(); - await gridApi.grid.clearCheckboxRow(); - if (row) { - await gridApi.grid.setCheckboxRow(row, true); - } - await nextTick(); - syncingSingleSelection.value = false; +/** 获取当前表格数据 */ +function getTableRows() { + return gridApi.grid.getTableData().fullData as MesMdItemApi.Item[]; } -/** 处理勾选变化,单选模式只保留最后一条 */ -async function handleCheckboxChange({ - checked, - records, - row, -}: { - checked: boolean; - records: MesMdItemApi.Item[]; - row?: MesMdItemApi.Item; -}) { - if (syncingSingleSelection.value) { - return; - } - if (!multiple.value) { - const selected = checked && row ? [row] : []; - selectedRows.value = selected; - await syncSingleSelection(selected[0]); - return; - } - selectedRows.value = records; +/** 获取多选记录,包含 VXE reserve 跨页记录 */ +function getMultipleSelectedRows() { + const selectedMap = new Map(); + const records = [ + ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []), + ...(gridApi.grid.getCheckboxRecords?.() ?? []), + ] as MesMdItemApi.Item[]; + records.forEach((row) => { + const rowId = row.id; + if (rowId != null) { + selectedMap.set(rowId, row); + } + }); + return [...selectedMap.values()]; } -/** 处理全选变化 */ -function handleCheckboxAll({ records }: { records: MesMdItemApi.Item[] }) { - if (syncingSingleSelection.value) { +/** 处理勾选变化 */ +function handleCheckboxSelectChange() { + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理单选变化 */ +function handleRadioChange(row: MesMdItemApi.Item) { + selectedRows.value = [row]; +} + +/** 多选模式下切换行勾选 */ +async function toggleMultipleRow(row: MesMdItemApi.Item) { + const selected = gridApi.grid.isCheckedByCheckboxRow(row); + await gridApi.grid.setCheckboxRow(row, !selected); + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理行双击 */ +async function handleCellDblclick({ row }: { row: MesMdItemApi.Item }) { + if (multiple.value) { + await toggleMultipleRow(row); return; } - selectedRows.value = records; + selectedRows.value = [row]; + await gridApi.grid.setRadioRow(row); + handleConfirm(); } /** 按分类筛选物料 */ @@ -78,18 +84,25 @@ function handleItemTypeNodeClick(row: MesMdItemTypeApi.ItemType | undefined) { } /** 回显预选物料 */ -function applyPreSelection() { +async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - const rows = gridApi.grid.getData() as MesMdItemApi.Item[]; + const rows = getTableRows(); for (const row of rows) { - if (row.id && preSelectedIds.value.includes(row.id)) { - gridApi.grid.setCheckboxRow(row, true); - if (!multiple.value) { - selectedRows.value = [row]; - } + if (row.id == null || !preSelectedIds.value.includes(row.id)) { + continue; } + if (multiple.value) { + await gridApi.grid.setCheckboxRow(row, true); + } else { + await gridApi.grid.setRadioRow(row); + selectedRows.value = [row]; + return; + } + } + if (multiple.value) { + selectedRows.value = getMultipleSelectedRows(); } } @@ -98,7 +111,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ schema: useItemSelectGridFormSchema(), }, gridOptions: { - columns: useItemSelectGridColumns(), + columns: useItemSelectGridColumns(true), height: 560, keepSource: true, checkboxConfig: { @@ -106,6 +119,10 @@ const [Grid, gridApi] = useVbenVxeGrid({ range: true, reserve: true, }, + radioConfig: { + highlight: true, + trigger: 'row', + }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { @@ -129,8 +146,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, } as VxeTableGridOptions, gridEvents: { - checkboxAll: handleCheckboxAll, - checkboxChange: handleCheckboxChange, + cellDblclick: handleCellDblclick, + checkboxAll: handleCheckboxSelectChange, + checkboxChange: handleCheckboxSelectChange, + radioChange: ({ row }: { row: MesMdItemApi.Item }) => { + handleRadioChange(row); + }, }, }); @@ -140,6 +161,8 @@ async function resetQueryState() { selectedRows.value = []; typeTreeRef.value?.reset(); await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.clearRadioRow(); await gridApi.formApi.resetForm(); } @@ -152,10 +175,13 @@ async function openModal( multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; await nextTick(); + gridApi.setGridOptions({ + columns: useItemSelectGridColumns(multiple.value), + }); await resetQueryState(); await gridApi.query(); await nextTick(); - applyPreSelection(); + await applyPreSelection(); } /** 关闭物料选择弹窗 */ @@ -166,14 +192,12 @@ async function closeModal() { /** 确认选择物料 */ function handleConfirm() { - if (selectedRows.value.length === 0) { + const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value; + if (rows.length === 0) { ElMessage.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); return; } - emit( - 'selected', - multiple.value ? selectedRows.value : [selectedRows.value[0]!], - ); + emit('selected', multiple.value ? rows : [rows[0]!]); open.value = false; } diff --git a/apps/web-ele/src/views/mes/md/item/components/select.vue b/apps/web-ele/src/views/mes/md/item/components/select.vue index 577c7a669..71de21f95 100644 --- a/apps/web-ele/src/views/mes/md/item/components/select.vue +++ b/apps/web-ele/src/views/mes/md/item/components/select.vue @@ -38,17 +38,16 @@ const selectedItem = ref(); // 当前选中物料 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.clearable && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据物料编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -56,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getItem(id as number); + selectedItem.value = await getItem(id); } catch (error) { console.error('[MdItemSelect] resolveItemById failed:', error); } @@ -88,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的物料 */ diff --git a/apps/web-ele/src/views/mes/md/item/data.ts b/apps/web-ele/src/views/mes/md/item/data.ts index 5a6de2a82..970423c67 100644 --- a/apps/web-ele/src/views/mes/md/item/data.ts +++ b/apps/web-ele/src/views/mes/md/item/data.ts @@ -3,9 +3,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdItemApi } from '#/api/mes/md/item'; import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom'; -import { DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; @@ -360,9 +360,11 @@ export function useItemSelectGridFormSchema(): VbenFormSchema[] { } /** 物料选择弹窗列表字段 */ -export function useItemSelectGridColumns(): VxeTableGridOptions['columns'] { +export function useItemSelectGridColumns( + multiple = true, +): VxeTableGridOptions['columns'] { return [ - { type: 'checkbox', width: 50 }, + { type: multiple ? 'checkbox' : 'radio', width: 50 }, { field: 'code', title: '物料编码', diff --git a/apps/web-ele/src/views/mes/md/item/type/data.ts b/apps/web-ele/src/views/mes/md/item/type/data.ts index bc6152f8e..fd3b623e9 100644 --- a/apps/web-ele/src/views/mes/md/item/type/data.ts +++ b/apps/web-ele/src/views/mes/md/item/type/data.ts @@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdItemTypeApi } from '#/api/mes/md/item/type'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode, MesItemOrProductEnum } from '@vben/constants'; +import { + CommonStatusEnum, + DICT_TYPE, + MesAutoCodeRuleCode, + MesItemOrProductEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { handleTree } from '@vben/utils'; diff --git a/apps/web-ele/src/views/mes/md/vendor/components/select-dialog.vue b/apps/web-ele/src/views/mes/md/vendor/components/select-dialog.vue index 3d42266bc..b37d2e68c 100644 --- a/apps/web-ele/src/views/mes/md/vendor/components/select-dialog.vue +++ b/apps/web-ele/src/views/mes/md/vendor/components/select-dialog.vue @@ -16,71 +16,86 @@ import { useVendorSelectGridFormSchema, } from '../data'; +defineOptions({ name: 'MdVendorSelectDialog' }); + const emit = defineEmits<{ selected: [rows: MesMdVendorApi.Vendor[]]; }>(); const open = ref(false); // 弹窗是否打开 const multiple = ref(true); // 是否多选 -const syncingSingleSelection = ref(false); // 是否同步单选勾选状态 const selectedRows = ref([]); // 已选供应商列表 const preSelectedIds = ref([]); // 预选供应商编号列表 -/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */ -async function syncSingleSelection(row?: MesMdVendorApi.Vendor) { - syncingSingleSelection.value = true; - await nextTick(); - await gridApi.grid.clearCheckboxRow(); - if (row) { - await gridApi.grid.setCheckboxRow(row, true); - } - await nextTick(); - syncingSingleSelection.value = false; +/** 获取当前表格数据 */ +function getTableRows() { + return gridApi.grid.getTableData().fullData as MesMdVendorApi.Vendor[]; } -/** 处理勾选变化,单选模式只保留最后一条 */ -async function handleCheckboxChange({ - checked, - records, - row, -}: { - checked: boolean; - records: MesMdVendorApi.Vendor[]; - row?: MesMdVendorApi.Vendor; -}) { - if (syncingSingleSelection.value) { - return; - } - if (!multiple.value) { - const selected = checked && row ? [row] : []; - selectedRows.value = selected; - await syncSingleSelection(selected[0]); - return; - } - selectedRows.value = records; +/** 获取多选记录,包含 VXE reserve 跨页记录 */ +function getMultipleSelectedRows() { + const selectedMap = new Map(); + const records = [ + ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []), + ...(gridApi.grid.getCheckboxRecords?.() ?? []), + ] as MesMdVendorApi.Vendor[]; + records.forEach((row) => { + const rowId = row.id; + if (rowId != null) { + selectedMap.set(rowId, row); + } + }); + return [...selectedMap.values()]; } -/** 处理全选变化 */ -function handleCheckboxAll({ records }: { records: MesMdVendorApi.Vendor[] }) { - if (syncingSingleSelection.value) { +/** 处理勾选变化 */ +function handleCheckboxSelectChange() { + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理单选变化 */ +function handleRadioChange(row: MesMdVendorApi.Vendor) { + selectedRows.value = [row]; +} + +/** 多选模式下切换行勾选 */ +async function toggleMultipleRow(row: MesMdVendorApi.Vendor) { + const selected = gridApi.grid.isCheckedByCheckboxRow(row); + await gridApi.grid.setCheckboxRow(row, !selected); + selectedRows.value = getMultipleSelectedRows(); +} + +/** 处理行双击 */ +async function handleCellDblclick({ row }: { row: MesMdVendorApi.Vendor }) { + if (multiple.value) { + await toggleMultipleRow(row); return; } - selectedRows.value = records; + selectedRows.value = [row]; + await gridApi.grid.setRadioRow(row); + handleConfirm(); } /** 回显预选供应商 */ -function applyPreSelection() { +async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } - const rows = gridApi.grid.getData() as MesMdVendorApi.Vendor[]; + const rows = getTableRows(); for (const row of rows) { - if (row.id && preSelectedIds.value.includes(row.id)) { - gridApi.grid.setCheckboxRow(row, true); - if (!multiple.value) { - selectedRows.value = [row]; - } + if (row.id == null || !preSelectedIds.value.includes(row.id)) { + continue; } + if (multiple.value) { + await gridApi.grid.setCheckboxRow(row, true); + } else { + await gridApi.grid.setRadioRow(row); + selectedRows.value = [row]; + return; + } + } + if (multiple.value) { + selectedRows.value = getMultipleSelectedRows(); } } @@ -89,7 +104,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ schema: useVendorSelectGridFormSchema(), }, gridOptions: { - columns: useVendorSelectGridColumns(), + columns: useVendorSelectGridColumns(true), height: 520, keepSource: true, checkboxConfig: { @@ -97,6 +112,10 @@ const [Grid, gridApi] = useVbenVxeGrid({ range: true, reserve: true, }, + radioConfig: { + highlight: true, + trigger: 'row', + }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { @@ -119,8 +138,12 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, } as VxeTableGridOptions, gridEvents: { - checkboxAll: handleCheckboxAll, - checkboxChange: handleCheckboxChange, + cellDblclick: handleCellDblclick, + checkboxAll: handleCheckboxSelectChange, + checkboxChange: handleCheckboxSelectChange, + radioChange: ({ row }: { row: MesMdVendorApi.Vendor }) => { + handleRadioChange(row); + }, }, }); @@ -128,6 +151,8 @@ const [Grid, gridApi] = useVbenVxeGrid({ async function resetQueryState() { selectedRows.value = []; await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.clearRadioRow(); await gridApi.formApi.resetForm(); } @@ -140,10 +165,13 @@ async function openModal( multiple.value = options?.multiple ?? true; preSelectedIds.value = selectedIds || []; await nextTick(); + gridApi.setGridOptions({ + columns: useVendorSelectGridColumns(multiple.value), + }); await resetQueryState(); await gridApi.query(); await nextTick(); - applyPreSelection(); + await applyPreSelection(); } /** 关闭供应商选择弹窗 */ @@ -154,14 +182,12 @@ async function closeModal() { /** 确认选择供应商 */ function handleConfirm() { - if (selectedRows.value.length === 0) { + const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value; + if (rows.length === 0) { ElMessage.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); return; } - emit( - 'selected', - multiple.value ? selectedRows.value : [selectedRows.value[0]!], - ); + emit('selected', multiple.value ? rows : [rows[0]!]); open.value = false; } diff --git a/apps/web-ele/src/views/mes/md/vendor/components/select.vue b/apps/web-ele/src/views/mes/md/vendor/components/select.vue index 3a378b992..2d1973464 100644 --- a/apps/web-ele/src/views/mes/md/vendor/components/select.vue +++ b/apps/web-ele/src/views/mes/md/vendor/components/select.vue @@ -38,17 +38,16 @@ const selectedItem = ref(); // 当前选中供应商 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.clearable && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据供应商编号回显选择器 */ async function resolveItemById(id: number | undefined) { - if (id === null) { + if (id == null) { selectedItem.value = undefined; return; } @@ -56,7 +55,7 @@ async function resolveItemById(id: number | undefined) { return; } try { - selectedItem.value = await getVendor(id as number); + selectedItem.value = await getVendor(id); } catch (error) { console.error('[MdVendorSelect] resolveItemById failed:', error); } @@ -88,8 +87,8 @@ function handleClick(event: MouseEvent) { clearSelected(); return; } - const selectedIds = props.modelValue === null ? [] : [props.modelValue]; - dialogRef.value?.open(selectedIds as number[], { multiple: false }); + const selectedIds = props.modelValue == null ? [] : [props.modelValue]; + dialogRef.value?.open(selectedIds, { multiple: false }); } /** 回填选中的供应商 */ diff --git a/apps/web-ele/src/views/mes/md/vendor/data.ts b/apps/web-ele/src/views/mes/md/vendor/data.ts index 7f10ba16e..3aed1d87c 100644 --- a/apps/web-ele/src/views/mes/md/vendor/data.ts +++ b/apps/web-ele/src/views/mes/md/vendor/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdVendorApi } from '#/api/mes/md/vendor'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; @@ -415,9 +415,11 @@ export function useVendorSelectGridFormSchema(): VbenFormSchema[] { } /** 供应商选择弹窗的字段 */ -export function useVendorSelectGridColumns(): VxeTableGridOptions['columns'] { +export function useVendorSelectGridColumns( + multiple = true, +): VxeTableGridOptions['columns'] { return [ - { type: 'checkbox', width: 50 }, + { type: multiple ? 'checkbox' : 'radio', width: 50 }, { field: 'code', title: '供应商编码', diff --git a/apps/web-ele/src/views/mes/md/workstation/components/select.vue b/apps/web-ele/src/views/mes/md/workstation/components/select.vue index 1f34206a8..c8b677e3e 100644 --- a/apps/web-ele/src/views/mes/md/workstation/components/select.vue +++ b/apps/web-ele/src/views/mes/md/workstation/components/select.vue @@ -40,12 +40,11 @@ const selectedItem = ref(); // 选中的工作 const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 选择器展示名称 const showClear = computed( - // 是否显示清空图标 () => props.clearable && !props.disabled && hovering.value && - props.modelValue !== null, + props.modelValue != null, ); /** 根据工作站编号回显选择器 */ diff --git a/apps/web-ele/src/views/mes/md/workstation/data.ts b/apps/web-ele/src/views/mes/md/workstation/data.ts index f074a6170..6825b3868 100644 --- a/apps/web-ele/src/views/mes/md/workstation/data.ts +++ b/apps/web-ele/src/views/mes/md/workstation/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; -import { DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; diff --git a/apps/web-ele/src/views/mes/md/workstation/workshop/data.ts b/apps/web-ele/src/views/mes/md/workstation/workshop/data.ts index b7ba9f169..a3eae1bc1 100644 --- a/apps/web-ele/src/views/mes/md/workstation/workshop/data.ts +++ b/apps/web-ele/src/views/mes/md/workstation/workshop/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesMdWorkshopApi } from '#/api/mes/md/workstation/workshop'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { ElButton } from 'element-plus'; From 3f7134d3fc335044407a1ea0eb141ed6328ce7b3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 30 May 2026 19:36:16 +0800 Subject: [PATCH 03/19] =?UTF-8?q?fix(mes):=20=E4=BF=AE=E5=A4=8D=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=20review=20=E7=AC=AC=E4=B8=80=E6=89=B9=205=20?= =?UTF-8?q?=E4=B8=AA=20bug=EF=BC=88B001=20vue=20=E8=AF=AF=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=20P0=20+=20B002-B005=20P1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/mes/dv/checkrecord/modules/line-list.vue | 4 ++-- .../views/mes/dv/maintenrecord/modules/line-list.vue | 4 ++-- apps/web-antd/src/views/mes/pro/process/data.ts | 4 ++-- apps/web-antd/src/views/mes/pro/route/data.ts | 4 ++-- apps/web-antd/src/views/mes/qc/template/data.ts | 4 ++-- apps/web-antd/src/views/mes/wm/barcode/data.ts | 10 ++++++++-- .../web-antd/src/views/mes/wm/barcode/modules/form.vue | 4 ++-- apps/web-antd/src/views/mes/wm/itemreceipt/data.ts | 1 - apps/web-antd/src/views/mes/wm/materialstock/data.ts | 9 ++++++++- apps/web-antd/src/views/mes/wm/returnsales/data.ts | 1 - .../web-antd/src/views/mes/wm/stocktaking/plan/data.ts | 9 +++++++-- .../mes/wm/stocktaking/plan/modules/param-form.vue | 4 ++-- apps/web-antd/src/views/mes/wm/warehouse/area/data.ts | 4 ++-- .../src/views/mes/dv/checkrecord/modules/line-list.vue | 4 ++-- .../views/mes/dv/maintenrecord/modules/line-list.vue | 4 ++-- apps/web-ele/src/views/mes/pro/process/data.ts | 4 ++-- apps/web-ele/src/views/mes/pro/route/data.ts | 4 ++-- apps/web-ele/src/views/mes/qc/template/data.ts | 4 ++-- apps/web-ele/src/views/mes/wm/barcode/data.ts | 10 ++++++++-- apps/web-ele/src/views/mes/wm/barcode/modules/form.vue | 4 ++-- apps/web-ele/src/views/mes/wm/itemreceipt/data.ts | 1 - apps/web-ele/src/views/mes/wm/materialstock/data.ts | 9 ++++++++- apps/web-ele/src/views/mes/wm/returnsales/data.ts | 1 - apps/web-ele/src/views/mes/wm/stocktaking/plan/data.ts | 9 +++++++-- .../mes/wm/stocktaking/plan/modules/param-form.vue | 4 ++-- apps/web-ele/src/views/mes/wm/warehouse/area/data.ts | 4 ++-- 26 files changed, 78 insertions(+), 46 deletions(-) diff --git a/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue b/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue index 9debff5fd..3b77b79fd 100644 --- a/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue +++ b/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue @@ -2,9 +2,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvCheckRecordLineApi } from '#/api/mes/dv/checkrecord/line'; -import { computed, MesDvCheckResultEnum, MesDvSubjectTypeEnum, ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; -import { DICT_TYPE } from '@vben/constants'; +import { DICT_TYPE, MesDvCheckResultEnum, MesDvSubjectTypeEnum } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { message, Modal } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue b/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue index 1b03fa032..9c16b9c74 100644 --- a/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue +++ b/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue @@ -2,9 +2,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvMaintenRecordLineApi } from '#/api/mes/dv/maintenrecord/line'; -import { computed, MesDvMaintenStatusEnum, MesDvSubjectTypeEnum, ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; -import { DICT_TYPE } from '@vben/constants'; +import { DICT_TYPE, MesDvMaintenStatusEnum, MesDvSubjectTypeEnum } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { message, Modal } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/pro/process/data.ts b/apps/web-antd/src/views/mes/pro/process/data.ts index a5d37c260..65e915c43 100644 --- a/apps/web-antd/src/views/mes/pro/process/data.ts +++ b/apps/web-antd/src/views/mes/pro/process/data.ts @@ -3,9 +3,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProProcessApi } from '#/api/mes/pro/process'; import type { MesProProcessContentApi } from '#/api/mes/pro/process/content'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/pro/route/data.ts b/apps/web-antd/src/views/mes/pro/route/data.ts index 24ce8c65d..a6640473e 100644 --- a/apps/web-antd/src/views/mes/pro/route/data.ts +++ b/apps/web-antd/src/views/mes/pro/route/data.ts @@ -5,9 +5,9 @@ import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/qc/template/data.ts b/apps/web-antd/src/views/mes/qc/template/data.ts index 48536ec1c..ee865a60d 100644 --- a/apps/web-antd/src/views/mes/qc/template/data.ts +++ b/apps/web-antd/src/views/mes/qc/template/data.ts @@ -4,9 +4,9 @@ import type { MesQcTemplateApi } from '#/api/mes/qc/template'; import type { MesQcTemplateIndicatorApi } from '#/api/mes/qc/template/indicator'; import type { MesQcTemplateItemApi } from '#/api/mes/qc/template/item'; -import { DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { Button } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/wm/barcode/data.ts b/apps/web-antd/src/views/mes/wm/barcode/data.ts index 4d79c7b3b..a33e1a598 100644 --- a/apps/web-antd/src/views/mes/wm/barcode/data.ts +++ b/apps/web-antd/src/views/mes/wm/barcode/data.ts @@ -4,9 +4,14 @@ import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode'; import type { MesWmBarcodeConfigApi } from '#/api/mes/wm/barcode/config'; import type { DescriptionItemSchema } from '#/components/description'; -import { BarcodeBizTypeEnum, DICT_TYPE, h, markRaw } from 'vue'; +import { h, markRaw } from 'vue'; -import { CommonStatusEnum } from '@vben/constants'; +import { + BarcodeBizTypeEnum, + CommonStatusEnum, + DICT_TYPE, + MesProWorkOrderStatusEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { formatDateTime } from '@vben/utils'; @@ -222,6 +227,7 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { label: '工单', component: markRaw(ProWorkOrderSelect), componentProps: { + status: MesProWorkOrderStatusEnum.CONFIRMED, onChange: (item: any) => syncBizDetail(formApi, item), }, dependencies: { diff --git a/apps/web-antd/src/views/mes/wm/barcode/modules/form.vue b/apps/web-antd/src/views/mes/wm/barcode/modules/form.vue index 553e9416e..e6530fa51 100644 --- a/apps/web-antd/src/views/mes/wm/barcode/modules/form.vue +++ b/apps/web-antd/src/views/mes/wm/barcode/modules/form.vue @@ -1,10 +1,10 @@ + + diff --git a/apps/web-antd/src/views/system/notify/template/components/index.ts b/apps/web-antd/src/views/system/notify/template/components/index.ts new file mode 100644 index 000000000..42715dfa6 --- /dev/null +++ b/apps/web-antd/src/views/system/notify/template/components/index.ts @@ -0,0 +1 @@ +export { default as NotifyTemplateSelect } from './select.vue'; diff --git a/apps/web-antd/src/views/system/notify/template/components/select.vue b/apps/web-antd/src/views/system/notify/template/components/select.vue new file mode 100644 index 000000000..7dbe59290 --- /dev/null +++ b/apps/web-antd/src/views/system/notify/template/components/select.vue @@ -0,0 +1,82 @@ + + + diff --git a/apps/web-antd/src/views/system/sms/template/components/index.ts b/apps/web-antd/src/views/system/sms/template/components/index.ts new file mode 100644 index 000000000..0b1f9ad98 --- /dev/null +++ b/apps/web-antd/src/views/system/sms/template/components/index.ts @@ -0,0 +1 @@ +export { default as SmsTemplateSelect } from './select.vue'; diff --git a/apps/web-antd/src/views/system/sms/template/components/select.vue b/apps/web-antd/src/views/system/sms/template/components/select.vue new file mode 100644 index 000000000..188536173 --- /dev/null +++ b/apps/web-antd/src/views/system/sms/template/components/select.vue @@ -0,0 +1,82 @@ + + + diff --git a/apps/web-ele/src/api/iot/alert/config/index.ts b/apps/web-ele/src/api/iot/alert/config/index.ts index d419a5563..a1d3bdfcc 100644 --- a/apps/web-ele/src/api/iot/alert/config/index.ts +++ b/apps/web-ele/src/api/iot/alert/config/index.ts @@ -14,6 +14,9 @@ export namespace AlertConfigApi { receiveUserIds?: number[]; receiveUserNames?: string[]; receiveTypes?: number[]; + smsTemplateCode?: string; + mailTemplateCode?: string; + notifyTemplateCode?: string; createTime?: Date; } } diff --git a/apps/web-ele/src/api/system/mail/template/index.ts b/apps/web-ele/src/api/system/mail/template/index.ts index 57f722cf5..fffd5d62b 100644 --- a/apps/web-ele/src/api/system/mail/template/index.ts +++ b/apps/web-ele/src/api/system/mail/template/index.ts @@ -17,6 +17,13 @@ export namespace SystemMailTemplateApi { createTime: Date; } + /** 邮件模版精简信息 */ + export interface MailTemplateSimple { + id: number; + name: string; + code: string; + } + /** 邮件发送信息 */ export interface MailSendReqVO { toMails: string[]; @@ -35,6 +42,13 @@ export function getMailTemplatePage(params: PageParam) { ); } +/** 查询邮件模版精简列表 */ +export function getSimpleMailTemplateList() { + return requestClient.get( + '/system/mail-template/simple-list', + ); +} + /** 查询邮件模版详情 */ export function getMailTemplate(id: number) { return requestClient.get( diff --git a/apps/web-ele/src/api/system/notify/template/index.ts b/apps/web-ele/src/api/system/notify/template/index.ts index dd19f4b8f..92c7d7bcc 100644 --- a/apps/web-ele/src/api/system/notify/template/index.ts +++ b/apps/web-ele/src/api/system/notify/template/index.ts @@ -16,6 +16,13 @@ export namespace SystemNotifyTemplateApi { remark: string; } + /** 站内信模板精简信息 */ + export interface NotifyTemplateSimple { + id: number; + name: string; + code: string; + } + /** 发送站内信请求 */ export interface NotifySendReqVO { userId: number; @@ -33,6 +40,13 @@ export function getNotifyTemplatePage(params: PageParam) { ); } +/** 查询站内信模板精简列表 */ +export function getSimpleNotifyTemplateList() { + return requestClient.get( + '/system/notify-template/simple-list', + ); +} + /** 查询站内信模板详情 */ export function getNotifyTemplate(id: number) { return requestClient.get( diff --git a/apps/web-ele/src/api/system/sms/template/index.ts b/apps/web-ele/src/api/system/sms/template/index.ts index eccfb911e..5cfc5ca9b 100644 --- a/apps/web-ele/src/api/system/sms/template/index.ts +++ b/apps/web-ele/src/api/system/sms/template/index.ts @@ -19,6 +19,13 @@ export namespace SystemSmsTemplateApi { createTime?: Date; } + /** 短信模板精简信息 */ + export interface SmsTemplateSimple { + id: number; + name: string; + code: string; + } + /** 发送短信请求 */ export interface SmsSendReqVO { mobile: string; @@ -35,6 +42,13 @@ export function getSmsTemplatePage(params: PageParam) { ); } +/** 查询短信模板精简列表 */ +export function getSimpleSmsTemplateList() { + return requestClient.get( + '/system/sms-template/simple-list', + ); +} + /** 查询短信模板详情 */ export function getSmsTemplate(id: number) { return requestClient.get( diff --git a/apps/web-ele/src/views/iot/alert/config/data.ts b/apps/web-ele/src/views/iot/alert/config/data.ts index 7508f4471..ec8a5a576 100644 --- a/apps/web-ele/src/views/iot/alert/config/data.ts +++ b/apps/web-ele/src/views/iot/alert/config/data.ts @@ -2,12 +2,25 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { AlertConfigApi } from '#/api/iot/alert/config'; -import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; +import { markRaw } from 'vue'; + +import { + CommonStatusEnum, + DICT_TYPE, + IotAlertReceiveTypeEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { getSimpleRuleSceneList } from '#/api/iot/rule/scene'; import { getSimpleUserList } from '#/api/system/user'; import { getRangePickerDefaultProps } from '#/utils'; +import { MailTemplateSelect } from '#/views/system/mail/template/components'; +import { NotifyTemplateSelect } from '#/views/system/notify/template/components'; +import { SmsTemplateSelect } from '#/views/system/sms/template/components'; + +function hasReceiveType(values: Partial>, type: number) { + return Array.isArray(values.receiveTypes) && values.receiveTypes.includes(type); +} /** 新增/修改告警配置的表单 */ export function useFormSchema(): VbenFormSchema[] { @@ -98,6 +111,60 @@ export function useFormSchema(): VbenFormSchema[] { defaultValue: [], rules: 'required', }, + { + fieldName: 'smsTemplateCode', + label: '短信模板', + component: markRaw(SmsTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.SMS), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.SMS) && + values.smsTemplateCode + ) { + formApi.setFieldValue('smsTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, + { + fieldName: 'mailTemplateCode', + label: '邮件模板', + component: markRaw(MailTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL) && + values.mailTemplateCode + ) { + formApi.setFieldValue('mailTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, + { + fieldName: 'notifyTemplateCode', + label: '站内信模板', + component: markRaw(NotifyTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY) && + values.notifyTemplateCode + ) { + formApi.setFieldValue('notifyTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, ]; } diff --git a/apps/web-ele/src/views/system/mail/template/components/index.ts b/apps/web-ele/src/views/system/mail/template/components/index.ts new file mode 100644 index 000000000..4acfb35e7 --- /dev/null +++ b/apps/web-ele/src/views/system/mail/template/components/index.ts @@ -0,0 +1 @@ +export { default as MailTemplateSelect } from './select.vue'; diff --git a/apps/web-ele/src/views/system/mail/template/components/select.vue b/apps/web-ele/src/views/system/mail/template/components/select.vue new file mode 100644 index 000000000..d99a29f2e --- /dev/null +++ b/apps/web-ele/src/views/system/mail/template/components/select.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/web-ele/src/views/system/notify/template/components/index.ts b/apps/web-ele/src/views/system/notify/template/components/index.ts new file mode 100644 index 000000000..42715dfa6 --- /dev/null +++ b/apps/web-ele/src/views/system/notify/template/components/index.ts @@ -0,0 +1 @@ +export { default as NotifyTemplateSelect } from './select.vue'; diff --git a/apps/web-ele/src/views/system/notify/template/components/select.vue b/apps/web-ele/src/views/system/notify/template/components/select.vue new file mode 100644 index 000000000..22bd8aa89 --- /dev/null +++ b/apps/web-ele/src/views/system/notify/template/components/select.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/web-ele/src/views/system/sms/template/components/index.ts b/apps/web-ele/src/views/system/sms/template/components/index.ts new file mode 100644 index 000000000..0b1f9ad98 --- /dev/null +++ b/apps/web-ele/src/views/system/sms/template/components/index.ts @@ -0,0 +1 @@ +export { default as SmsTemplateSelect } from './select.vue'; diff --git a/apps/web-ele/src/views/system/sms/template/components/select.vue b/apps/web-ele/src/views/system/sms/template/components/select.vue new file mode 100644 index 000000000..23a8e5fb9 --- /dev/null +++ b/apps/web-ele/src/views/system/sms/template/components/select.vue @@ -0,0 +1,84 @@ + + + diff --git a/packages/constants/src/biz-iot-enum.ts b/packages/constants/src/biz-iot-enum.ts index 5275bc9da..b7ca4b895 100644 --- a/packages/constants/src/biz-iot-enum.ts +++ b/packages/constants/src/biz-iot-enum.ts @@ -85,6 +85,14 @@ export const CodecTypeEnum = { ALINK: 'Alink', // 阿里云 Alink 协议 } as const; +// ========== IOT - 告警模块 ========== +/** IoT 告警接收方式枚举,与后端 IotAlertReceiveTypeEnum 保持一致 */ +export const IotAlertReceiveTypeEnum = { + SMS: 1, // 短信 + MAIL: 2, // 邮箱 + NOTIFY: 3, // 站内信 +} as const; + // ========== IOT - 物模型 ========== /** IoT 产品物模型类型枚举类 */ export const IoTThingModelTypeEnum = { From 1edaf023c23cd824d7ff554996d5b30b62f43ecc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 30 May 2026 22:15:55 +0800 Subject: [PATCH 06/19] =?UTF-8?q?refactor(mes):=20stocktaking/plan=20?= =?UTF-8?q?=E7=9B=98=E7=82=B9=E6=9D=A1=E4=BB=B6=E8=A1=A8=E5=8D=95=20schema?= =?UTF-8?q?=20=E5=8C=96=EF=BC=88R034=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将盘点方案条件弹窗从手写
模板重构为 useVbenForm + schema,提升 antd/ele 复用度,对齐 Vben5「schema 放 data.ts」约定。 - 新增 condition-value-input.vue(antd/ele 各一份):把「条件值」的 7 选 1 (仓库/库区/库位级联、物料、批次、质量状态)+ 级联临时态 + valueCode/ valueName 回写封装为单一 v-model 组件,经 valueChange 事件回填,避免 schema 出现重复 fieldName 的渲染层风险。归 components/,与 pro/route 的 RouteColorPicker 等同类自定义控件保持一致。 - useParamFormSchema 合并进同目录 data.ts,删除临时 param-data.ts; param-form.vue 改为 useVbenForm + setState(schema),onConfirm/onOpenChange 结构与注释(// 提交表单、// 关闭并提示、// 加载数据、// 设置到 values) 对齐 dv/subject 范式。 - 字典 options 用 getDictOptions(..., 'number') + NumberDictDataType 断言, 移除冗余 .map() 转换。 - defineProps/defineEmits 内去掉非 vben 风格的 JSDoc 注释;去掉无谓的 ParamTypeEnum alias,直接使用 MesWmStockTakingParamTypeEnum。 - 质量状态无实体 id,提交校验按 type 区分 valueCode / valueId。 验证: - pnpm exec eslint (antd + ele)通过 - pnpm -F @vben/web-antd / @vben/web-ele exec vue-tsc 过滤 stocktaking/plan 无报错 Ref: project_duibiao/mes/review_vben/INDEX.md (MES-R034) --- .../plan/components/condition-value-input.vue | 204 ++++++++++ .../wm/stocktaking/plan/components/index.ts | 1 + .../src/views/mes/wm/stocktaking/plan/data.ts | 86 +++- .../stocktaking/plan/modules/param-form.vue | 234 ++--------- apps/web-ele/src/views/mes/wm/batch/data.ts | 372 ++++++++++++++++++ .../plan/components/condition-value-input.vue | 210 ++++++++++ .../wm/stocktaking/plan/components/index.ts | 1 + .../src/views/mes/wm/stocktaking/plan/data.ts | 87 +++- .../stocktaking/plan/modules/param-form.vue | 255 ++---------- 9 files changed, 1035 insertions(+), 415 deletions(-) create mode 100644 apps/web-antd/src/views/mes/wm/stocktaking/plan/components/condition-value-input.vue create mode 100644 apps/web-ele/src/views/mes/wm/batch/data.ts create mode 100644 apps/web-ele/src/views/mes/wm/stocktaking/plan/components/condition-value-input.vue diff --git a/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/condition-value-input.vue b/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/condition-value-input.vue new file mode 100644 index 000000000..b753f7bdd --- /dev/null +++ b/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/condition-value-input.vue @@ -0,0 +1,204 @@ + + + diff --git a/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/index.ts b/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/index.ts index 5a374ab09..2d492d050 100644 --- a/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/index.ts +++ b/apps/web-antd/src/views/mes/wm/stocktaking/plan/components/index.ts @@ -1,2 +1,3 @@ +export { default as StockTakingPlanConditionValueInput } from './condition-value-input.vue'; export { default as StockTakingPlanSelectDialog } from './select-dialog.vue'; export { default as StockTakingPlanSelect } from './select.vue'; diff --git a/apps/web-antd/src/views/mes/wm/stocktaking/plan/data.ts b/apps/web-antd/src/views/mes/wm/stocktaking/plan/data.ts index 056230f58..0253ad4ab 100644 --- a/apps/web-antd/src/views/mes/wm/stocktaking/plan/data.ts +++ b/apps/web-antd/src/views/mes/wm/stocktaking/plan/data.ts @@ -1,9 +1,11 @@ +import type { NumberDictDataType } from '@vben/hooks'; + import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesWmStockTakingPlanApi } from '#/api/mes/wm/stocktaking/plan'; import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan/param'; -import { h } from 'vue'; +import { h, markRaw } from 'vue'; import { CommonStatusEnum, @@ -18,6 +20,8 @@ import { Button } from 'ant-design-vue'; import { z } from '#/adapter/form'; import { generateAutoCode } from '#/api/mes/md/autocode/record'; +import { StockTakingPlanConditionValueInput } from './components'; + /** 表单类型 */ export type FormType = 'create' | 'detail' | 'update'; @@ -292,6 +296,86 @@ export function useParamGridColumns( ]; } +/** 盘点方案条件表单 schema */ +export function useParamFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { + return [ + { + fieldName: 'type', + label: '条件类型', + component: 'Select', + componentProps: { + options: getDictOptions( + DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE, + 'number', + ) as NumberDictDataType[], + placeholder: '请选择条件类型', + // 条件类型变化:清空已选条件值,避免残留旧类型的条件值 + onChange: async () => { + await formApi?.setValues({ + valueCode: '', + valueId: undefined, + valueName: '', + }); + }, + }, + rules: 'selectRequired', + }, + { + fieldName: 'valueId', + label: '条件值', + component: markRaw(StockTakingPlanConditionValueInput), + // 条件值控件内部按条件类型切换选择器,仅选择类型后展示 + dependencies: { + triggerFields: ['type'], + if: (values) => values.type != null, + componentProps: (values) => ({ + type: values.type, + valueCode: values.valueCode, + // 条件值控件回填 valueId / valueCode / valueName + onValueChange: async (payload: { + valueCode?: string; + valueId?: number; + valueName?: string; + }) => { + await formApi?.setValues({ + valueCode: payload.valueCode ?? '', + valueId: payload.valueId, + valueName: payload.valueName ?? '', + }); + }, + }), + }, + }, + { + // 条件值编码:由条件值控件回写,隐藏字段 + fieldName: 'valueCode', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + // 条件值名称:由条件值控件回写,隐藏字段 + fieldName: 'valueName', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} + /** 选择弹窗的搜索表单 */ export function useSelectGridFormSchema(): VbenFormSchema[] { return [ diff --git a/apps/web-antd/src/views/mes/wm/stocktaking/plan/modules/param-form.vue b/apps/web-antd/src/views/mes/wm/stocktaking/plan/modules/param-form.vue index 6ccb0ca3e..807a77eb0 100644 --- a/apps/web-antd/src/views/mes/wm/stocktaking/plan/modules/param-form.vue +++ b/apps/web-antd/src/views/mes/wm/stocktaking/plan/modules/param-form.vue @@ -4,144 +4,68 @@ import type { MesWmStockTakingPlanParamApi } from '#/api/mes/wm/stocktaking/plan import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; -import { DICT_TYPE, MesWmStockTakingParamTypeEnum } from '@vben/constants'; -import { getDictOptions } from '@vben/hooks'; +import { MesWmStockTakingParamTypeEnum } from '@vben/constants'; -import { Form, message, Select, Textarea } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; +import { useVbenForm } from '#/adapter/form'; import { createStockTakingPlanParam, getStockTakingPlanParam, updateStockTakingPlanParam, } from '#/api/mes/wm/stocktaking/plan/param'; -import { getWarehouseArea } from '#/api/mes/wm/warehouse/area'; -import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location'; import { $t } from '#/locales'; -import { MdItemSelect } from '#/views/mes/md/item/components'; -import { WmBatchSelect } from '#/views/mes/wm/batch/components'; -import { - WmWarehouseAreaSelect, - WmWarehouseLocationSelect, - WmWarehouseSelect, -} from '#/views/mes/wm/warehouse/components'; + +import { useParamFormSchema } from '../data'; const emit = defineEmits(['success']); -const formData = ref({}); -const planId = ref(); // TODO @AI:这里是不是要尾注释 -const locationWarehouseId = ref(); // 库区选择器临时数据:选仓库后传给库区选择器 -const areaWarehouseId = ref(); // 库位选择器临时数据:选仓库后传给库区选择器 -const areaLocationId = ref(); // 库位选择器临时数据:选库区后传给库位选择器 +const formId = ref(); // 当前编辑的条件编号 +const planId = ref(); // 所属盘点方案编号 -// TODO @AI:按照项目的风格,不要 mapping;直接界面那处理掉;简化逻辑; -const paramTypeOptions = getDictOptions( - DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE, - 'number', -).map(({ label, value }) => ({ label, value: Number(value) })); -const qualityStatusOptions = getDictOptions( - DICT_TYPE.MES_WM_QUALITY_STATUS, - 'string', -).map(({ label, value }) => ({ label, value: String(value) })); - -// TODO @AI:如果 title 和前面的 const 也是变量,不用空行; const getTitle = computed(() => - formData.value?.id + formId.value ? $t('ui.actionTitle.edit', ['盘点条件']) : $t('ui.actionTitle.create', ['盘点条件']), ); -/** 条件类型变化:清空已选条件值和级联临时数据 */ -function handleTypeChange() { - formData.value.valueId = undefined; - formData.value.valueCode = ''; - formData.value.valueName = ''; - locationWarehouseId.value = undefined; - areaWarehouseId.value = undefined; - areaLocationId.value = undefined; -} - -/** 通用选择器变化:回填条件值编码、名称 */ -function handleSelectorChange(item?: any) { - formData.value.valueId = item?.id; - formData.value.valueCode = item?.code || ''; - formData.value.valueName = item?.name || item?.nickname || ''; -} - -/** 批次选择器变化 */ -function handleBatchChange(batch?: any) { - formData.value.valueId = batch?.id; - formData.value.valueCode = batch?.code || ''; - formData.value.valueName = batch?.code || ''; -} - -/** 质量状态选择器变化:无实体编号,仅记录字典编码和文案 */ -function handleQualityStatusChange(value: any) { - const selected = qualityStatusOptions.find((item) => item.value === value); - formData.value.valueId = undefined; - formData.value.valueCode = value; - formData.value.valueName = selected?.label || ''; -} - -/** 库区仓库选择回调:清空库区 */ -function handleLocationWarehouseChange() { - formData.value.valueId = undefined; - formData.value.valueCode = ''; - formData.value.valueName = ''; -} - -/** 库位仓库选择回调:清空库区和库位 */ -function handleAreaWarehouseChange() { - areaLocationId.value = undefined; - formData.value.valueId = undefined; - formData.value.valueCode = ''; - formData.value.valueName = ''; -} - -/** 库位库区选择回调:清空库位 */ -function handleAreaLocationChange() { - formData.value.valueId = undefined; - formData.value.valueCode = ''; - formData.value.valueName = ''; -} - -/** 编辑时回填级联选择器的上级数据(库区所属仓库、库位所属仓库/库区) */ -async function loadCascadeData() { - if (!formData.value.type || !formData.value.valueId) { - return; - } - const valueId = formData.value.valueId; - if (formData.value.type === MesWmStockTakingParamTypeEnum.LOCATION) { - const location = await getWarehouseLocation(valueId); - locationWarehouseId.value = location?.warehouseId; - } else if (formData.value.type === MesWmStockTakingParamTypeEnum.AREA) { - const area = await getWarehouseArea(valueId); - areaWarehouseId.value = area?.warehouseId; - areaLocationId.value = area?.locationId; - } -} +const [Form, formApi] = useVbenForm({ + commonConfig: { + componentProps: { + class: 'w-full', + }, + }, + layout: 'horizontal', + schema: [], + showDefaultActions: false, + wrapperClass: 'grid-cols-1', +}); const [Modal, modalApi] = useVbenModal({ async onConfirm() { - if (!formData.value.type) { - message.warning('请选择条件类型'); + const { valid } = await formApi.validate(); + if (!valid) { return; } + const values = + (await formApi.getValues()) as MesWmStockTakingPlanParamApi.StockTakingPlanParam; // 质量状态校验 valueCode,其余类型校验 valueId - const valid = - formData.value.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS - ? !!formData.value.valueCode - : formData.value.valueId != null; - if (!valid) { + const valueValid = + values.type === MesWmStockTakingParamTypeEnum.QUALITY_STATUS + ? !!values.valueCode + : values.valueId != null; + if (!valueValid) { message.warning('请选择条件值'); return; } modalApi.lock(); // 提交表单 const data = { - ...formData.value, + ...values, + id: formId.value, planId: planId.value, } as MesWmStockTakingPlanParamApi.StockTakingPlanParam; try { - await (formData.value.id + await (formId.value ? updateStockTakingPlanParam(data) : createStockTakingPlanParam(data)); // 关闭并提示 @@ -154,113 +78,31 @@ const [Modal, modalApi] = useVbenModal({ }, async onOpenChange(isOpen: boolean) { if (!isOpen) { - formData.value = {}; - locationWarehouseId.value = undefined; - areaWarehouseId.value = undefined; - areaLocationId.value = undefined; + formId.value = undefined; return; } + formApi.setState({ schema: useParamFormSchema(formApi) }); // 加载数据 const data = modalApi.getData<{ id?: number; planId: number }>(); planId.value = data.planId; + formId.value = data.id; if (!data.id) { return; } modalApi.lock(); try { - formData.value = await getStockTakingPlanParam(data.id); - await loadCascadeData(); + const param = await getStockTakingPlanParam(data.id); + // 设置到 values + await formApi.setValues(param); } finally { modalApi.unlock(); } }, }); - -const ParamTypeEnum = MesWmStockTakingParamTypeEnum; -