feat: integrate Luoyang pharmacy workflow

This commit is contained in:
2026-07-22 18:00:17 +08:00
parent 31312ae975
commit e1fe818dce
27 changed files with 1925 additions and 163 deletions
@@ -2,7 +2,7 @@
<span v-if="disabled" class="medicine-name-readonly">{{ modelValue || '' }}</span>
<el-select
v-else
:model-value="modelValue"
:model-value="selectedMedicineId"
class="medicine-name-select w-full"
filterable
remote
@@ -14,7 +14,14 @@
@visible-change="onVisibleChange"
@update:model-value="onUpdate"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.name" />
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
<div class="medicine-name-option">
<span>{{ item.name }}</span>
<span class="medicine-name-option__meta">
{{ item.id > 0 ? [item.supplier, item.unit, `ID ${item.id}`].filter(Boolean).join(' · ') : '历史名称,请重新选择' }}
</span>
</div>
</el-option>
</el-select>
</template>
@@ -24,6 +31,7 @@ import { medicineLists } from '@/api/medicine'
const props = withDefaults(
defineProps<{
modelValue: string
medicineId?: number | null
disabled?: boolean
}>(),
{ disabled: false }
@@ -31,18 +39,30 @@ const props = withDefaults(
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:medicineId': [value: number | undefined]
}>()
const loading = ref(false)
const options = ref<{ id: number; name: string }[]>([])
type MedicineOption = { id: number; name: string; supplier?: string; unit?: string }
const options = ref<MedicineOption[]>([])
const selectedMedicineId = computed<number | ''>(() => {
const id = Number(props.medicineId)
if (Number.isInteger(id) && id > 0) {
return id
}
return (props.modelValue || '').trim() ? 0 : ''
})
function ensureCurrentInOptions() {
const v = (props.modelValue || '').trim()
if (!v) {
return
}
if (!options.value.some((o) => o.name === v)) {
options.value = [{ id: 0, name: v }, ...options.value]
const id = Number(props.medicineId)
const currentId = Number.isInteger(id) && id > 0 ? id : 0
if (!options.value.some((o) => o.id === currentId)) {
options.value = [{ id: currentId, name: v }, ...options.value]
}
}
@@ -56,7 +76,7 @@ const remoteMethod = async (query: string) => {
page_size: 100,
status: 1
})
options.value = res.lists || []
options.value = (res.lists || []) as MedicineOption[]
ensureCurrentInOptions()
} catch (e) {
console.error(e)
@@ -72,13 +92,19 @@ const onVisibleChange = (open: boolean) => {
}
}
const onUpdate = (val: string) => {
emit('update:modelValue', val || '')
const onUpdate = (val: number | string) => {
const id = Number(val)
const selected = id > 0 ? options.value.find((item) => item.id === id) : undefined
emit('update:modelValue', selected?.name || '')
emit('update:medicineId', selected?.id)
}
watch(
() => props.modelValue,
() => [props.modelValue, props.medicineId],
() => {
if (!(props.modelValue || '').trim() && Number(props.medicineId) > 0) {
emit('update:medicineId', undefined)
}
ensureCurrentInOptions()
},
{ immediate: true }
@@ -93,4 +119,23 @@ watch(
.medicine-name-select.w-full {
width: 100%;
}
.medicine-name-option {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 60%);
align-items: center;
gap: 16px;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.medicine-name-option__meta {
color: var(--el-text-color-secondary);
font-size: 12px;
text-align: right;
}
</style>
@@ -139,7 +139,12 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
>
<div class="herb-editor-card__title">主方 {{ row.index + 1 }}</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<MedicineNameSelect
v-model="formData.herbs[row.index].name"
v-model:medicine-id="formData.herbs[row.index].medicine_id"
:disabled="herbsLocked"
class="herb-editor-card__select"
/>
<div
v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -176,7 +181,12 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
>
<div class="herb-editor-card__title">辅方</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<MedicineNameSelect
v-model="formData.herbs[row.index].name"
v-model:medicine-id="formData.herbs[row.index].medicine_id"
:disabled="herbsLocked"
class="herb-editor-card__select"
/>
<div
v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -1045,6 +1055,7 @@ import { Search } from '@element-plus/icons-vue'
type FormulaType = '主方' | '辅方'
interface Herb {
medicine_id?: number
name: string
dosage: number
formula_type?: FormulaType
@@ -1071,6 +1082,10 @@ function normalizeHerbRow(raw: any): Herb {
dosage: Number(raw?.dosage) || 0,
formula_type: normalizeFormulaType(raw?.formula_type)
}
const medicineId = Number(raw?.medicine_id ?? raw?.id ?? 0)
if (Number.isInteger(medicineId) && medicineId > 0) {
row.medicine_id = medicineId
}
if (raw?.locked === true || raw?.locked === 1) {
row.locked = true
}
@@ -1578,8 +1593,8 @@ function parseRecipeToHerbs(text: string): { name: string; dosage: number }[] {
return herbs
}
/** 仅在药品库中存在「完全同名」药材时返回规范药名,否则返回 null(不模糊猜测 */
async function resolveHerbNameFromLibrary(rawName: string): Promise<string | null> {
/** 仅有一条完全同名记录时返回稳定药材身份,否则不猜测 */
async function resolveHerbFromLibrary(rawName: string): Promise<{ medicine_id: number; name: string } | null> {
const q = rawName.trim()
if (!q) return null
try {
@@ -1590,8 +1605,10 @@ async function resolveHerbNameFromLibrary(rawName: string): Promise<string | nul
status: 1
})
const lists = (res.lists || []) as { id: number; name: string }[]
const exact = lists.find((item) => (item.name ?? '').trim() === q)
return exact ? exact.name.trim() : null
const exact = lists.filter((item) => (item.name ?? '').trim() === q)
return exact.length === 1
? { medicine_id: Number(exact[0].id), name: exact[0].name.trim() }
: null
} catch {
return null
}
@@ -1612,12 +1629,12 @@ async function handlePasteRecipeImport() {
const resolved: Herb[] = []
const skippedNames: string[] = []
for (const row of parsed) {
const name = await resolveHerbNameFromLibrary(row.name)
if (!name) {
const medicine = await resolveHerbFromLibrary(row.name)
if (!medicine) {
skippedNames.push(row.name.trim())
continue
}
resolved.push({ name, dosage: row.dosage, formula_type: '主方' })
resolved.push({ ...medicine, dosage: row.dosage, formula_type: '主方' })
}
const skippedUnique = [...new Set(skippedNames.filter(Boolean))]
if (resolved.length === 0) {