1. 加工费模块重构(1/3)
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Button, Input, InputNumber, Popover, Tag } from 'ant-design-vue';
|
||||
|
||||
type TagOption = { label: string; value: number | string };
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
displayFormatter?: (value: unknown) => string;
|
||||
label?: string;
|
||||
options?: TagOption[];
|
||||
placeholder?: string;
|
||||
type?: 'number' | 'tags' | 'text';
|
||||
value?: null | number | string;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
label: '',
|
||||
options: () => [],
|
||||
placeholder: '请输入',
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [value: number | string];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
const draftText = ref('');
|
||||
const draftNumber = ref<null | number>(null);
|
||||
const saving = ref(false);
|
||||
|
||||
watch(open, (visible) => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
if (props.type === 'number') {
|
||||
const raw = props.value;
|
||||
draftNumber.value =
|
||||
raw !== null && raw !== undefined && raw !== '' ? Number(raw) : null;
|
||||
return;
|
||||
}
|
||||
draftText.value = String(props.value ?? '');
|
||||
});
|
||||
|
||||
function displayText() {
|
||||
if (props.displayFormatter) {
|
||||
return props.displayFormatter(props.value);
|
||||
}
|
||||
if (props.value === null || props.value === undefined || props.value === '') {
|
||||
return '-';
|
||||
}
|
||||
if (props.type === 'tags') {
|
||||
const matched = props.options.find(
|
||||
(item) => String(item.value) === String(props.value),
|
||||
);
|
||||
return matched?.label ?? String(props.value);
|
||||
}
|
||||
return String(props.value);
|
||||
}
|
||||
|
||||
async function confirmSave() {
|
||||
saving.value = true;
|
||||
try {
|
||||
if (props.type === 'number') {
|
||||
if (draftNumber.value === null || draftNumber.value === undefined) {
|
||||
return;
|
||||
}
|
||||
emit('save', draftNumber.value);
|
||||
} else {
|
||||
const text = draftText.value.trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
emit('save', text);
|
||||
}
|
||||
open.value = false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectTag(value: number | string) {
|
||||
emit('save', value);
|
||||
open.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover v-model:open="open" trigger="click" placement="bottomLeft">
|
||||
<template #content>
|
||||
<div class="inline-editor-popover">
|
||||
<div v-if="label" class="inline-editor-popover__label">{{ label }}</div>
|
||||
<template v-if="type === 'tags'">
|
||||
<div class="inline-editor-popover__tags">
|
||||
<Tag
|
||||
v-for="opt in options"
|
||||
:key="String(opt.value)"
|
||||
class="cursor-pointer"
|
||||
:color="String(value) === String(opt.value) ? 'processing' : 'default'"
|
||||
@click="selectTag(opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<InputNumber
|
||||
v-model:value="draftNumber"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<div class="inline-editor-popover__actions">
|
||||
<Button size="small" @click="open = false">取消</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="confirmSave"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Input
|
||||
v-model:value="draftText"
|
||||
:placeholder="placeholder"
|
||||
@press-enter="confirmSave"
|
||||
/>
|
||||
<div class="inline-editor-popover__actions">
|
||||
<Button size="small" @click="open = false">取消</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="confirmSave"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<Button
|
||||
class="h-auto p-0 text-left"
|
||||
:disabled="disabled"
|
||||
size="small"
|
||||
type="link"
|
||||
@click.stop
|
||||
>
|
||||
{{ displayText() }}
|
||||
</Button>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inline-editor-popover {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.inline-editor-popover__label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.inline-editor-popover__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.inline-editor-popover__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { updateProcess } from '#/views/system/process/api';
|
||||
|
||||
import {
|
||||
CALC_METHOD_MAP,
|
||||
CALC_METHOD_OPTIONS,
|
||||
UNIT_OPTIONS,
|
||||
} from '../constants';
|
||||
import type { ProcessRuleRow } from '../constants';
|
||||
import InlineFieldEditor from './inline-field-editor.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
method: ProcessRuleRow | null | undefined;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
updated: [patch: Partial<ProcessRuleRow>];
|
||||
}>();
|
||||
|
||||
async function saveField(payload: Record<string, unknown>) {
|
||||
if (!props.method) {
|
||||
return;
|
||||
}
|
||||
await updateProcess({
|
||||
id: props.method.id,
|
||||
node_type: 'rule',
|
||||
...payload,
|
||||
});
|
||||
emit('updated', payload as Partial<ProcessRuleRow>);
|
||||
message.success('保存成功');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="method" class="method-info-bar">
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">级别</span>
|
||||
<span>煎法</span>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">名称</span>
|
||||
<InlineFieldEditor
|
||||
:value="method.name"
|
||||
label="名称"
|
||||
placeholder="请输入名称"
|
||||
@save="(val) => saveField({ name: val })"
|
||||
/>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">计算方式</span>
|
||||
<InlineFieldEditor
|
||||
type="tags"
|
||||
:value="method.calc_method"
|
||||
label="计算方式"
|
||||
:options="[...CALC_METHOD_OPTIONS]"
|
||||
:display-formatter="
|
||||
(val) => CALC_METHOD_MAP[Number(val)] || String(val ?? '-')
|
||||
"
|
||||
@save="(val) => saveField({ calc_method: Number(val) })"
|
||||
/>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">计算价格</span>
|
||||
<InlineFieldEditor
|
||||
type="number"
|
||||
:value="method.price"
|
||||
label="计算价格"
|
||||
placeholder="请输入价格"
|
||||
@save="(val) => saveField({ price: val })"
|
||||
/>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">计算单位</span>
|
||||
<InlineFieldEditor
|
||||
type="tags"
|
||||
:value="method.unit"
|
||||
label="计算单位"
|
||||
:options="UNIT_OPTIONS.map((u) => ({ label: u, value: u }))"
|
||||
@save="(val) => saveField({ unit: val })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="method-info-bar method-info-bar--empty">
|
||||
请选择或新增煎法
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.method-info-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 12px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.method-info-bar--empty {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.method-info-bar__item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.method-info-bar__label {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -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<string, any>) {
|
||||
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<string, any>) {
|
||||
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<Record<string, any>>();
|
||||
const { values, update, formLevel, onSuccess } = modalApi.getData<
|
||||
Record<string, any>
|
||||
>();
|
||||
isUpdate.value = update;
|
||||
onSuccessRef.value = onSuccess ?? null;
|
||||
const levelLabels: Record<number, string> = {
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}加工费`" class="w-[30%]">
|
||||
<Modal :title="modalTitle" class="w-[32%]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Form, FormItem, Select, message } from 'ant-design-vue';
|
||||
|
||||
import { updateProcess } from '#/views/system/process/api';
|
||||
|
||||
import type { ProcessNoteRow, ProcessRuleRow } from '../constants';
|
||||
|
||||
type MoveTarget =
|
||||
| { kind: 'method'; row: ProcessRuleRow }
|
||||
| { kind: 'note'; row: ProcessNoteRow };
|
||||
|
||||
const moveTarget = ref<MoveTarget | null>(null);
|
||||
const targetId = ref<number | undefined>(undefined);
|
||||
const level1List = ref<ProcessRuleRow[]>([]);
|
||||
const allMethods = ref<ProcessRuleRow[]>([]);
|
||||
const onSuccess = ref<(() => void) | null>(null);
|
||||
|
||||
const isNote = computed(() => moveTarget.value?.kind === 'note');
|
||||
|
||||
const options = computed(() => {
|
||||
if (!moveTarget.value) {
|
||||
return [];
|
||||
}
|
||||
if (moveTarget.value.kind === 'method') {
|
||||
return level1List.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
return allMethods.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
if (!moveTarget.value || !targetId.value) {
|
||||
message.error('请选择目标父级');
|
||||
return;
|
||||
}
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
if (moveTarget.value.kind === 'method') {
|
||||
await updateProcess({
|
||||
id: moveTarget.value.row.id,
|
||||
node_type: 'rule',
|
||||
pid: targetId.value,
|
||||
});
|
||||
} else {
|
||||
const row = moveTarget.value.row;
|
||||
await updateProcess({
|
||||
id: row.id,
|
||||
node_type: 'note',
|
||||
real_id: row.real_id,
|
||||
rule_id: targetId.value,
|
||||
note: row.note ?? row.name,
|
||||
});
|
||||
}
|
||||
message.success('移动成功');
|
||||
onSuccess.value?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<{
|
||||
allMethods: ProcessRuleRow[];
|
||||
level1List: ProcessRuleRow[];
|
||||
onSuccess?: () => void;
|
||||
target: MoveTarget;
|
||||
}>();
|
||||
moveTarget.value = data?.target ?? null;
|
||||
level1List.value = data?.level1List ?? [];
|
||||
allMethods.value = data?.allMethods ?? [];
|
||||
onSuccess.value = data?.onSuccess ?? null;
|
||||
if (moveTarget.value?.kind === 'method') {
|
||||
targetId.value = moveTarget.value.row.pid || undefined;
|
||||
} else if (moveTarget.value?.kind === 'note') {
|
||||
targetId.value = moveTarget.value.row.rule_id ?? moveTarget.value.row.pid;
|
||||
}
|
||||
} else {
|
||||
moveTarget.value = null;
|
||||
targetId.value = undefined;
|
||||
onSuccess.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="isNote ? '移动备注到煎法' : '移动煎法到制剂'"
|
||||
class="w-[400px]"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<FormItem :label="isNote ? '目标煎法' : '目标制剂'" required>
|
||||
<Select
|
||||
v-model:value="targetId"
|
||||
:options="options"
|
||||
placeholder="请选择"
|
||||
show-search
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
option?.label?.toLowerCase().includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Form, FormItem, InputNumber, message } from 'ant-design-vue';
|
||||
|
||||
import { updateProcess } from '#/views/system/process/api';
|
||||
|
||||
const noteRow = ref<Record<string, any>>({});
|
||||
const volumeValue = ref<null | number>(null);
|
||||
const gridApiRef = ref<any>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
if (volumeValue.value === null || volumeValue.value === undefined) {
|
||||
message.error('请输入毫升');
|
||||
return;
|
||||
}
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const realId = noteRow.value.real_id ?? Math.abs(Number(noteRow.value.id));
|
||||
await updateProcess({
|
||||
id: noteRow.value.id,
|
||||
node_type: 'note',
|
||||
real_id: realId,
|
||||
note: noteRow.value.note ?? noteRow.value.name,
|
||||
volume: volumeValue.value,
|
||||
});
|
||||
message.success('保存成功');
|
||||
gridApiRef.value?.query?.();
|
||||
// gridApiRef.value?.reload?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<{
|
||||
gridApi: any;
|
||||
row: Record<string, any>;
|
||||
}>();
|
||||
gridApiRef.value = data?.gridApi;
|
||||
noteRow.value = data?.row ?? {};
|
||||
const raw = noteRow.value.volume;
|
||||
volumeValue.value =
|
||||
raw !== null && raw !== undefined && raw !== '' ? Number(raw) : null;
|
||||
} else {
|
||||
noteRow.value = {};
|
||||
volumeValue.value = null;
|
||||
gridApiRef.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="设置毫升" class="w-[400px]">
|
||||
<Form layout="vertical">
|
||||
<FormItem label="备注名称">
|
||||
<span>{{ noteRow.name ?? noteRow.note ?? '-' }}</span>
|
||||
</FormItem>
|
||||
<FormItem label="毫升" required>
|
||||
<InputNumber
|
||||
v-model:value="volumeValue"
|
||||
:min="1"
|
||||
class="w-full"
|
||||
placeholder="请输入毫升,用于传给 MES"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -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<string, any>[]) {
|
||||
const level1: ProcessRuleRow[] = [];
|
||||
const level2Map: Record<number, ProcessRuleRow[]> = {};
|
||||
const notesMap: Record<number, ProcessNoteRow[]> = {};
|
||||
|
||||
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<ProcessRuleRow[]>([]);
|
||||
const level2Map = ref<Record<number, ProcessRuleRow[]>>({});
|
||||
const notesMap = ref<Record<number, ProcessNoteRow[]>>({});
|
||||
const selectedLevel1Id = ref<number | null>(null);
|
||||
const selectedMethodId = ref<number | null>(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<ProcessRuleRow>) {
|
||||
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<ProcessNoteRow>) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<number, string> = {
|
||||
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: '计算单位',
|
||||
|
||||
45
apps/web-antd/src/views/system/process/config/note-table.ts
Normal file
45
apps/web-antd/src/views/system/process/config/note-table.ts
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getProcessList } from '#/views/system/process/api';
|
||||
|
||||
const calcMethodMap: Record<number, string> = {
|
||||
1: '固定价格',
|
||||
2: '贴数',
|
||||
3: '克数',
|
||||
};
|
||||
|
||||
function getLevelLabel(row: Record<string, any>) {
|
||||
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<string, any>) {
|
||||
if (row.node_type === 'note') {
|
||||
return 3;
|
||||
}
|
||||
if (Number(row.pid) === 0) {
|
||||
return 1;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
37
apps/web-antd/src/views/system/process/constants.ts
Normal file
37
apps/web-antd/src/views/system/process/constants.ts
Normal file
@@ -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<number, string> = {
|
||||
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;
|
||||
};
|
||||
@@ -1,65 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, watch } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
Spin,
|
||||
Tabs,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import type { ContextMenuItem } from '#/components/context-menu';
|
||||
import { showContextMenu } from '#/components/context-menu';
|
||||
|
||||
import { deleteProcess } from './api';
|
||||
import { deleteProcess, updateProcess } from './api';
|
||||
import InlineFieldEditor from './components/inline-field-editor.vue';
|
||||
import MethodInfoBar from './components/method-info-bar.vue';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import VolumeModal from './components/volume-modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { getProcessNodeLevel, gridOptions } from './config/table';
|
||||
import MovePidModal from './components/move-pid-modal.vue';
|
||||
import { useProcessTree } from './composables/useProcessTree';
|
||||
import type { ProcessNoteRow, ProcessRuleRow } from './constants';
|
||||
import { noteGridOptions } from './config/note-table';
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents,
|
||||
});
|
||||
const {
|
||||
allLevel2Rules,
|
||||
currentMethods,
|
||||
currentNotes,
|
||||
filteredLevel1,
|
||||
keyword,
|
||||
level1List,
|
||||
loadData,
|
||||
loading,
|
||||
patchNote,
|
||||
patchRule,
|
||||
selectLevel1,
|
||||
selectMethod,
|
||||
selectedLevel1,
|
||||
selectedLevel1Id,
|
||||
selectedMethod,
|
||||
selectedMethodId,
|
||||
} = useProcessTree();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const [VolumeModalComponent, volumeModalApi] = useVbenModal({
|
||||
connectedComponent: VolumeModal,
|
||||
const [MoveModal, moveModalApi] = useVbenModal({
|
||||
connectedComponent: MovePidModal,
|
||||
});
|
||||
|
||||
function showVolumeModal(row: Record<string, any>) {
|
||||
volumeModalApi.setData({
|
||||
row,
|
||||
gridApi,
|
||||
});
|
||||
volumeModalApi.open();
|
||||
const [NoteGrid, noteGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
...noteGridOptions,
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
|
||||
function syncNoteGrid() {
|
||||
noteGridApi.setGridOptions({ data: currentNotes.value });
|
||||
}
|
||||
|
||||
function getVolumeLinkLabel(row: Record<string, any>) {
|
||||
const volume = row.volume;
|
||||
if (volume !== null && volume !== undefined && volume !== '') {
|
||||
return `${volume}ml`;
|
||||
}
|
||||
return '设置';
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
syncNoteGrid();
|
||||
});
|
||||
|
||||
watch(currentNotes, syncNoteGrid, { deep: true });
|
||||
watch(selectedMethodId, syncNoteGrid);
|
||||
|
||||
function refresh() {
|
||||
return loadData().then(syncNoteGrid);
|
||||
}
|
||||
|
||||
function openModal(options: {
|
||||
function openCreateModal(options: {
|
||||
formLevel: number;
|
||||
isUpdate?: boolean;
|
||||
parentRow?: Record<string, any>;
|
||||
@@ -70,11 +87,7 @@ function openModal(options: {
|
||||
|
||||
if (!isUpdate) {
|
||||
if (formLevel === 1) {
|
||||
formValues = {
|
||||
node_type: 'rule',
|
||||
pid: 0,
|
||||
formLevel: 1,
|
||||
};
|
||||
formValues = { node_type: 'rule', pid: 0, formLevel: 1 };
|
||||
} else if (formLevel === 2 && parentRow) {
|
||||
formValues = {
|
||||
node_type: 'rule',
|
||||
@@ -86,6 +99,7 @@ function openModal(options: {
|
||||
node_type: 'note',
|
||||
rule_id: parentRow.id,
|
||||
formLevel: 3,
|
||||
volume: 50,
|
||||
};
|
||||
}
|
||||
} else if (formLevel === 3) {
|
||||
@@ -93,7 +107,7 @@ function openModal(options: {
|
||||
...values,
|
||||
formLevel: 3,
|
||||
note: values.note ?? values.name,
|
||||
rule_id: values.pid,
|
||||
rule_id: values.rule_id ?? values.pid,
|
||||
real_id: values.real_id ?? Math.abs(Number(values.id)),
|
||||
};
|
||||
} else {
|
||||
@@ -108,147 +122,306 @@ function openModal(options: {
|
||||
values: formValues,
|
||||
update: isUpdate,
|
||||
formLevel,
|
||||
gridApi,
|
||||
onSuccess: refresh,
|
||||
});
|
||||
formModalApi.open();
|
||||
}
|
||||
|
||||
const showCreateLevel1 = () => {
|
||||
openModal({ formLevel: 1 });
|
||||
};
|
||||
|
||||
const showEditModal = (row: Record<string, any>) => {
|
||||
openModal({
|
||||
formLevel: getProcessNodeLevel(row),
|
||||
isUpdate: true,
|
||||
values: row,
|
||||
function openMoveModal(
|
||||
target:
|
||||
| { kind: 'method'; row: ProcessRuleRow }
|
||||
| { kind: 'note'; row: ProcessNoteRow },
|
||||
) {
|
||||
moveModalApi.setData({
|
||||
target,
|
||||
level1List: level1List.value,
|
||||
allMethods: allLevel2Rules(),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
};
|
||||
moveModalApi.open();
|
||||
}
|
||||
|
||||
const showCreateChildRule = (row: Record<string, any>) => {
|
||||
openModal({
|
||||
formLevel: 2,
|
||||
parentRow: row,
|
||||
});
|
||||
};
|
||||
function buildLevel1Menus(row: ProcessRuleRow): ContextMenuItem[] {
|
||||
return [
|
||||
{
|
||||
key: 'add-method',
|
||||
label: '新增煎法',
|
||||
handler: () => openCreateModal({ formLevel: 2, parentRow: row }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const showCreateChildNote = (row: Record<string, any>) => {
|
||||
openModal({
|
||||
formLevel: 3,
|
||||
parentRow: row,
|
||||
});
|
||||
};
|
||||
function buildMethodMenus(row: ProcessRuleRow): ContextMenuItem[] {
|
||||
return [
|
||||
{
|
||||
key: 'add-note',
|
||||
label: '新增备注',
|
||||
handler: () => openCreateModal({ formLevel: 3, parentRow: row }),
|
||||
},
|
||||
{
|
||||
key: 'move-pid',
|
||||
label: '移动父级',
|
||||
handler: () => openMoveModal({ kind: 'method', row }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deleteRow = (row: Record<string, any>) => {
|
||||
deleteProcess({ ids: [row.id] }).then(() => {
|
||||
message.success('删除成功!');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
function buildNoteMenus(row: ProcessNoteRow): ContextMenuItem[] {
|
||||
const method = selectedMethod.value;
|
||||
return [
|
||||
{
|
||||
key: 'add-note',
|
||||
label: '新增备注',
|
||||
handler: () => {
|
||||
if (method) {
|
||||
openCreateModal({ formLevel: 3, parentRow: method });
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'move-pid',
|
||||
label: '移动父级',
|
||||
handler: () => openMoveModal({ kind: 'note', row }),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
handler: async () => {
|
||||
await deleteProcess({ ids: [row.id] });
|
||||
message.success('删除成功');
|
||||
await refresh();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const expandAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(true);
|
||||
};
|
||||
function onLevel1ContextMenu(e: MouseEvent, row: ProcessRuleRow) {
|
||||
showContextMenu(e, buildLevel1Menus(row), row);
|
||||
}
|
||||
|
||||
const collapseAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(false);
|
||||
};
|
||||
function onMethodTabContextMenu(e: MouseEvent, row: ProcessRuleRow) {
|
||||
showContextMenu(e, buildMethodMenus(row), row);
|
||||
}
|
||||
|
||||
function onNoteContextMenu(e: MouseEvent, row: ProcessNoteRow) {
|
||||
showContextMenu(e, buildNoteMenus(row), row);
|
||||
}
|
||||
|
||||
async function saveLevel1Name(row: ProcessRuleRow, name: string) {
|
||||
await updateProcess({ id: row.id, node_type: 'rule', name });
|
||||
patchRule(row.id, { name });
|
||||
message.success('保存成功');
|
||||
}
|
||||
|
||||
async function saveNoteField(
|
||||
row: ProcessNoteRow,
|
||||
field: 'name' | 'volume',
|
||||
value: number | string,
|
||||
) {
|
||||
const payload: Record<string, unknown> = {
|
||||
id: row.id,
|
||||
node_type: 'note',
|
||||
real_id: row.real_id,
|
||||
note: row.note ?? row.name,
|
||||
};
|
||||
if (field === 'name') {
|
||||
payload.note = value;
|
||||
} else {
|
||||
payload.volume = value;
|
||||
}
|
||||
await updateProcess(payload);
|
||||
patchNote(row.id, field === 'name' ? { name: String(value) } : { volume: Number(value) });
|
||||
message.success('保存成功');
|
||||
}
|
||||
|
||||
function onTabChange(key: string | number) {
|
||||
selectMethod(Number(key));
|
||||
syncNoteGrid();
|
||||
}
|
||||
|
||||
function onSelectLevel1(id: number) {
|
||||
selectLevel1(id);
|
||||
syncNoteGrid();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="加工费管理">
|
||||
<FormModal />
|
||||
<VolumeModalComponent />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增制剂',
|
||||
type: 'primary',
|
||||
icon: 'ant-design:plus-outlined',
|
||||
onClick: showCreateLevel1,
|
||||
},
|
||||
{
|
||||
label: '展开全部',
|
||||
type: 'primary',
|
||||
onClick: expandAll,
|
||||
},
|
||||
{
|
||||
label: '收起全部',
|
||||
type: 'primary',
|
||||
onClick: collapseAll,
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #volume="{ row }">
|
||||
<Button
|
||||
v-if="row.node_type === 'note'"
|
||||
class="h-auto p-0"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="showVolumeModal(row)"
|
||||
>
|
||||
{{ getVolumeLinkLabel(row) }}
|
||||
</Button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
...(getProcessNodeLevel(row) === 1
|
||||
? [
|
||||
{
|
||||
label: '新增煎法',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
onClick: () => showCreateChildRule(row),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(getProcessNodeLevel(row) === 2
|
||||
? [
|
||||
{
|
||||
label: '新增备注',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
onClick: () => showCreateChildNote(row),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
onClick: () => showEditModal(row),
|
||||
},
|
||||
...(getProcessNodeLevel(row) === 3
|
||||
? [
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
icon: 'ant-design:delete-outlined',
|
||||
size: 'small',
|
||||
popConfirm: {
|
||||
title: '确定删除该备注吗?',
|
||||
confirm: () => deleteRow(row),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]"
|
||||
<MoveModal />
|
||||
|
||||
<div class="process-layout">
|
||||
<aside class="process-sidebar">
|
||||
<div class="process-sidebar__toolbar">
|
||||
<Button type="primary" @click="openCreateModal({ formLevel: 1 })">
|
||||
<PlusOutlined />
|
||||
新增制剂
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
class="process-sidebar__search"
|
||||
placeholder="搜索名称"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="filteredLevel1.length" class="process-sidebar__list">
|
||||
<div
|
||||
v-for="item in filteredLevel1"
|
||||
:key="item.id"
|
||||
class="process-sidebar__item"
|
||||
:class="{ active: selectedLevel1Id === item.id }"
|
||||
@click="onSelectLevel1(item.id)"
|
||||
@contextmenu="onLevel1ContextMenu($event, item)"
|
||||
>
|
||||
<InlineFieldEditor
|
||||
:value="item.name"
|
||||
@click.stop
|
||||
@save="(val) => saveLevel1Name(item, String(val))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Empty v-else description="暂无制剂" />
|
||||
</Spin>
|
||||
</aside>
|
||||
|
||||
<main class="process-main">
|
||||
<template v-if="selectedLevel1">
|
||||
<div class="process-main__header">
|
||||
<span class="process-main__title">{{ selectedLevel1.name }}</span>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openCreateModal({ formLevel: 2, parentRow: selectedLevel1 })"
|
||||
>
|
||||
新增煎法
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
v-if="currentMethods.length"
|
||||
:active-key="String(selectedMethodId ?? '')"
|
||||
type="card"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<Tabs.TabPane
|
||||
v-for="method in currentMethods"
|
||||
:key="String(method.id)"
|
||||
>
|
||||
<template #tab>
|
||||
<span
|
||||
@contextmenu.prevent="onMethodTabContextMenu($event, method)"
|
||||
>
|
||||
{{ method.name }}
|
||||
</span>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Empty
|
||||
v-else
|
||||
description="暂无煎法,请右键制剂或点击新增"
|
||||
/>
|
||||
|
||||
<MethodInfoBar
|
||||
:method="selectedMethod"
|
||||
@updated="(patch) => selectedMethod && patchRule(selectedMethod.id, patch)"
|
||||
/>
|
||||
|
||||
<NoteGrid @contextmenu.prevent>
|
||||
<template #name="{ row }">
|
||||
<span @contextmenu="onNoteContextMenu($event, row)">
|
||||
<InlineFieldEditor
|
||||
:value="row.name"
|
||||
@save="(val) => saveNoteField(row, 'name', val)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<template #volume="{ row }">
|
||||
<span @contextmenu="onNoteContextMenu($event, row)">
|
||||
<InlineFieldEditor
|
||||
type="number"
|
||||
:value="row.volume"
|
||||
:display-formatter="
|
||||
(val) =>
|
||||
val !== null && val !== undefined && val !== ''
|
||||
? `${val}ml`
|
||||
: '设置'
|
||||
"
|
||||
@save="(val) => saveNoteField(row, 'volume', val)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</NoteGrid>
|
||||
</template>
|
||||
<Empty v-else description="请选择左侧制剂" />
|
||||
</main>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.process-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.process-sidebar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: 220px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.process-sidebar__toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.process-sidebar__search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.process-sidebar__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.process-sidebar__item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.process-sidebar__item:hover,
|
||||
.process-sidebar__item.active {
|
||||
background: #f0fbf8;
|
||||
}
|
||||
|
||||
.process-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.process-main__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.process-main__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user