更新
This commit is contained in:
@@ -75,12 +75,37 @@
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
中药处方 (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 class="info-section info-section--rp">
|
||||
<div class="rp-toolbar" role="region" aria-label="中药处方">
|
||||
<div class="rp-toolbar__lead">
|
||||
<span class="rp-toolbar__marker" aria-hidden="true" />
|
||||
<div class="rp-toolbar__text">
|
||||
<h3 class="rp-toolbar__heading">
|
||||
中药处方
|
||||
<span class="rp-toolbar__abbr">(RP)</span>
|
||||
</h3>
|
||||
<p class="rp-toolbar__meta">
|
||||
共 <strong>{{ formData.herbs.length }}</strong> 味药材 · 可手动添加、从处方库或粘贴药方批量录入(药品库同名匹配)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rp-toolbar__actions" role="toolbar" aria-label="处方药材快捷操作">
|
||||
<el-button type="primary" size="small" :icon="CirclePlus" @click="handleAddHerb">
|
||||
添加中药
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:icon="CollectionTag"
|
||||
@click="showLibraryDialog = true"
|
||||
>
|
||||
从处方库导入
|
||||
</el-button>
|
||||
<el-button type="primary" plain size="small" :icon="EditPen" @click="openPasteRecipeDialog">
|
||||
导入药方
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<el-form-item label="处方价格" prop="amount" class="!mb-0">
|
||||
@@ -135,11 +160,11 @@
|
||||
<el-select v-model="formData.prescription_type" placeholder="请选择处方类型" class="!w-full">
|
||||
<el-option label="浓缩水丸" value="浓缩水丸" />
|
||||
<el-option label="饮片" value="饮片" />
|
||||
<el-option label="颗粒" value="颗粒" />
|
||||
<!-- <el-option label="颗粒" value="颗粒" />
|
||||
<el-option label="丸剂" value="丸剂" />
|
||||
<el-option label="散剂" value="散剂" />
|
||||
<el-option label="膏方" value="膏方" />
|
||||
<el-option label="汤剂" value="汤剂" />
|
||||
<el-option label="汤剂" value="汤剂" /> -->
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -709,7 +734,7 @@ import html2canvas from 'html2canvas'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { Download, Document } from '@element-plus/icons-vue'
|
||||
import { CirclePlus, CollectionTag, Download, Document, EditPen } from '@element-plus/icons-vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
interface Herb {
|
||||
@@ -1761,6 +1786,107 @@ defineExpose({
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.info-section--rp {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.rp-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px 20px;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--el-bg-color) 0%,
|
||||
var(--el-fill-color-extra-light) 100%
|
||||
);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.rp-toolbar:hover {
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.07);
|
||||
}
|
||||
|
||||
.rp-toolbar__lead {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rp-toolbar__marker {
|
||||
width: 4px;
|
||||
min-height: 44px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--el-color-primary) 0%,
|
||||
var(--el-color-primary-light-5) 100%
|
||||
);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rp-toolbar__text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rp-toolbar__heading {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.rp-toolbar__abbr {
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rp-toolbar__meta {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rp-toolbar__meta strong {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.rp-toolbar__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rp-toolbar__actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.rp-toolbar__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.herbs-editor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -631,12 +631,22 @@
|
||||
|
||||
<el-form-item label="药材配方" prop="herbs" required>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<el-button type="primary" size="small" @click="addHerb">
|
||||
添加药材
|
||||
</el-button>
|
||||
<el-button type="success" size="small" class="ml-2" @click="openLibraryDialog">
|
||||
<el-button type="success" size="small" @click="openLibraryDialog">
|
||||
从处方库导入
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<el-button type="warning" size="small" plain :icon="EditPen" @click="openPasteRecipeDialog">
|
||||
导入药方
|
||||
</el-button>
|
||||
<span class="text-xs text-gray-500 ml-2 align-middle">
|
||||
粘贴文本解析药名与剂量;须与药品库名称完全一致才录入
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="editForm.herbs" class="mt-2" border>
|
||||
<el-table-column label="序号" type="index" width="60" />
|
||||
<el-table-column label="药材名称" min-width="220">
|
||||
@@ -1359,6 +1369,40 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 粘贴药方文本导入(与诊间 tcm-prescription 逻辑一致:药品库完全同名才录入) -->
|
||||
<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>
|
||||
|
||||
<el-dialog v-model="auditDialogVisible" title="处方审核" width="480px" destroy-on-close @closed="auditRemark = ''">
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
通过:处方保持有效。驳回:将同时作废此处方(与诊间「驳回/作废处方」一致)。
|
||||
@@ -1401,7 +1445,7 @@ import {
|
||||
getDoctors,
|
||||
prescriptionLibraryLists
|
||||
} from '@/api/tcm'
|
||||
import { searchPatients as searchPatientsAPI } from '@/api/order'
|
||||
import { medicineLists } from '@/api/medicine'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { roleAll } from '@/api/perms/role'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
@@ -1410,7 +1454,7 @@ import feedback from '@/utils/feedback'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
|
||||
import { Search, Download, Printer } from '@element-plus/icons-vue'
|
||||
import { Search, Download, Printer, EditPen } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref, watch, nextTick, defineAsyncComponent } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import html2canvas from 'html2canvas'
|
||||
@@ -2971,6 +3015,177 @@ watch(showLibraryDialog, (open) => {
|
||||
}
|
||||
})
|
||||
|
||||
/** 粘贴药方导入(与 components/tcm-prescription 一致) */
|
||||
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 normalizeRecipePasteInput(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 stripRecipePasteHeaders(text: string): string {
|
||||
return text
|
||||
.replace(/^\s*Rp[::**\s]*/i, '')
|
||||
.replace(/^\s*处方[::\s]*/, '')
|
||||
.replace(/^\s*中药处方[::\s]*/, '')
|
||||
.trimStart()
|
||||
}
|
||||
|
||||
function stripHerbPasteNote(name: string): string {
|
||||
return name
|
||||
.replace(/([^)]*)$/u, '')
|
||||
.replace(/\([^)]*\)$/u, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function parseHerbPasteToken(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: stripHerbPasteNote(name), dosage: dose }))
|
||||
}
|
||||
|
||||
const m = s.match(/^(.+?)\s*(\d+(?:\.\d+)?)\s*(?:克|g|G|钱)?$/i)
|
||||
if (m) {
|
||||
const name = stripHerbPasteNote(m[1].trim())
|
||||
const dose = parseFloat(m[2])
|
||||
if (!name || !Number.isFinite(dose) || dose <= 0) return null
|
||||
return [{ name, dosage: dose }]
|
||||
}
|
||||
|
||||
const nameOnly = stripHerbPasteNote(s)
|
||||
if (nameOnly.length >= 2 && nameOnly.length <= 16) {
|
||||
return [{ name: nameOnly, dosage: 6 }]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function chunkRecipePasteText(text: string): string[] {
|
||||
const t = stripRecipePasteHeaders(normalizeRecipePasteInput(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) && !parseHerbPasteToken(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 parseRecipePasteToHerbs(text: string): { name: string; dosage: number }[] {
|
||||
const herbs: { name: string; dosage: number }[] = []
|
||||
for (const c of chunkRecipePasteText(text)) {
|
||||
const parsed = parseHerbPasteToken(c)
|
||||
if (!parsed) continue
|
||||
herbs.push(...parsed)
|
||||
}
|
||||
return herbs
|
||||
}
|
||||
|
||||
async function resolvePasteHerbNameFromLibrary(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 = parseRecipePasteToHerbs(pasteRecipeText.value)
|
||||
if (!parsed.length) {
|
||||
feedback.msgWarning('未能从文本中解析出药材,请检查格式(药名 + 剂量,或用顿号/换行分隔)')
|
||||
return
|
||||
}
|
||||
pasteRecipeImportLoading.value = true
|
||||
try {
|
||||
const resolved: Array<{ name: string; dosage: number }> = []
|
||||
const skippedNames: string[] = []
|
||||
for (const row of parsed) {
|
||||
const name = await resolvePasteHerbNameFromLibrary(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') {
|
||||
editForm.herbs = resolved
|
||||
} else {
|
||||
editForm.herbs.push(...resolved)
|
||||
}
|
||||
const dupNames = findDuplicateHerbNamesLocal()
|
||||
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 addHerb = () => {
|
||||
editForm.herbs.push({
|
||||
|
||||
Reference in New Issue
Block a user