更新
This commit is contained in:
@@ -80,6 +80,7 @@
|
|||||||
中药处方 (RP)
|
中药处方 (RP)
|
||||||
<el-button type="primary" size="small" @click="handleAddHerb">添加中药</el-button>
|
<el-button type="primary" size="small" @click="handleAddHerb">添加中药</el-button>
|
||||||
<el-button type="success" size="small" @click="showLibraryDialog = true">从处方库导入</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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<el-form-item label="处方价格" prop="amount" class="!mb-0">
|
<el-form-item label="处方价格" prop="amount" class="!mb-0">
|
||||||
@@ -661,6 +662,40 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -668,6 +703,7 @@ import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
|
|||||||
import feedback from '@/utils/feedback'
|
import feedback from '@/utils/feedback'
|
||||||
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
||||||
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail, prescriptionLibraryLists } from '@/api/tcm'
|
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail, prescriptionLibraryLists } from '@/api/tcm'
|
||||||
|
import { medicineLists } from '@/api/medicine'
|
||||||
import { getDictData } from '@/api/app'
|
import { getDictData } from '@/api/app'
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
import { jsPDF } from 'jspdf'
|
import { jsPDF } from 'jspdf'
|
||||||
@@ -778,6 +814,179 @@ const libraryPage = ref(1)
|
|||||||
const libraryPageSize = ref(15)
|
const libraryPageSize = ref(15)
|
||||||
const libraryTotal = ref(0)
|
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({
|
const templateConfig = reactive({
|
||||||
stationName: '成都双流甄养堂互联网医院',
|
stationName: '成都双流甄养堂互联网医院',
|
||||||
showDisclaimer: true,
|
showDisclaimer: true,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user