更新
This commit is contained in:
@@ -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="示例: 黄芪15 党参12 茯苓10 柴胡10g、黄芩10、半夏12克 黄芪、白术、茯苓各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(/[0-9]/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
Reference in New Issue
Block a user