This commit is contained in:
Your Name
2026-05-08 16:51:36 +08:00
parent 33fb055bfe
commit 0026a92697
2 changed files with 210 additions and 1 deletions
@@ -80,6 +80,7 @@
中药处方 (RP)
<el-button type="primary" size="small" @click="handleAddHerb">添加中药</el-button>
<el-button type="success" size="small" @click="showLibraryDialog = true">从处方库导入</el-button>
<el-button type="warning" size="small" @click="openPasteRecipeDialog">导入药方</el-button>
</div>
<div class="mb-3">
<el-form-item label="处方价格" prop="amount" class="!mb-0">
@@ -661,6 +662,40 @@
/>
</div>
</el-dialog>
<!-- 粘贴药方文本解析药名与剂量 -->
<el-dialog
v-model="showPasteRecipeDialog"
title="导入药方(识别药材)"
width="560px"
:close-on-click-modal="false"
destroy-on-close
@closed="pasteRecipeText = ''"
>
<p class="text-gray-500 text-sm mb-2 leading-relaxed">
粘贴整张药方文本系统按药名 + 剂量解析药名须与药品库中名称完全一致才会录入否则跳过支持顿号/逗号/换行分隔单行可用空格分味黄芪15g
白术10g黄芪党参各10克等同剂量写法亦可识别
</p>
<el-input
v-model="pasteRecipeText"
type="textarea"
:rows="10"
placeholder="示例:&#10;黄芪15 党参12 茯苓10&#10;柴胡10g、黄芩10、半夏12克&#10;黄芪、白术、茯苓各15克"
/>
<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2">
<span class="text-sm text-gray-600 shrink-0">导入方式</span>
<el-radio-group v-model="pasteRecipeImportMode">
<el-radio label="replace">覆盖现有药材</el-radio>
<el-radio label="append">追加到末尾</el-radio>
</el-radio-group>
</div>
<template #footer>
<el-button @click="showPasteRecipeDialog = false">取消</el-button>
<el-button type="primary" :loading="pasteRecipeImportLoading" @click="handlePasteRecipeImport">
识别并导入
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
@@ -668,6 +703,7 @@ import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
import feedback from '@/utils/feedback'
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail, prescriptionLibraryLists } from '@/api/tcm'
import { medicineLists } from '@/api/medicine'
import { getDictData } from '@/api/app'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
@@ -778,6 +814,179 @@ const libraryPage = ref(1)
const libraryPageSize = ref(15)
const libraryTotal = ref(0)
/** 粘贴药方导入 */
const showPasteRecipeDialog = ref(false)
const pasteRecipeText = ref('')
const pasteRecipeImportMode = ref<'replace' | 'append'>('replace')
const pasteRecipeImportLoading = ref(false)
const PASTE_RECIPE_SKIP_SEGMENT =
/^(用法|用量|医嘱|忌口|水煎服|空腹|饭后|温水|内服|外用|备注|顿服|分服|ml|毫升|[×xX]\s*\d)/
function normalizeRecipeInput(raw: string): string {
let s = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
s = s.replace(/[-]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xff10 + 48))
return s.trim()
}
function stripRecipeHeaders(text: string): string {
return text
.replace(/^\s*Rp[::**\s]*/i, '')
.replace(/^\s*处方[:\s]*/, '')
.replace(/^\s*中药处方[:\s]*/, '')
.trimStart()
}
function stripHerbNote(name: string): string {
return name
.replace(/[^]*$/u, '')
.replace(/\([^)]*\)$/u, '')
.trim()
}
/** 单段解析为多味(含「各」),失败返回 null */
function parseHerbToken(seg: string): { name: string; dosage: number }[] | null {
const s = seg.trim()
if (!s || PASTE_RECIPE_SKIP_SEGMENT.test(s)) return null
if (/^\d/.test(s)) return null
if (/^\d+\s*剂$/.test(s)) return null
const ge = s.match(/^(.+)\s*各\s*(\d+(?:\.\d+)?)\s*(?:克|g|G)?$/i)
if (ge) {
const dose = parseFloat(ge[2])
if (!Number.isFinite(dose) || dose <= 0) return null
const names = ge[1]
.split(/[、,,\s]+/)
.map((x) => x.trim())
.filter(Boolean)
if (!names.length) return null
return names.map((name) => ({ name: stripHerbNote(name), dosage: dose }))
}
const m = s.match(/^(.+?)\s*(\d+(?:\.\d+)?)\s*(?:克|g|G|钱)?$/i)
if (m) {
const name = stripHerbNote(m[1].trim())
const dose = parseFloat(m[2])
if (!name || !Number.isFinite(dose) || dose <= 0) return null
return [{ name, dosage: dose }]
}
const nameOnly = stripHerbNote(s)
if (nameOnly.length >= 2 && nameOnly.length <= 16) {
return [{ name: nameOnly, dosage: 6 }]
}
return null
}
function chunkRecipeText(text: string): string[] {
const t = stripRecipeHeaders(normalizeRecipeInput(text))
const out: string[] = []
for (const line of t.split(/\n+/)) {
const ln = line.trim()
if (!ln) continue
for (const piece of ln.split(/[、,,;]/)) {
const p = piece.trim()
if (!p) continue
if (/\s/.test(p) && !parseHerbToken(p)) {
const sub = p.split(/\s+(?=[\u4e00-\u9fff]{2,})/)
for (const x of sub) {
const c = x.trim()
if (c) out.push(c)
}
} else {
out.push(p)
}
}
}
return out
}
function parseRecipeToHerbs(text: string): { name: string; dosage: number }[] {
const herbs: { name: string; dosage: number }[] = []
for (const c of chunkRecipeText(text)) {
const parsed = parseHerbToken(c)
if (!parsed) continue
herbs.push(...parsed)
}
return herbs
}
/** 仅在药品库中存在「完全同名」药材时返回规范药名,否则返回 null(不模糊猜测) */
async function resolveHerbNameFromLibrary(rawName: string): Promise<string | null> {
const q = rawName.trim()
if (!q) return null
try {
const res = await medicineLists({
name: q,
page_no: 1,
page_size: 200,
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
} catch {
return null
}
}
function openPasteRecipeDialog() {
showPasteRecipeDialog.value = true
}
async function handlePasteRecipeImport() {
const parsed = parseRecipeToHerbs(pasteRecipeText.value)
if (!parsed.length) {
feedback.msgWarning('未能从文本中解析出药材,请检查格式(药名 + 剂量,或用顿号/换行分隔)')
return
}
pasteRecipeImportLoading.value = true
try {
const resolved: Herb[] = []
const skippedNames: string[] = []
for (const row of parsed) {
const name = await resolveHerbNameFromLibrary(row.name)
if (!name) {
skippedNames.push(row.name.trim())
continue
}
resolved.push({ name, dosage: row.dosage })
}
const skippedUnique = [...new Set(skippedNames.filter(Boolean))]
if (resolved.length === 0) {
feedback.msgWarning(
skippedUnique.length
? `药品库中无以下完全同名药材,未录入:${skippedUnique.join('、')}`
: '未能匹配到药品库中的药材'
)
return
}
if (pasteRecipeImportMode.value === 'replace') {
formData.herbs = resolved
} else {
formData.herbs.push(...resolved)
}
const dupNames = findDuplicateHerbNames()
const skipHint =
skippedUnique.length > 0 ? `;药品库未收录已跳过:${skippedUnique.join('、')}` : ''
if (dupNames.length) {
feedback.msgWarning(
`已导入 ${resolved.length} 味药材${skipHint}。存在重复药名:${dupNames.join(
'、'
)},请合并剂量或删除多余行`
)
} else if (skippedUnique.length) {
feedback.msgSuccess(`已导入 ${resolved.length} 味药材${skipHint},请核对剂量`)
} else {
feedback.msgSuccess(`已导入 ${resolved.length} 味药材,请核对药名与剂量`)
}
showPasteRecipeDialog.value = false
pasteRecipeText.value = ''
} finally {
pasteRecipeImportLoading.value = false
}
}
const templateConfig = reactive({
stationName: '成都双流甄养堂互联网医院',
showDisclaimer: true,
File diff suppressed because one or more lines are too long