From 8eb479696e556f2d901e1cce89c7a9e0bf51e51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Thu, 18 Jun 2026 17:04:14 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=8A=A0=E5=B7=A5=E8=B4=B9=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E9=87=8D=E6=9E=84=EF=BC=881/3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/inline-field-editor.vue | 186 +++++++ .../process/components/method-info-bar.vue | 118 ++++ .../views/system/process/components/modal.vue | 49 +- .../process/components/move-pid-modal.vue | 122 ++++ .../process/components/volume-modal.vue | 79 --- .../process/composables/useProcessTree.ts | 214 +++++++ .../src/views/system/process/config/form.ts | 70 ++- .../views/system/process/config/note-table.ts | 45 ++ .../src/views/system/process/config/search.ts | 37 -- .../src/views/system/process/config/table.ts | 119 ---- .../src/views/system/process/constants.ts | 37 ++ .../src/views/system/process/index.vue | 521 ++++++++++++------ 12 files changed, 1170 insertions(+), 427 deletions(-) create mode 100644 apps/web-antd/src/views/system/process/components/inline-field-editor.vue create mode 100644 apps/web-antd/src/views/system/process/components/method-info-bar.vue create mode 100644 apps/web-antd/src/views/system/process/components/move-pid-modal.vue delete mode 100644 apps/web-antd/src/views/system/process/components/volume-modal.vue create mode 100644 apps/web-antd/src/views/system/process/composables/useProcessTree.ts create mode 100644 apps/web-antd/src/views/system/process/config/note-table.ts delete mode 100644 apps/web-antd/src/views/system/process/config/search.ts delete mode 100644 apps/web-antd/src/views/system/process/config/table.ts create mode 100644 apps/web-antd/src/views/system/process/constants.ts diff --git a/apps/web-antd/src/views/system/process/components/inline-field-editor.vue b/apps/web-antd/src/views/system/process/components/inline-field-editor.vue new file mode 100644 index 00000000..5400fb60 --- /dev/null +++ b/apps/web-antd/src/views/system/process/components/inline-field-editor.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/apps/web-antd/src/views/system/process/components/method-info-bar.vue b/apps/web-antd/src/views/system/process/components/method-info-bar.vue new file mode 100644 index 00000000..071c3483 --- /dev/null +++ b/apps/web-antd/src/views/system/process/components/method-info-bar.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/apps/web-antd/src/views/system/process/components/modal.vue b/apps/web-antd/src/views/system/process/components/modal.vue index 02399b4a..1d6761e8 100644 --- a/apps/web-antd/src/views/system/process/components/modal.vue +++ b/apps/web-antd/src/views/system/process/components/modal.vue @@ -10,7 +10,7 @@ import { createProcess, updateProcess } from '#/views/system/process/api'; import { modalFormProps } from '#/views/system/process/config/form'; const isUpdate = ref(false); -const gridApi = ref(); +const onSuccessRef = ref<(() => void) | null>(null); const [Form, formApi] = useVbenForm(modalFormProps); @@ -24,6 +24,9 @@ function buildSubmitPayload(values: Record) { payload.node_type = 'note'; payload.rule_id = values.rule_id; payload.note = values.note; + if (values.volume !== undefined && values.volume !== null) { + payload.volume = values.volume; + } if (values.real_id) { payload.real_id = values.real_id; } @@ -49,19 +52,39 @@ function validateFormValues(values: Record) { message.error('请输入备注名称'); return false; } + if (values.volume === null || values.volume === undefined || values.volume === '') { + message.error('请输入毫升'); + return false; + } return true; } if (!String(values.name ?? '').trim()) { message.error('请输入名称'); return false; } - if (formLevel === 2 && !values.pid) { - message.error('请选择父级制剂'); - return false; + if (formLevel === 2) { + if (!values.pid) { + message.error('请选择父级制剂'); + return false; + } + if (!values.calc_method) { + message.error('请选择计算方式'); + return false; + } + if (values.price === null || values.price === undefined || values.price === '') { + message.error('请输入计算价格'); + return false; + } + if (!values.unit) { + message.error('请选择计算单位'); + return false; + } } return true; } +const modalTitle = ref('新增加工费'); + const [Modal, modalApi] = useVbenModal({ fullscreenButton: false, draggable: true, @@ -79,7 +102,7 @@ const [Modal, modalApi] = useVbenModal({ submitApi(payload) .then(() => { message.success('保存成功'); - gridApi.value?.reload(); + onSuccessRef.value?.(); modalApi.close(); }) .finally(() => { @@ -87,10 +110,18 @@ const [Modal, modalApi] = useVbenModal({ }); }, onOpenChange(isOpen: boolean) { - gridApi.value = isOpen ? modalApi.getData()?.gridApi : null; if (isOpen) { - const { values, update, formLevel } = modalApi.getData>(); + const { values, update, formLevel, onSuccess } = modalApi.getData< + Record + >(); isUpdate.value = update; + onSuccessRef.value = onSuccess ?? null; + const levelLabels: Record = { + 1: '制剂', + 2: '煎法', + 3: '备注', + }; + modalTitle.value = `${update ? '编辑' : '新增'}${levelLabels[formLevel] ?? '加工费'}`; formApi.resetForm(); if (values) { const formValues = { @@ -101,12 +132,14 @@ const [Modal, modalApi] = useVbenModal({ }; formApi.setValues(formValues); } + } else { + onSuccessRef.value = null; } }, }); diff --git a/apps/web-antd/src/views/system/process/components/move-pid-modal.vue b/apps/web-antd/src/views/system/process/components/move-pid-modal.vue new file mode 100644 index 00000000..0f3ad4eb --- /dev/null +++ b/apps/web-antd/src/views/system/process/components/move-pid-modal.vue @@ -0,0 +1,122 @@ + + + diff --git a/apps/web-antd/src/views/system/process/components/volume-modal.vue b/apps/web-antd/src/views/system/process/components/volume-modal.vue deleted file mode 100644 index 9fce4928..00000000 --- a/apps/web-antd/src/views/system/process/components/volume-modal.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - diff --git a/apps/web-antd/src/views/system/process/composables/useProcessTree.ts b/apps/web-antd/src/views/system/process/composables/useProcessTree.ts new file mode 100644 index 00000000..6d394e44 --- /dev/null +++ b/apps/web-antd/src/views/system/process/composables/useProcessTree.ts @@ -0,0 +1,214 @@ +import { computed, ref } from 'vue'; + +import { getProcessList } from '#/views/system/process/api'; + +import type { ProcessNoteRow, ProcessRuleRow } from '../constants'; + +export function parseProcessListItems(items: Record[]) { + const level1: ProcessRuleRow[] = []; + const level2Map: Record = {}; + const notesMap: Record = {}; + + for (const item of items) { + if (item.node_type === 'note') { + const ruleId = Number(item.pid ?? item.rule_id); + const note: ProcessNoteRow = { + ...item, + real_id: item.real_id ?? Math.abs(Number(item.id)), + rule_id: ruleId, + name: item.name ?? item.note, + }; + if (!notesMap[ruleId]) { + notesMap[ruleId] = []; + } + notesMap[ruleId].push(note); + continue; + } + + const rule: ProcessRuleRow = { ...item, node_type: 'rule' }; + if (Number(rule.pid) === 0) { + level1.push(rule); + continue; + } + + const parentId = Number(rule.pid); + if (!level2Map[parentId]) { + level2Map[parentId] = []; + } + level2Map[parentId].push(rule); + } + + level1.sort((a, b) => a.id - b.id); + Object.values(level2Map).forEach((list) => list.sort((a, b) => a.id - b.id)); + Object.values(notesMap).forEach((list) => + list.sort((a, b) => a.id - b.id), + ); + + return { level1, level2Map, notesMap }; +} + +export function useProcessTree() { + const loading = ref(false); + const keyword = ref(''); + const level1List = ref([]); + const level2Map = ref>({}); + const notesMap = ref>({}); + const selectedLevel1Id = ref(null); + const selectedMethodId = ref(null); + + const filteredLevel1 = computed(() => { + const kw = keyword.value.trim().toLowerCase(); + if (!kw) { + return level1List.value; + } + return level1List.value.filter((item) => { + const methods = level2Map.value[item.id] ?? []; + if (item.name.toLowerCase().includes(kw)) { + return true; + } + return methods.some((method) => { + if (method.name.toLowerCase().includes(kw)) { + return true; + } + const notes = notesMap.value[method.id] ?? []; + return notes.some((note) => note.name.toLowerCase().includes(kw)); + }); + }); + }); + + const currentMethods = computed(() => { + if (!selectedLevel1Id.value) { + return []; + } + return level2Map.value[selectedLevel1Id.value] ?? []; + }); + + const selectedMethod = computed(() => + currentMethods.value.find((item) => item.id === selectedMethodId.value), + ); + + const currentNotes = computed(() => { + if (!selectedMethodId.value) { + return []; + } + const kw = keyword.value.trim().toLowerCase(); + const notes = notesMap.value[selectedMethodId.value] ?? []; + if (!kw) { + return notes; + } + return notes.filter((note) => note.name.toLowerCase().includes(kw)); + }); + + const selectedLevel1 = computed(() => + level1List.value.find((item) => item.id === selectedLevel1Id.value), + ); + + async function loadData() { + loading.value = true; + try { + const res = await getProcessList({ + page: 1, + pageSize: 500, + }); + const items = res?.items ?? []; + const parsed = parseProcessListItems(items); + level1List.value = parsed.level1; + level2Map.value = parsed.level2Map; + notesMap.value = parsed.notesMap; + + if ( + !selectedLevel1Id.value || + !level1List.value.some((item) => item.id === selectedLevel1Id.value) + ) { + selectedLevel1Id.value = level1List.value[0]?.id ?? null; + } + + const methods = selectedLevel1Id.value + ? (level2Map.value[selectedLevel1Id.value] ?? []) + : []; + if ( + !selectedMethodId.value || + !methods.some((item) => item.id === selectedMethodId.value) + ) { + selectedMethodId.value = methods[0]?.id ?? null; + } + } finally { + loading.value = false; + } + } + + function selectLevel1(id: number) { + selectedLevel1Id.value = id; + const methods = level2Map.value[id] ?? []; + selectedMethodId.value = methods[0]?.id ?? null; + } + + function selectMethod(id: number) { + selectedMethodId.value = id; + } + + function patchRule(id: number, patch: Partial) { + const inLevel1 = level1List.value.find((item) => item.id === id); + if (inLevel1) { + Object.assign(inLevel1, patch); + return; + } + for (const list of Object.values(level2Map.value)) { + const found = list.find((item) => item.id === id); + if (found) { + Object.assign(found, patch); + if (patch.pid !== undefined && Number(patch.pid) === 0) { + loadData(); + } + return; + } + } + } + + function patchNote(noteId: number, patch: Partial) { + for (const list of Object.values(notesMap.value)) { + const found = list.find( + (item) => item.id === noteId || item.real_id === noteId, + ); + if (found) { + Object.assign(found, patch); + if (patch.name) { + found.note = patch.name; + } + if (patch.rule_id && patch.rule_id !== found.rule_id) { + loadData(); + } + return; + } + } + } + + function allLevel2Rules() { + const result: ProcessRuleRow[] = []; + for (const list of Object.values(level2Map.value)) { + result.push(...list); + } + return result.sort((a, b) => a.id - b.id); + } + + return { + allLevel2Rules, + currentMethods, + currentNotes, + filteredLevel1, + keyword, + level1List, + level2Map, + loadData, + loading, + notesMap, + patchNote, + patchRule, + selectLevel1, + selectMethod, + selectedLevel1, + selectedLevel1Id, + selectedMethod, + selectedMethodId, + }; +} diff --git a/apps/web-antd/src/views/system/process/config/form.ts b/apps/web-antd/src/views/system/process/config/form.ts index 6a06c198..1dbb4d5b 100644 --- a/apps/web-antd/src/views/system/process/config/form.ts +++ b/apps/web-antd/src/views/system/process/config/form.ts @@ -1,6 +1,10 @@ import type { VbenFormProps } from '#/adapter/form'; import { getProcessOptionPidApi } from '#/views/system/process/api'; +import { + CALC_METHOD_OPTIONS, + UNIT_OPTIONS, +} from '#/views/system/process/constants'; export const modalFormProps: VbenFormProps = { wrapperClass: 'grid-cols-12', @@ -66,6 +70,30 @@ export const modalFormProps: VbenFormProps = { triggerFields: ['has_children'], }, }, + { + component: 'VbenInput', + fieldName: 'level_label', + label: '级别', + componentProps: { + disabled: true, + }, + dependencies: { + show(values) { + return [2, 3].includes(Number(values.formLevel)); + }, + trigger(values, form) { + const labels: Record = { + 2: '煎法', + 3: '备注', + }; + form.setFieldValue( + 'level_label', + labels[Number(values.formLevel)] ?? '', + ); + }, + triggerFields: ['formLevel'], + }, + }, { component: 'VbenInput', componentProps: { @@ -86,7 +114,7 @@ export const modalFormProps: VbenFormProps = { placeholder: '请输入备注名称', }, fieldName: 'note', - label: '备注名称', + label: '名称', dependencies: { show(values) { return Number(values.formLevel) === 3; @@ -95,14 +123,30 @@ export const modalFormProps: VbenFormProps = { }, }, { - component: 'VbenSelect', + component: 'InputNumber', componentProps: { - placeholder: '请选择计算方式', - options: [ - { label: '固定价格', value: 1 }, - { label: '贴数', value: 2 }, - { label: '克数', value: 3 }, - ], + placeholder: '请输入毫升', + class: 'w-full', + min: 1, + }, + fieldName: 'volume', + label: '毫升', + dependencies: { + show(values) { + return Number(values.formLevel) === 3; + }, + triggerFields: ['formLevel'], + }, + }, + { + component: 'RadioGroup', + componentProps: { + optionType: 'button', + buttonStyle: 'solid', + options: CALC_METHOD_OPTIONS.map((item) => ({ + label: item.label, + value: item.value, + })), }, fieldName: 'calc_method', label: '计算方式', @@ -142,6 +186,7 @@ export const modalFormProps: VbenFormProps = { componentProps: { placeholder: '请输入计算价格', class: 'w-full', + min: 0, }, fieldName: 'price', label: '计算价格', @@ -153,9 +198,14 @@ export const modalFormProps: VbenFormProps = { }, }, { - component: 'VbenInput', + component: 'RadioGroup', componentProps: { - placeholder: '请输入计算单位', + optionType: 'button', + buttonStyle: 'solid', + options: UNIT_OPTIONS.map((unit) => ({ + label: unit, + value: unit, + })), }, fieldName: 'unit', label: '计算单位', diff --git a/apps/web-antd/src/views/system/process/config/note-table.ts b/apps/web-antd/src/views/system/process/config/note-table.ts new file mode 100644 index 00000000..3d26a3dc --- /dev/null +++ b/apps/web-antd/src/views/system/process/config/note-table.ts @@ -0,0 +1,45 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +export const noteGridOptions: VxeGridProps = { + columnConfig: { + useKey: true, + }, + rowConfig: { + useKey: true, + keyField: 'id', + }, + columns: [ + { + field: 'level', + title: '级别', + width: 80, + formatter: () => '备注', + }, + { + field: 'name', + title: '名称', + minWidth: 160, + slots: { default: 'name' }, + }, + { + field: 'volume', + title: '毫升', + width: 120, + slots: { default: 'volume' }, + }, + { + field: 'created_at', + title: '创建时间', + minWidth: 160, + }, + ], + height: 'auto', + border: true, + showOverflow: true, + pagerConfig: { + enabled: false, + }, + toolbarConfig: { + enabled: false, + }, +}; diff --git a/apps/web-antd/src/views/system/process/config/search.ts b/apps/web-antd/src/views/system/process/config/search.ts deleted file mode 100644 index 98d583c7..00000000 --- a/apps/web-antd/src/views/system/process/config/search.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { VbenFormProps } from '#/adapter/form'; - -// import dayjs from 'dayjs'; - -export const formOptions: VbenFormProps = { - // 默认展开 - collapsed: false, - schema: [ - { - component: 'VbenInput', - componentProps: { - placeholder: '输入名称', - }, - defaultValue: '', - fieldName: 'name', - label: '加工费名称', - }, - // { - // component: 'RangePicker', - // componentProps: { - // format: 'YYYY-MM-DD', // 确保格式与你需要的一致 - // }, - // // defaultValue: [dayjs().startOf('month'), dayjs()], - // fieldName: 'search_time', - // label: '时间范围', - // }, - ], - // 控制表单是否显示折叠按钮 - showCollapseButton: true, - submitButtonOptions: { - content: '查询', - }, - // 是否在字段值改变时提交表单 - submitOnChange: false, - // 按下回车时是否提交表单 - submitOnEnter: false, -}; diff --git a/apps/web-antd/src/views/system/process/config/table.ts b/apps/web-antd/src/views/system/process/config/table.ts deleted file mode 100644 index 828182a9..00000000 --- a/apps/web-antd/src/views/system/process/config/table.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { VxeGridProps } from '#/adapter/vxe-table'; - -import { getProcessList } from '#/views/system/process/api'; - -const calcMethodMap: Record = { - 1: '固定价格', - 2: '贴数', - 3: '克数', -}; - -function getLevelLabel(row: Record) { - if (row.node_type === 'note') { - return '备注'; - } - if (Number(row.pid) === 0) { - return '制剂'; - } - return '煎法'; -} - -export const gridOptions: VxeGridProps = { - checkboxConfig: { - highlight: true, - labelField: '', - }, - columnConfig: { - useKey: true, - }, - rowConfig: { - useKey: true, - }, - columns: [ - { type: 'checkbox', width: 60 }, - { width: 60, treeNode: true }, - { field: 'id', align: 'left', title: 'ID', width: 100 }, - { - field: 'level', - title: '级别', - width: 90, - formatter: ({ row }) => getLevelLabel(row), - }, - { field: 'name', title: '名称' }, - { - field: 'calc_method', - title: '计算方式', - formatter: ({ row, cellValue }) => { - if (row.node_type === 'note') { - return '-'; - } - return calcMethodMap[Number(cellValue)] || cellValue || '-'; - }, - }, - { - field: 'price', - title: '计算价格', - formatter: ({ row, cellValue }) => - row.node_type === 'note' ? '-' : (cellValue ?? '-'), - }, - { - field: 'unit', - title: '计算单位', - formatter: ({ row, cellValue }) => - row.node_type === 'note' ? '-' : (cellValue ?? '-'), - }, - { - field: 'volume', - title: '毫升', - width: 100, - slots: { default: 'volume' }, - }, - { field: 'created_at', title: '创建时间' }, - { type: 'html', title: '操作', slots: { default: 'action' }, width: 220 }, - ], - keepSource: true, - pagerConfig: {}, - proxyConfig: { - ajax: { - query: async ({ page }, formValues) => { - return await getProcessList({ - page: page.currentPage, - pageSize: page.pageSize, - ...formValues, - }); - }, - }, - }, - treeConfig: { - parentField: 'pid', - rowField: 'id', - transform: true, - expandAll: false, - }, - height: 'auto', - border: false, - toolbarConfig: { - search: true, - refresh: true, - print: false, - export: false, - zoom: true, - slots: { - buttons: 'toolbar-buttons', - }, - custom: { - icon: 'vxe-icon-menu', - }, - }, - showOverflow: false, -}; - -export function getProcessNodeLevel(row: Record) { - if (row.node_type === 'note') { - return 3; - } - if (Number(row.pid) === 0) { - return 1; - } - return 2; -} diff --git a/apps/web-antd/src/views/system/process/constants.ts b/apps/web-antd/src/views/system/process/constants.ts new file mode 100644 index 00000000..88477ec5 --- /dev/null +++ b/apps/web-antd/src/views/system/process/constants.ts @@ -0,0 +1,37 @@ +export const CALC_METHOD_OPTIONS = [ + { label: '固定价格', value: 1 }, + { label: '贴数', value: 2 }, + { label: '克数', value: 3 }, +] as const; + +export const UNIT_OPTIONS = ['g', 'ml', '剂'] as const; + +export const CALC_METHOD_MAP: Record = { + 1: '固定价格', + 2: '贴数', + 3: '克数', +}; + +export type ProcessRuleRow = { + calc_method?: number; + created_at?: number | string; + has_children?: boolean; + id: number; + name: string; + node_type: 'rule'; + pid: number; + price?: number | string; + unit?: string; +}; + +export type ProcessNoteRow = { + created_at?: number | string; + id: number; + name: string; + node_type: 'note'; + note?: string; + pid: number; + real_id: number; + rule_id?: number; + volume?: number | null; +}; diff --git a/apps/web-antd/src/views/system/process/index.vue b/apps/web-antd/src/views/system/process/index.vue index 6d5fbe75..7d3859a9 100644 --- a/apps/web-antd/src/views/system/process/index.vue +++ b/apps/web-antd/src/views/system/process/index.vue @@ -1,65 +1,82 @@ + +