1. 加工费模块重构
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
CI / CI OK (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
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled

2. 快递费模块重构
3. 订单导出功能优化
4. 商品订单的折扣功能
5. 修复了一些已知BUG
This commit is contained in:
李琦
2026-06-23 15:06:41 +08:00
parent 8eb479696e
commit 824dc2b0e3
34 changed files with 3852 additions and 641 deletions

View File

@@ -1,186 +1,182 @@
<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>
<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);
}
function isEmptyValue() {
return props.value === null || props.value === undefined || 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) {

View File

@@ -1,118 +1,220 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { Descriptions, Empty, 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';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.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>
<section class="rounded-lg border border-border bg-muted/30 dark:border-gray-700">
<ConfigSectionHeader class="mb-0 px-4 pt-3" title="煎法配置" />
<div v-if="method" class="px-4 pb-3 pt-1">
<Descriptions
bordered
:column="3"
size="small"
class="process-method-descriptions"
>
<Descriptions.Item label="级别">煎法</Descriptions.Item>
<Descriptions.Item label="名称">
<InlineFieldEditor
:value="method.name"
label="名称"
placeholder="请输入名称"
@save="(val) => saveField({ name: val })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算方式">
<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) })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算价格">
<InlineFieldEditor
type="number"
:value="method.price"
label="计算价格"
placeholder="请输入价格"
@save="(val) => saveField({ price: val })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算单位" :span="2">
<InlineFieldEditor
type="tags"
:value="method.unit"
label="计算单位"
:options="UNIT_OPTIONS.map((u) => ({ label: u, value: u }))"
@save="(val) => saveField({ unit: val })"
/>
</Descriptions.Item>
</Descriptions>
</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 v-else class="flex justify-center px-4 py-6">
<Empty description="请选择上方煎法 Tab" />
</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>
</section>
</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;
.process-method-descriptions :deep(.ant-descriptions-view) {
border-color: hsl(var(--border));
}
.method-info-bar--empty {
color: #9ca3af;
.process-method-descriptions :deep(.ant-descriptions-item-label) {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.3);
}
.method-info-bar__item {
display: flex;
gap: 8px;
align-items: center;
min-width: 140px;
.process-method-descriptions :deep(.ant-descriptions-item-content) {
color: hsl(var(--foreground));
background: hsl(var(--card));
}
.method-info-bar__label {
flex-shrink: 0;
font-size: 13px;
color: #6b7280;
}
</style>

View File

@@ -3,6 +3,10 @@ import { computed, ref } from 'vue';
import { getProcessList } from '#/views/system/process/api';
import type { ProcessNoteRow, ProcessRuleRow } from '../constants';
import {
readProcessSelection,
writeProcessSelection,
} from '../utils/processSelectionStorage';
export function parseProcessListItems(items: Record<string, any>[]) {
const level1: ProcessRuleRow[] = [];
@@ -47,14 +51,35 @@ export function parseProcessListItems(items: Record<string, any>[]) {
return { level1, level2Map, notesMap };
}
function restoreSelection(
level1: ProcessRuleRow[],
level2Map: Record<number, ProcessRuleRow[]>,
savedLevel1Id: number | null,
savedMethodId: number | null,
) {
let level1Id = savedLevel1Id;
if (!level1Id || !level1.some((item) => item.id === level1Id)) {
level1Id = level1[0]?.id ?? null;
}
const methods = level1Id ? (level2Map[level1Id] ?? []) : [];
let methodId = savedMethodId;
if (!methodId || !methods.some((item) => item.id === methodId)) {
methodId = methods[0]?.id ?? null;
}
return { level1Id, methodId };
}
export function useProcessTree() {
const saved = readProcessSelection();
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 selectedLevel1Id = ref<number | null>(saved.level1Id);
const selectedMethodId = ref<number | null>(saved.methodId);
const filteredLevel1 = computed(() => {
const kw = keyword.value.trim().toLowerCase();
@@ -103,6 +128,13 @@ export function useProcessTree() {
level1List.value.find((item) => item.id === selectedLevel1Id.value),
);
function persistSelection() {
writeProcessSelection({
level1Id: selectedLevel1Id.value,
methodId: selectedMethodId.value,
});
}
async function loadData() {
loading.value = true;
try {
@@ -116,22 +148,15 @@ export function useProcessTree() {
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;
}
const restored = restoreSelection(
level1List.value,
level2Map.value,
selectedLevel1Id.value,
selectedMethodId.value,
);
selectedLevel1Id.value = restored.level1Id;
selectedMethodId.value = restored.methodId;
persistSelection();
} finally {
loading.value = false;
}
@@ -141,10 +166,12 @@ export function useProcessTree() {
selectedLevel1Id.value = id;
const methods = level2Map.value[id] ?? [];
selectedMethodId.value = methods[0]?.id ?? null;
persistSelection();
}
function selectMethod(id: number) {
selectedMethodId.value = id;
persistSelection();
}
function patchRule(id: number, patch: Partial<ProcessRuleRow>) {

View File

@@ -1,5 +1,7 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { SUB_LIST_GRID_HEIGHT } from '#/views/system/shared/sub-list-grid';
export const noteGridOptions: VxeGridProps = {
columnConfig: {
useKey: true,
@@ -33,7 +35,7 @@ export const noteGridOptions: VxeGridProps = {
minWidth: 160,
},
],
height: 'auto',
height: SUB_LIST_GRID_HEIGHT,
border: true,
showOverflow: true,
pagerConfig: {

View File

@@ -1,16 +1,9 @@
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
Empty,
Input,
Spin,
Tabs,
message,
} from 'ant-design-vue';
import { Button, Empty, Tabs, message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -22,6 +15,9 @@ import InlineFieldEditor from './components/inline-field-editor.vue';
import MethodInfoBar from './components/method-info-bar.vue';
import FormModalDemo from './components/modal.vue';
import MovePidModal from './components/move-pid-modal.vue';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.vue';
import ConfigTreeSidebar from '#/views/system/shared/config-tree-sidebar.vue';
import type { TreeSidebarRow } from '#/views/system/shared/types';
import { useProcessTree } from './composables/useProcessTree';
import type { ProcessNoteRow, ProcessRuleRow } from './constants';
import { noteGridOptions } from './config/note-table';
@@ -45,6 +41,10 @@ const {
selectedMethodId,
} = useProcessTree();
const breadcrumbMethodName = computed(
() => selectedMethod.value?.name ?? '未选择煎法',
);
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
@@ -195,8 +195,8 @@ function buildNoteMenus(row: ProcessNoteRow): ContextMenuItem[] {
];
}
function onLevel1ContextMenu(e: MouseEvent, row: ProcessRuleRow) {
showContextMenu(e, buildLevel1Menus(row), row);
function onLevel1ContextMenu(e: MouseEvent, row: TreeSidebarRow) {
showContextMenu(e, buildLevel1Menus(row as ProcessRuleRow), row);
}
function onMethodTabContextMenu(e: MouseEvent, row: ProcessRuleRow) {
@@ -207,9 +207,10 @@ 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 });
async function saveLevel1Name(row: TreeSidebarRow, name: string) {
const rule = row as ProcessRuleRow;
await updateProcess({ id: rule.id, node_type: 'rule', name });
patchRule(rule.id, { name });
message.success('保存成功');
}
@@ -243,114 +244,198 @@ function onSelectLevel1(id: number) {
selectLevel1(id);
syncNoteGrid();
}
function openCreateMethod() {
if (!selectedLevel1.value) {
return;
}
openCreateModal({ formLevel: 2, parentRow: selectedLevel1.value });
}
function openCreateNote() {
if (!selectedMethod.value) {
return;
}
openCreateModal({ formLevel: 3, parentRow: selectedMethod.value });
}
</script>
<template>
<Page auto-content-height title="加工费管理">
<Page
auto-content-height
description="右键制剂、煎法 Tab 或备注行可新增/移动;点击虚线下划线字段可编辑"
title="加工费管理"
>
<FormModal />
<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="搜索名称"
/>
<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>
<div class="flex h-full min-h-0 gap-4">
<ConfigTreeSidebar
add-label="新增制剂"
empty-text="暂无制剂"
:filtered-level1="filteredLevel1"
:keyword="keyword"
:loading="loading"
search-placeholder="搜索制剂名称"
:selected-level1-id="selectedLevel1Id"
title="制剂"
:total-count="level1List.length"
@add-level1="openCreateModal({ formLevel: 1 })"
@contextmenu="onLevel1ContextMenu"
@save-name="saveLevel1Name"
@select="onSelectLevel1"
@update:keyword="(val) => (keyword = val)"
/>
<main class="process-main">
<main
class="min-w-0 flex-1 rounded-lg border border-border bg-card px-4 py-3 dark:border-gray-700"
>
<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 })"
<div class="space-y-4">
<div
class="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-3"
>
新增煎法
</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>
<div class="flex min-w-0 flex-wrap items-center gap-2 text-sm">
<span class="font-medium text-foreground">{{
selectedLevel1.name
}}</span>
<span class="text-muted-foreground">/</span>
<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`
: '设置'
:class="
selectedMethod
? 'text-foreground'
: 'text-muted-foreground'
"
@save="(val) => saveNoteField(row, 'volume', val)"
/>
</span>
</template>
</NoteGrid>
>
{{ breadcrumbMethodName }}
</span>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<Button size="small" @click="openCreateMethod">
<PlusOutlined />
新增煎法
</Button>
<Button
size="small"
type="primary"
:disabled="!selectedMethod"
@click="openCreateNote"
>
<PlusOutlined />
新增备注
</Button>
</div>
</div>
<section>
<Tabs
v-if="currentMethods.length"
:active-key="String(selectedMethodId ?? '')"
class="process-method-tabs select-none"
type="line"
@change="onTabChange"
>
<Tabs.TabPane
v-for="method in currentMethods"
:key="String(method.id)"
>
<template #tab>
<span
class="cursor-pointer"
@contextmenu.prevent="
onMethodTabContextMenu($event, method)
"
>
{{ method.name }}
</span>
</template>
</Tabs.TabPane>
</Tabs>
<div
v-else
class="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-8 dark:border-gray-700"
>
<Empty description="暂无煎法" />
<Button class="mt-4" type="primary" @click="openCreateMethod">
<PlusOutlined />
新增煎法
</Button>
</div>
</section>
<MethodInfoBar
:method="selectedMethod"
@updated="
(patch) => selectedMethod && patchRule(selectedMethod.id, patch)
"
/>
<section
class="rounded-lg border border-border dark:border-gray-700"
>
<ConfigSectionHeader
class="mb-3 px-4 pt-3"
:subtitle="selectedMethod ? `共 ${currentNotes.length} 条` : undefined"
title="备注列表"
>
<template #actions>
<Button
size="small"
type="primary"
:disabled="!selectedMethod"
@click="openCreateNote"
>
<PlusOutlined />
新增备注
</Button>
</template>
</ConfigSectionHeader>
<div
v-if="selectedMethod && currentNotes.length"
class="overflow-hidden rounded-b-lg border-t border-border dark:border-gray-700"
>
<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>
</div>
<div
v-else-if="selectedMethod"
class="flex flex-col items-center justify-center px-4 py-8"
>
<Empty description="暂无备注" />
<Button class="mt-4" type="primary" @click="openCreateNote">
<PlusOutlined />
新增备注
</Button>
</div>
<div v-else class="px-4 pb-4">
<Empty description="请先选择煎法" />
</div>
</section>
</div>
</template>
<Empty v-else description="请选择左侧制剂" />
</main>
@@ -359,69 +444,11 @@ function onSelectLevel1(id: number) {
</template>
<style scoped>
.process-layout {
display: flex;
gap: 16px;
height: 100%;
min-height: 0;
.process-method-tabs :deep(.ant-tabs-nav) {
margin-bottom: 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;
.process-method-tabs :deep(.ant-tabs-tab) {
transition: color 0.2s;
}
</style>

View File

@@ -0,0 +1,38 @@
const STORAGE_KEY = 'process-management-selection';
export interface ProcessSelectionState {
level1Id: number | null;
methodId: number | null;
}
export function readProcessSelection(): ProcessSelectionState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return { level1Id: null, methodId: null };
}
const parsed = JSON.parse(raw) as Partial<ProcessSelectionState>;
return {
level1Id:
parsed.level1Id != null && Number.isFinite(Number(parsed.level1Id))
? Number(parsed.level1Id)
: null,
methodId:
parsed.methodId != null && Number.isFinite(Number(parsed.methodId))
? Number(parsed.methodId)
: null,
};
} catch {
return { level1Id: null, methodId: null };
}
}
export function writeProcessSelection(state: ProcessSelectionState) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
level1Id: state.level1Id,
methodId: state.methodId,
}),
);
}