3182 lines
128 KiB
Vue
3182 lines
128 KiB
Vue
<template>
|
||
<el-drawer
|
||
v-model="visible"
|
||
:title="drawerTitle"
|
||
size="1200px"
|
||
:close-on-click-modal="false"
|
||
@close="handleClose"
|
||
>
|
||
<div v-if="!savedPrescription" class="prescription-edit">
|
||
<el-form ref="formRef" :model="formData" label-width="100px" class="prescription-form">
|
||
<div class="info-section">
|
||
<div class="section-title">患者信息</div>
|
||
<el-row :gutter="16">
|
||
<el-col :span="8">
|
||
<el-form-item label="姓名">
|
||
<span>{{ formData.patient_name }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="性别">
|
||
<span>{{ formData.gender_desc }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="年龄">
|
||
<span>{{ formData.age }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="电话">
|
||
<span>{{ formData.phone }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="门诊号">
|
||
<el-input v-model="formData.visit_no" placeholder="门诊号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="section-title">诊断信息</div>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="面象" prop="pulse">
|
||
<el-input v-model="formData.pulse" placeholder="面象" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="舌象" prop="tongue">
|
||
<el-input v-model="formData.tongue" placeholder="舌象" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<!-- <el-col :span="12">
|
||
<el-form-item label="舌象详情" prop="tongue_image">
|
||
<el-input v-model="formData.tongue_image" placeholder="舌象详情" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="脉象详情" prop="pulse_condition">
|
||
<el-input v-model="formData.pulse_condition" placeholder="脉象详情" />
|
||
</el-form-item>
|
||
</el-col> -->
|
||
<el-col :span="24">
|
||
<el-form-item label="临床诊断" prop="clinical_diagnosis">
|
||
<el-input
|
||
v-model="formData.clinical_diagnosis"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请输入临床诊断"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<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> 味(主方 {{ mainHerbCount }} / 辅方
|
||
{{ auxHerbCount }})· 可手动添加、从处方库或粘贴药方批量录入
|
||
</p>
|
||
<p v-if="herbsLocked" class="rp-toolbar__lock-tip">
|
||
<el-tag type="danger" size="small" effect="plain">禁用修改</el-tag>
|
||
含「禁用修改」处方库模板,药材已锁定,不可增删改,如需调整请重新「从处方库导入」覆盖
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="rp-toolbar__actions" role="toolbar" aria-label="处方药材快捷操作">
|
||
<el-button type="primary" size="small" :icon="CirclePlus" :disabled="herbsLocked" @click="handleAddHerb('主方')">
|
||
添加主方
|
||
</el-button>
|
||
<el-button type="primary" plain size="small" :disabled="herbsLocked" @click="handleAddHerb('辅方')">
|
||
添加辅方
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
size="small"
|
||
:icon="CollectionTag"
|
||
@click="openLibraryDialog"
|
||
>
|
||
从处方库导入
|
||
</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">
|
||
<el-input-number
|
||
v-model="formData.amount"
|
||
:min="0"
|
||
:precision="2"
|
||
placeholder="请输入处方总价(元)"
|
||
class="!w-40"
|
||
/>
|
||
<span class="ml-2 text-gray-500">元</span>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="herb-formula-block">
|
||
<div class="herb-formula-block__head">
|
||
<el-tag type="primary" size="small">主方</el-tag>
|
||
</div>
|
||
<div class="herbs-editor-grid">
|
||
<div
|
||
v-for="row in mainHerbRows"
|
||
:key="'main-' + row.index"
|
||
class="herb-editor-card"
|
||
: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" />
|
||
<div
|
||
v-if="isDuplicateHerbName(row.index)"
|
||
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
|
||
>
|
||
该药名与其他行重复,请合并剂量或删除多余行
|
||
</div>
|
||
<div class="herb-editor-card__row">
|
||
<el-input-number
|
||
v-model="formData.herbs[row.index].dosage"
|
||
:min="0.1"
|
||
:step="1"
|
||
:precision="1"
|
||
:disabled="herbsLocked"
|
||
placeholder="剂量(克)"
|
||
class="herb-editor-card__dose"
|
||
/>
|
||
<span class="text-gray-500 shrink-0">克</span>
|
||
<el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="herb-formula-block mt-4">
|
||
<div class="herb-formula-block__head">
|
||
<el-tag type="warning" size="small">辅方</el-tag>
|
||
</div>
|
||
<div class="herbs-editor-grid">
|
||
<div
|
||
v-for="row in auxHerbRows"
|
||
:key="'aux-' + row.index"
|
||
class="herb-editor-card"
|
||
: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" />
|
||
<div
|
||
v-if="isDuplicateHerbName(row.index)"
|
||
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
|
||
>
|
||
该药名与其他行重复,请合并剂量或删除多余行
|
||
</div>
|
||
<div class="herb-editor-card__row">
|
||
<el-input-number
|
||
v-model="formData.herbs[row.index].dosage"
|
||
:min="0.1"
|
||
:step="1"
|
||
:precision="1"
|
||
:disabled="herbsLocked"
|
||
placeholder="剂量(克)"
|
||
class="herb-editor-card__dose"
|
||
/>
|
||
<span class="text-gray-500 shrink-0">克</span>
|
||
<el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<p v-if="!auxHerbRows.length" class="text-sm text-gray-400 py-2">暂无辅方药材</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<el-row :gutter="16">
|
||
|
||
<el-col :span="8">
|
||
<el-form-item label="处方类型" prop="prescription_type">
|
||
<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-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="24">
|
||
<div class="usage-formula-title usage-formula-title--main">主方用法</div>
|
||
</el-col>
|
||
<!-- 用量字段 -->
|
||
<el-col :span="8">
|
||
<el-form-item label="用量" prop="dosage_amount">
|
||
<!-- 浓缩水丸:1-10g 下拉选择 -->
|
||
<el-select
|
||
v-if="formData.prescription_type === '浓缩水丸'"
|
||
v-model="formData.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="!w-full"
|
||
>
|
||
<el-option :label="1 + 'g'" :value="1" />
|
||
<el-option :label="2 + 'g'" :value="2" />
|
||
<el-option :label="3 + 'g'" :value="3" />
|
||
<el-option :label="4 + 'g'" :value="4" />
|
||
<el-option :label="5 + 'g'" :value="5" />
|
||
<el-option :label="6 + 'g'" :value="6" />
|
||
<el-option :label="7 + 'g'" :value="7" />
|
||
<el-option :label="8 + 'g'" :value="8" />
|
||
<el-option :label="9 + 'g'" :value="9" />
|
||
<el-option :label="10 + 'g'" :value="10" />
|
||
</el-select>
|
||
<!-- 饮片:50-250ml 下拉选择,步进50 -->
|
||
<el-select
|
||
v-else-if="formData.prescription_type === '饮片'"
|
||
v-model="formData.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="!w-full"
|
||
>
|
||
<el-option label="50ml" :value="50" />
|
||
<el-option label="100ml" :value="100" />
|
||
<el-option label="120ml" :value="120" />
|
||
<el-option label="150ml" :value="150" />
|
||
<el-option label="180ml" :value="180" />
|
||
<el-option label="200ml" :value="200" />
|
||
<el-option label="250ml" :value="250" />
|
||
</el-select>
|
||
<!-- 其他类型:自由输入 -->
|
||
<el-input-number
|
||
v-else
|
||
v-model="formData.dosage_amount"
|
||
:min="0"
|
||
:precision="2"
|
||
class="!w-full"
|
||
/>
|
||
<span v-if="formData.prescription_type !== '浓缩水丸' && formData.prescription_type !== '饮片'" class="ml-2">{{ formData.dosage_unit }}</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col v-if="formData.prescription_type === '浓缩水丸'" :span="8">
|
||
<el-form-item label=" " prop="dosage_bag_count" label-width="12px">
|
||
<el-select v-model="formData.dosage_bag_count" placeholder="请选择袋数" class="!w-[120px]">
|
||
<el-option label="1袋" :value="1" />
|
||
<el-option label="2袋" :value="2" />
|
||
<el-option label="3袋" :value="3" />
|
||
<el-option label="4袋" :value="4" />
|
||
<el-option label="5袋" :value="5" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<!-- 饮片每贴出包数 -->
|
||
<el-col v-if="formData.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="每贴出包数" prop="bags_per_dose">
|
||
<el-select v-model="formData.bags_per_dose" placeholder="请选择" class="!w-full">
|
||
<el-option label="1包" :value="1" />
|
||
<el-option label="2包" :value="2" />
|
||
<el-option label="3包" :value="3" />
|
||
<el-option label="4包" :value="4" />
|
||
<el-option label="5包" :value="5" />
|
||
<el-option label="6包" :value="6" />
|
||
<el-option label="7包" :value="7" />
|
||
<el-option label="8包" :value="8" />
|
||
<el-option label="9包" :value="9" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<!-- 饮片代煎选项 -->
|
||
<el-col v-if="formData.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="是否代煎" prop="need_decoction">
|
||
<el-radio-group v-model="formData.need_decoction">
|
||
<el-radio :label="true">代煎</el-radio>
|
||
<el-radio :label="false">不代煎</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="8">
|
||
<el-form-item label="服用天数" prop="usage_days">
|
||
<el-input-number v-model="formData.usage_days" :min="1" :max="365" class="!w-full" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="每天几次" prop="times_per_day">
|
||
<el-input-number v-model="formData.times_per_day" :min="1" :max="6" class="!w-full" placeholder="每天服用次数" />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<template v-if="auxHerbRows.length > 0">
|
||
<el-col :span="24">
|
||
<div class="usage-formula-title usage-formula-title--aux">辅方用法</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="用量">
|
||
<el-select
|
||
v-if="formData.prescription_type === '浓缩水丸'"
|
||
v-model="formData.aux_usage.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="!w-full"
|
||
>
|
||
<el-option v-for="n in 10" :key="n" :label="n + 'g'" :value="n" />
|
||
</el-select>
|
||
<el-select
|
||
v-else-if="formData.prescription_type === '饮片'"
|
||
v-model="formData.aux_usage.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="!w-full"
|
||
>
|
||
<el-option label="50ml" :value="50" />
|
||
<el-option label="100ml" :value="100" />
|
||
<el-option label="120ml" :value="120" />
|
||
<el-option label="150ml" :value="150" />
|
||
<el-option label="180ml" :value="180" />
|
||
<el-option label="200ml" :value="200" />
|
||
<el-option label="250ml" :value="250" />
|
||
</el-select>
|
||
<el-input-number
|
||
v-else
|
||
v-model="formData.aux_usage.dosage_amount"
|
||
:min="0"
|
||
:precision="2"
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col v-if="formData.prescription_type === '浓缩水丸'" :span="8">
|
||
<el-form-item label=" " label-width="12px">
|
||
<el-select
|
||
v-model="formData.aux_usage.dosage_bag_count"
|
||
placeholder="请选择袋数"
|
||
class="!w-[120px]"
|
||
>
|
||
<el-option label="1袋" :value="1" />
|
||
<el-option label="2袋" :value="2" />
|
||
<el-option label="3袋" :value="3" />
|
||
<el-option label="4袋" :value="4" />
|
||
<el-option label="5袋" :value="5" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col v-if="formData.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="是否代煎">
|
||
<el-radio-group v-model="formData.aux_usage.need_decoction">
|
||
<el-radio :label="true">代煎</el-radio>
|
||
<el-radio :label="false">不代煎</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col v-if="formData.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="每贴出包数">
|
||
<el-select v-model="formData.aux_usage.bags_per_dose" placeholder="请选择" class="!w-full">
|
||
<el-option v-for="n in 9" :key="n" :label="n + '包'" :value="n" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="每天几次">
|
||
<el-input-number
|
||
v-model="formData.aux_usage.times_per_day"
|
||
:min="1"
|
||
:max="6"
|
||
class="!w-full"
|
||
placeholder="每天服用次数"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="服用天数">
|
||
<el-input-number
|
||
v-model="formData.aux_usage.usage_days"
|
||
:min="1"
|
||
:max="365"
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</template>
|
||
<!-- <el-col :span="8">
|
||
<el-form-item label="剂数" prop="dose_count">
|
||
<el-input-number v-model="formData.dose_count" :min="1" :max="999" class="!w-full" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="剂量单位" prop="dose_unit">
|
||
<el-select v-model="formData.dose_unit" 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-select>
|
||
</el-form-item>
|
||
</el-col> -->
|
||
<el-col :span="24">
|
||
<el-form-item label="用法" prop="usage_instruction">
|
||
<el-input v-model="formData.usage_instruction" placeholder="" maxlength="200" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服用时间" prop="usage_time">
|
||
<el-select v-model="formData.usage_time" 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-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服用方式" prop="usage_way">
|
||
<el-select v-model="formData.usage_way" 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-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="忌口" prop="dietary_taboo">
|
||
<el-select v-model="formData.dietary_taboo" multiple 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-option label="咖啡" value="咖啡" />
|
||
<el-option label="烟草" value="烟草" />
|
||
<el-option label="萝卜" value="萝卜" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="其他说明" prop="usage_notes">
|
||
<el-input v-model="formData.usage_notes" type="textarea" :rows="2" placeholder="其他服用注意事项" maxlength="200" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="医师" prop="doctor_name">
|
||
<el-input v-model="formData.doctor_name" placeholder="医师姓名" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="是否共享" prop="is_shared">
|
||
<el-switch
|
||
v-model="formData.is_shared"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
active-text="所有人可见"
|
||
inactive-text="仅自己可见"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="医师签名" prop="doctor_signature" required>
|
||
<div class="signature-pad-wrap">
|
||
<canvas
|
||
ref="signatureCanvasRef"
|
||
class="signature-canvas"
|
||
@pointerdown.prevent="onSignaturePointerDown"
|
||
@pointermove.prevent="onSignaturePointerMove"
|
||
@pointerup.prevent="onSignaturePointerUp"
|
||
@pointercancel.prevent="onSignaturePointerUp"
|
||
/>
|
||
<div class="signature-actions">
|
||
<el-button size="small" @click="clearSignature">清空签名</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="text-xs text-amber-600 mt-1">* 必填,请在框内手写医师签名</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-form>
|
||
|
||
</div>
|
||
|
||
<div v-else class="rx-wrap">
|
||
<el-tabs
|
||
v-model="prescriptionTabType"
|
||
class="rx-slip-tabs"
|
||
>
|
||
<el-tab-pane label="药房联" name="internal" />
|
||
<el-tab-pane label="处方联" name="user" />
|
||
</el-tabs>
|
||
<div class="rx-toolbar">
|
||
<el-tag v-if="savedPrescription.void_status === 1" type="danger" size="small">已作废</el-tag>
|
||
<el-tag v-else-if="approvedPreviewOnly" type="success" size="small">已通过</el-tag>
|
||
<el-tag
|
||
v-else-if="Number(savedPrescription.audit_status) === 0"
|
||
type="warning"
|
||
size="small"
|
||
>待审核</el-tag>
|
||
<el-tag v-else-if="Number(savedPrescription.audit_status) === 2" type="danger" size="small">已驳回</el-tag>
|
||
<div class="flex-1" />
|
||
</div>
|
||
|
||
<div v-if="savedPrescription.void_status === 1" class="rx-status rx-status-danger">
|
||
作废人:{{ savedPrescription.void_by_name || '—' }},作废时间:{{ formatVoidTime(savedPrescription.void_time) }}
|
||
</div>
|
||
<div
|
||
v-else-if="Number(savedPrescription.audit_status) === 2"
|
||
class="rx-status rx-status-danger"
|
||
>
|
||
<div class="font-medium">处方审核:已驳回</div>
|
||
<div v-if="savedPrescription.audit_remark" class="mt-1">驳回意见:{{ savedPrescription.audit_remark }}</div>
|
||
</div>
|
||
|
||
<div class="prescription-preview-content">
|
||
<div v-if="savedPrescription" ref="prescriptionPrintRef" class="rx-paper">
|
||
<div class="rx-title">{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}</div>
|
||
<div class="rx-notice">
|
||
<span class="rx-notice-text">
|
||
{{
|
||
prescriptionTabType === 'user'
|
||
? '服药前请核对姓名、电话、医生等信息以及医嘱等要点'
|
||
: '服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点'
|
||
}}
|
||
</span>
|
||
<span class="rx-notice-meta">
|
||
<span>日期:{{ rxDateText }}</span>
|
||
<span>编号:{{ rxSerialText }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="rx-info">
|
||
<div class="rx-info-row">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">姓名</span>
|
||
<span class="rx-val">{{ savedPrescription.patient_name || '—' }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">性别</span>
|
||
<span class="rx-val">{{ slipGenderLabel(savedPrescription.gender) }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">年龄</span>
|
||
<span class="rx-val">{{ slipAgeText(savedPrescription.age) }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">电话</span>
|
||
<span class="rx-val">{{ savedPrescription.phone || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="rx-info-row rx-info-row-full">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">门诊号</span>
|
||
<span class="rx-val">{{ savedPrescription.visit_no || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="savedPrescription.pulse || savedPrescription.tongue" class="rx-info-row rx-info-row-full">
|
||
<div v-if="savedPrescription.pulse" class="rx-info-cell">
|
||
<span class="rx-key">面象</span>
|
||
<span class="rx-val">{{ savedPrescription.pulse }}</span>
|
||
</div>
|
||
<div v-if="savedPrescription.tongue" class="rx-info-cell">
|
||
<span class="rx-key">舌象</span>
|
||
<span class="rx-val">{{ savedPrescription.tongue }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="rx-info-row rx-info-row-full">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">临床诊断</span>
|
||
<span class="rx-val">{{ savedPrescription.clinical_diagnosis || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rx-rp">
|
||
<div class="rx-watermark">{{ prescriptionTabType === 'internal' ? '药房联' : '处方联' }}</div>
|
||
<div class="rx-rp-head">
|
||
<div class="rx-rp-label">Rp.</div>
|
||
<div class="rx-rp-cols-head">
|
||
<div class="rx-rp-col-head">
|
||
<span>用药 (单剂)</span>
|
||
<span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
|
||
</div>
|
||
<div class="rx-rp-col-head">
|
||
<span>用药 (单剂)</span>
|
||
<span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="rx-herbs">
|
||
<template v-if="prescriptionTabType === 'internal'">
|
||
<template v-if="savedMainHerbs.length">
|
||
<div class="rx-herb-section-label">主方</div>
|
||
<div
|
||
v-for="(h, i) in savedMainHerbs"
|
||
:key="'main-' + i"
|
||
class="rx-herb-cell"
|
||
>
|
||
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }}克)</span>
|
||
<span class="rx-herb-total">{{ rxHerbTotal(h.dosage) }}克</span>
|
||
</div>
|
||
</template>
|
||
<template v-if="savedAuxHerbs.length">
|
||
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
|
||
<div
|
||
v-for="(h, i) in savedAuxHerbs"
|
||
:key="'aux-' + i"
|
||
class="rx-herb-cell"
|
||
>
|
||
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }}克)</span>
|
||
<span class="rx-herb-total">{{ rxHerbTotal(h.dosage) }}克</span>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
<template v-else>
|
||
<template v-if="savedMainHerbs.length">
|
||
<div class="rx-herb-section-label">主方</div>
|
||
<div
|
||
v-for="(h, i) in savedMainHerbs"
|
||
:key="'user-main-' + i"
|
||
class="rx-herb-cell"
|
||
>
|
||
<span class="rx-herb-name">{{ h.name }}</span>
|
||
<span class="rx-herb-total">{{ h.dosage }}克</span>
|
||
</div>
|
||
</template>
|
||
<template v-if="savedAuxHerbs.length">
|
||
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
|
||
<div
|
||
v-for="(h, i) in savedAuxHerbs"
|
||
:key="'user-aux-' + i"
|
||
class="rx-herb-cell"
|
||
>
|
||
<span class="rx-herb-name">{{ h.name }}</span>
|
||
<span class="rx-herb-total">{{ h.dosage }}克</span>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rx-text">
|
||
<template v-if="prescriptionTabType === 'internal'">
|
||
<p v-if="savedAuxUsageText">主方服法:{{ rxUsageText }}</p>
|
||
<p v-else>服法:{{ rxUsageText }}</p>
|
||
<p v-if="savedAuxUsageText">辅方服法:{{ savedAuxUsageText }}</p>
|
||
</template>
|
||
<p v-if="rxAdviceText">医嘱:{{ rxAdviceText }}</p>
|
||
<p v-if="prescriptionTabType === 'internal' && rxRemarkText">备注:{{ rxRemarkText }}</p>
|
||
<p v-if="savedPrescription.dietary_taboo && savedPrescription.dietary_taboo.length">
|
||
忌口:{{ Array.isArray(savedPrescription.dietary_taboo) ? savedPrescription.dietary_taboo.join('、') : savedPrescription.dietary_taboo }}
|
||
</p>
|
||
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">药房备注:{{ rxPharmacyRemarkText }}</p>
|
||
<p
|
||
v-if="prescriptionTabType === 'internal' && rxOutPelletText && savedPrescription.prescription_type !== '饮片'"
|
||
class="rx-text-warn"
|
||
>
|
||
出丸:{{ rxOutPelletText }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="rx-bottom">
|
||
<div
|
||
class="rx-bot-row rx-bot-row-1"
|
||
:class="{ 'rx-bot-row-1--user': prescriptionTabType === 'user' }"
|
||
>
|
||
<div class="rx-bot-cell rx-bot-stack rx-bot-doctor">
|
||
<div class="rx-bot-label">医师</div>
|
||
<div class="rx-bot-doctor-body">
|
||
<img
|
||
v-if="savedPrescription.doctor_signature"
|
||
:src="savedPrescription.doctor_signature"
|
||
alt="医师签名"
|
||
class="rx-bot-sign-img"
|
||
/>
|
||
<div v-if="!savedPrescription.doctor_signature" class="rx-bot-doctor-name">
|
||
{{ savedPrescription.doctor_name || '—' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">类型:</span>
|
||
<span class="rx-bot-meta-val">{{ rxTypeText }}</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">天数:</span>
|
||
<span class="rx-bot-meta-val">{{ rxSlipMedicationDaysText }}</span>
|
||
</div>
|
||
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">剂量:</span>
|
||
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}克</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="prescriptionTabType === 'user'" class="rx-bot-row rx-hospital-row">
|
||
<div class="rx-hospital-line">成都双流甄养堂互联网医院有限公司 联系方式:4001667339</div>
|
||
<div class="rx-hospital-line">地址:四川省成都市双流区黄甲街道黄龙大道二段280号</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详细病历 - 单独A3纸张 -->
|
||
<div v-if="savedPrescription.case_record && !hideCaseRecordPreview" ref="caseRecordPrintRef" class="case-record-a3">
|
||
<div class="case-record-a3-inner">
|
||
<h2 class="case-record-a3-title">{{ templateConfig.stationName }} 详细病历</h2>
|
||
<div class="case-record-a3-body">
|
||
<!-- 基本信息 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">基本信息</div>
|
||
<div class="cr-grid">
|
||
<div class="cr-item"><span class="cr-label">诊单ID</span>{{ savedPrescription.case_record.patient_id || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">姓名</span>{{ savedPrescription.case_record.patient_name || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">身份证号</span>{{ savedPrescription.case_record.id_card || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">手机号</span>{{ savedPrescription.case_record.phone || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">性别</span>{{ savedPrescription.gender_desc }}</div>
|
||
<div class="cr-item"><span class="cr-label">年龄</span>{{ savedPrescription.case_record.age ?? '—' }}岁</div>
|
||
<div class="cr-item"><span class="cr-label">婚姻状态</span>{{ ['未婚','已婚','离异'][savedPrescription.case_record.marital_status] ?? '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">身高</span>{{ savedPrescription.case_record.height ?? '—' }}cm</div>
|
||
<div class="cr-item"><span class="cr-label">体重</span>{{ savedPrescription.case_record.weight ?? '—' }}kg</div>
|
||
<div class="cr-item"><span class="cr-label">地区</span>{{ savedPrescription.case_record.region || '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 生命体征 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">生命体征</div>
|
||
<div class="cr-grid">
|
||
<div class="cr-item"><span class="cr-label">血压</span>{{ (savedPrescription.case_record.systolic_pressure != null && savedPrescription.case_record.diastolic_pressure != null) ? (savedPrescription.case_record.systolic_pressure + '/' + savedPrescription.case_record.diastolic_pressure + ' mmHg') : '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">空腹血糖</span>{{ savedPrescription.case_record.fasting_blood_sugar != null ? savedPrescription.case_record.fasting_blood_sugar + ' mmol/L' : '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 主诉 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">主诉</div>
|
||
<div class="cr-grid">
|
||
<div class="cr-item"><span class="cr-label">诊断日期</span>{{ savedPrescription.case_record.diagnosis_date || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">诊断类型</span>{{ getDictLabel(diagnosisTypeOptions, savedPrescription.case_record.diagnosis_type) || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">证型</span>{{ getDictLabel(syndromeTypeOptions, savedPrescription.case_record.syndrome_type) || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">糖尿病期数</span>{{ getDictLabel(diabetesTypeOptions, savedPrescription.case_record.diabetes_type) || '—' }}</div>
|
||
<div class="cr-item"><span class="cr-label">发现糖尿病患病史</span>{{ formatDiabetesDiscoveryDisplay(savedPrescription.case_record?.diabetes_discovery_year) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">当地医院诊断</span>{{ formatLocalDiagnosis(savedPrescription.case_record.local_hospital_diagnosis) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">当地就诊医院</span>{{ savedPrescription.case_record.local_hospital_name || '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 现病史 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">现病史</div>
|
||
<div class="cr-grid">
|
||
<div class="cr-item cr-full"><span class="cr-label">口腔感觉</span>{{ getMultiLabels(appetiteOptions, savedPrescription.case_record.appetite) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">每日饮水量</span>{{ getDictLabel(waterIntakeOptions, savedPrescription.case_record.water_intake) || '—' }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">体重变化</span>{{ getDictLabel(weightChangeOptions, savedPrescription.case_record.weight_change) || '—' }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">脂肪肝程度</span>{{ getDictLabel(fattyLiverDegreeOptions, savedPrescription.case_record.fatty_liver_degree) || '—' }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">饮食情况</span>{{ getMultiLabels(dietConditionOptions, savedPrescription.case_record.diet_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">肢体感觉</span>{{ getMultiLabels(bodyFeelingOptions, savedPrescription.case_record.body_feeling) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">睡眠情况</span>{{ getMultiLabels(sleepConditionOptions, savedPrescription.case_record.sleep_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">眼睛情况</span>{{ getMultiLabels(eyeConditionOptions, savedPrescription.case_record.eye_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">头部感觉</span>{{ getMultiLabels(headFeelingOptions, savedPrescription.case_record.head_feeling) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">出汗情况</span>{{ getMultiLabels(sweatConditionOptions, savedPrescription.case_record.sweat_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">皮肤情况</span>{{ getMultiLabels(skinConditionOptions, savedPrescription.case_record.skin_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">小便情况</span>{{ getMultiLabels(urineConditionOptions, savedPrescription.case_record.urine_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">大便情况</span>{{ getMultiLabels(stoolConditionOptions, savedPrescription.case_record.stool_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">腰肾情况</span>{{ getMultiLabels(kidneyConditionOptions, savedPrescription.case_record.kidney_condition) }}</div>
|
||
<div class="cr-item cr-full"><span class="cr-label">其他补充</span>{{ savedPrescription.case_record.symptoms || '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 既往史 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">既往史</div>
|
||
<div class="cr-item cr-full"><span class="cr-label"></span>{{ getMultiLabels(pastHistoryOptions, savedPrescription.case_record.past_history) }}</div>
|
||
</div>
|
||
<!-- 其他病史 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">其他病史</div>
|
||
<div class="cr-grid">
|
||
<div class="cr-item"><span class="cr-label">外伤史</span>{{ savedPrescription.case_record.trauma_history === 1 ? '有' : '无' }}</div>
|
||
<div class="cr-item"><span class="cr-label">手术史</span>{{ savedPrescription.case_record.surgery_history === 1 ? '有' : '无' }}</div>
|
||
<div class="cr-item"><span class="cr-label">过敏史</span>{{ savedPrescription.case_record.allergy_history === 1 ? '有' : '无' }}</div>
|
||
<div class="cr-item"><span class="cr-label">家族病史</span>{{ savedPrescription.case_record.family_history === 1 ? '有' : '无' }}</div>
|
||
<div class="cr-item"><span class="cr-label">妊娠哺乳史</span>{{ savedPrescription.case_record.pregnancy_history === 1 ? '有' : '无' }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 诊断信息 -->
|
||
<div class="cr-section">
|
||
<div class="cr-section-title">诊断信息</div>
|
||
<div class="cr-grid" v-if="savedPrescription.case_record.tongue_images?.length">
|
||
<div class="cr-item cr-full">
|
||
<span class="cr-label">舌苔照片</span>
|
||
<div class="cr-images">
|
||
<img v-for="(img, idx) in savedPrescription.case_record.tongue_images" :key="idx" :src="getImageUrl(img)" class="cr-img" crossorigin="anonymous" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cr-grid">
|
||
<div class="cr-item cr-full"><span class="cr-label">舌象</span>{{ savedPrescription.case_record.tongue_coating || '—' }}</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="savedPrescription && (savedPrescription.audit_by_name || savedPrescription.audit_remark)"
|
||
class="rx-audit"
|
||
>
|
||
<div v-if="savedPrescription.audit_by_name">
|
||
审核人:{{ savedPrescription.audit_by_name }}
|
||
<span v-if="savedPrescription.audit_time" class="ml-2 text-gray-400">
|
||
{{ formatAuditTime(savedPrescription.audit_time) }}
|
||
</span>
|
||
</div>
|
||
<div v-if="savedPrescription.audit_remark" class="mt-1">审核意见:{{ savedPrescription.audit_remark }}</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end gap-2 mt-4">
|
||
<el-button @click="handleDownloadImage">
|
||
<el-icon><Download /></el-icon>
|
||
下载图片
|
||
</el-button>
|
||
<el-button type="primary" @click="handleDownloadPdf">
|
||
<el-icon><Document /></el-icon>
|
||
下载PDF
|
||
</el-button>
|
||
<el-tooltip
|
||
v-if="savedPrescription.void_status !== 1 && !approvedPreviewOnly"
|
||
:disabled="!hasPrescriptionOrderBlockVoid"
|
||
content="该处方已存在业务订单,无法作废,请前往已开处方里修改"
|
||
placement="top"
|
||
>
|
||
<span class="inline-block">
|
||
<el-button
|
||
type="danger"
|
||
plain
|
||
:disabled="hasPrescriptionOrderBlockVoid"
|
||
:loading="voiding"
|
||
@click="handleVoid"
|
||
>
|
||
作废处方
|
||
</el-button>
|
||
</span>
|
||
</el-tooltip>
|
||
<el-button v-if="!approvedPreviewOnly" @click="handleNewPrescription">新建处方</el-button>
|
||
<el-button @click="handleClose">关闭</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="!savedPrescription" #footer>
|
||
<div class="flex justify-end gap-2">
|
||
<el-button @click="handleClose">取消</el-button>
|
||
<el-button type="primary" @click="handleSave" :loading="saving">保存处方</el-button>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- 处方库选择弹窗 -->
|
||
<el-dialog
|
||
v-model="showLibraryDialog"
|
||
title="从处方库导入"
|
||
width="800px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="mb-4 flex gap-3">
|
||
<el-input
|
||
v-model="librarySearchName"
|
||
class="flex-1"
|
||
placeholder="请输入处方名称搜索"
|
||
clearable
|
||
@keyup.enter="searchLibrary"
|
||
@clear="searchLibrary"
|
||
>
|
||
<template #append>
|
||
<el-button :icon="Search" @click="searchLibrary" />
|
||
</template>
|
||
</el-input>
|
||
<el-select
|
||
v-model="librarySearchFormulaType"
|
||
placeholder="处方类型"
|
||
clearable
|
||
class="w-[140px]"
|
||
@change="searchLibrary"
|
||
>
|
||
<el-option label="全部" value="" />
|
||
<el-option label="主方" value="主方" />
|
||
<el-option label="辅方" value="辅方" />
|
||
</el-select>
|
||
</div>
|
||
<div class="mb-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="libraryImportMode">
|
||
<el-radio label="replace">覆盖同类型药材</el-radio>
|
||
<el-radio label="append">追加到末尾</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="libraryLoading"
|
||
:data="libraryList"
|
||
height="400"
|
||
@row-click="handleSelectLibrary"
|
||
style="cursor: pointer"
|
||
>
|
||
<el-table-column label="处方名称" prop="prescription_name" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="处方类型" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.formula_type === '辅方' ? 'warning' : 'primary'" size="small">
|
||
{{ row.formula_type || '主方' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="药材数量" width="100">
|
||
<template #default="{ row }">
|
||
{{ row.herbs?.length || 0 }}味
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="药材明细" min-width="300" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.herbs && row.herbs.length > 0">
|
||
{{ row.herbs.map((h: any) => `${h.name} ${h.dosage}g`).join('、') }}
|
||
</span>
|
||
<span v-else class="text-gray-400">暂无药材</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="是否公开" width="110">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.is_public ? 'success' : 'info'" size="small">
|
||
{{ row.is_public ? '所有人可见' : '仅自己可见' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建人" prop="creator_name" width="100" />
|
||
<el-table-column label="操作" width="100" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click.stop="handleImportLibrary(row)">
|
||
导入
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="flex justify-end mt-4">
|
||
<el-pagination
|
||
v-model:current-page="libraryPage"
|
||
v-model:page-size="libraryPageSize"
|
||
:total="libraryTotal"
|
||
:page-sizes="[10, 15, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@current-change="loadLibraryList"
|
||
@size-change="loadLibraryList"
|
||
/>
|
||
</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>
|
||
<span class="text-xs text-gray-500">(粘贴导入至主方,保留已有辅方)</span>
|
||
</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">
|
||
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'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import useAppStore from '@/stores/modules/app'
|
||
import { CirclePlus, CollectionTag, Download, Document, EditPen } from '@element-plus/icons-vue'
|
||
import { Search } from '@element-plus/icons-vue'
|
||
|
||
type FormulaType = '主方' | '辅方'
|
||
|
||
interface Herb {
|
||
name: string
|
||
dosage: number
|
||
formula_type?: FormulaType
|
||
/** 来自「禁用修改」处方库模板:锁定后不可增删改,只能再次导入覆盖 */
|
||
locked?: boolean
|
||
}
|
||
|
||
type AuxUsageForm = {
|
||
dosage_amount?: number
|
||
dosage_bag_count: number
|
||
need_decoction: boolean
|
||
bags_per_dose: number
|
||
times_per_day: number
|
||
usage_days: number
|
||
}
|
||
|
||
function normalizeFormulaType(v: unknown): FormulaType {
|
||
return v === '辅方' ? '辅方' : '主方'
|
||
}
|
||
|
||
function normalizeHerbRow(raw: any): Herb {
|
||
const row: Herb = {
|
||
name: String(raw?.name ?? '').trim(),
|
||
dosage: Number(raw?.dosage) || 0,
|
||
formula_type: normalizeFormulaType(raw?.formula_type)
|
||
}
|
||
if (raw?.locked === true || raw?.locked === 1) {
|
||
row.locked = true
|
||
}
|
||
return row
|
||
}
|
||
|
||
function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm {
|
||
if (prescriptionType === '饮片') {
|
||
return {
|
||
dosage_amount: 50,
|
||
dosage_bag_count: 1,
|
||
need_decoction: false,
|
||
bags_per_dose: 1,
|
||
times_per_day: 3,
|
||
usage_days: 7
|
||
}
|
||
}
|
||
if (prescriptionType === '浓缩水丸') {
|
||
return {
|
||
dosage_amount: 5,
|
||
dosage_bag_count: 1,
|
||
need_decoction: false,
|
||
bags_per_dose: 1,
|
||
times_per_day: 3,
|
||
usage_days: 7
|
||
}
|
||
}
|
||
return {
|
||
dosage_amount: 1,
|
||
dosage_bag_count: 1,
|
||
need_decoction: false,
|
||
bags_per_dose: 1,
|
||
times_per_day: 3,
|
||
usage_days: 7
|
||
}
|
||
}
|
||
|
||
function normalizeAuxUsageForm(raw: unknown, prescriptionType: string): AuxUsageForm {
|
||
const base = defaultAuxUsage(prescriptionType)
|
||
if (!raw || typeof raw !== 'object') return { ...base }
|
||
const o = raw as Record<string, unknown>
|
||
return {
|
||
dosage_amount:
|
||
o.dosage_amount !== null && o.dosage_amount !== undefined && o.dosage_amount !== ''
|
||
? Number(o.dosage_amount)
|
||
: base.dosage_amount,
|
||
dosage_bag_count: o.dosage_bag_count != null ? Number(o.dosage_bag_count) || 1 : base.dosage_bag_count,
|
||
need_decoction: o.need_decoction === 1 || o.need_decoction === true,
|
||
bags_per_dose: o.bags_per_dose != null ? Number(o.bags_per_dose) || 1 : base.bags_per_dose,
|
||
times_per_day: o.times_per_day != null ? Number(o.times_per_day) || 3 : base.times_per_day,
|
||
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days
|
||
}
|
||
}
|
||
|
||
function buildUsageSegmentText(
|
||
usage: {
|
||
prescription_type?: string
|
||
dosage_amount?: number | null
|
||
dosage_unit?: string
|
||
dosage_bag_count?: number
|
||
times_per_day?: number
|
||
usage_way?: string
|
||
usage_time?: string
|
||
},
|
||
fallbackWay?: string,
|
||
fallbackTime?: string
|
||
): string {
|
||
const pt = usage.prescription_type || '浓缩水丸'
|
||
const times = Number(usage.times_per_day) > 0 ? Number(usage.times_per_day) : 3
|
||
const amount = usage.dosage_amount != null ? Number(usage.dosage_amount) : 10
|
||
const unit = usage.dosage_unit || (pt === '饮片' ? 'ml' : 'g')
|
||
const usageWay = usage.usage_way || fallbackWay || '温水送服'
|
||
const usageTime = usage.usage_time || fallbackTime || ''
|
||
const seg: string[] = []
|
||
seg.push(`每天${times}次`)
|
||
if (pt === '浓缩水丸') {
|
||
const bags = Number(usage.dosage_bag_count) > 0 ? Number(usage.dosage_bag_count) : 1
|
||
seg.push(`一次${bags}袋`)
|
||
seg.push(`每袋${amount}${unit}`)
|
||
} else {
|
||
seg.push(`一次${amount}${unit}`)
|
||
}
|
||
seg.push(usageWay)
|
||
if (usageTime) seg.push(usageTime)
|
||
return seg.join(', ')
|
||
}
|
||
|
||
interface PrescriptionForm {
|
||
diagnosis_id: number
|
||
appointment_id: number
|
||
prescription_name: string
|
||
prescription_type: string
|
||
dosage_amount: number | undefined // 用量数值
|
||
dosage_unit: string // 用量单位 (g 或 ml)
|
||
dosage_bag_count: number // 用量袋数(浓缩水丸,1-5袋)
|
||
need_decoction: boolean // 是否代煎(仅饮片)
|
||
bags_per_dose: number // 每贴出包数(仅饮片,1-9包)
|
||
patient_name: string
|
||
gender: number
|
||
gender_desc: string
|
||
age: number
|
||
phone: string
|
||
visit_no: string
|
||
prescription_date: string
|
||
pulse: string
|
||
tongue: string
|
||
tongue_image: string
|
||
pulse_condition: string
|
||
clinical_diagnosis: string
|
||
case_record: any
|
||
herbs: Herb[]
|
||
dose_count: number
|
||
dose_unit: string
|
||
usage_days: number
|
||
times_per_day: number
|
||
usage_instruction: string
|
||
usage_time: string
|
||
usage_way: string
|
||
dietary_taboo: string[]
|
||
usage_notes: string
|
||
amount: number
|
||
doctor_name: string
|
||
doctor_signature: string
|
||
is_shared: number
|
||
aux_usage: AuxUsageForm
|
||
}
|
||
|
||
const emit = defineEmits(['success'])
|
||
|
||
const visible = ref(false)
|
||
const formRef = ref()
|
||
const saving = ref(false)
|
||
const voiding = ref(false)
|
||
const prescriptionPrintRef = ref<HTMLElement>()
|
||
const caseRecordPrintRef = ref<HTMLElement>()
|
||
const signatureCanvasRef = ref<HTMLCanvasElement>()
|
||
/** 当前一笔的 pointerId(配合 setPointerCapture,移出画布仍跟手) */
|
||
const signatureActivePointerId = ref<number | null>(null)
|
||
/** 签名板 CSS 像素尺寸(内部分辨率按 DPR 放大,触摸更跟手) */
|
||
const SIGNATURE_CSS_W = 560
|
||
const SIGNATURE_CSS_H = 168
|
||
const savedPrescription = ref<any>(null)
|
||
/** 预览联次:药房联 / 处方联(与消费者处方查看一致) */
|
||
const prescriptionTabType = ref<'internal' | 'user'>('internal')
|
||
/** 诊单列表「查看」仅展示处方联,不展示详细病历 A3 */
|
||
const hideCaseRecordPreview = ref(false)
|
||
|
||
/** 已通过且未作废:与消费者处方「查看」一致,仅预览,不可作废/再开新方 */
|
||
const approvedPreviewOnly = computed(() => {
|
||
const s = savedPrescription.value
|
||
if (!s) return false
|
||
return Number(s.audit_status) === 1 && Number(s.void_status) !== 1
|
||
})
|
||
|
||
/** 已有关联业务订单(与列表 has_prescription_order 一致)则禁止作废 */
|
||
const hasPrescriptionOrderBlockVoid = computed(() => {
|
||
const s = savedPrescription.value
|
||
if (!s) return false
|
||
return Number(s.has_prescription_order) === 1
|
||
})
|
||
|
||
const drawerTitle = computed(() => (approvedPreviewOnly.value ? '查看处方' : '中医处方单'))
|
||
|
||
const savedMainHerbs = computed(() => {
|
||
const list = savedPrescription.value?.herbs
|
||
if (!Array.isArray(list)) return []
|
||
return list.filter((h: Herb) => normalizeFormulaType(h?.formula_type) === '主方')
|
||
})
|
||
|
||
const savedAuxHerbs = computed(() => {
|
||
const list = savedPrescription.value?.herbs
|
||
if (!Array.isArray(list)) return []
|
||
return list.filter((h: Herb) => normalizeFormulaType(h?.formula_type) === '辅方')
|
||
})
|
||
|
||
const savedAuxUsageText = computed(() => {
|
||
const v = savedPrescription.value
|
||
if (!v || !savedAuxHerbs.value.length) return ''
|
||
const aux = normalizeAuxUsageForm(v.aux_usage, v.prescription_type || '浓缩水丸')
|
||
return buildUsageSegmentText(
|
||
{
|
||
prescription_type: v.prescription_type,
|
||
dosage_amount: aux.dosage_amount,
|
||
dosage_unit: v.dosage_unit,
|
||
dosage_bag_count: aux.dosage_bag_count,
|
||
times_per_day: aux.times_per_day,
|
||
usage_way: v.usage_way,
|
||
usage_time: v.usage_time
|
||
},
|
||
v.usage_way,
|
||
v.usage_time
|
||
)
|
||
})
|
||
|
||
const savedSlipHerbsList = computed(() => {
|
||
const h = savedPrescription.value?.herbs
|
||
return Array.isArray(h) ? h : []
|
||
})
|
||
|
||
const padRxN = (n: number) => String(n).padStart(2, '0')
|
||
|
||
const rxDateText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return '—'
|
||
const ts = v.create_time ?? v.update_time ?? null
|
||
if (ts) {
|
||
const t = String(ts).length === 10 ? Number(ts) * 1000 : Number(ts)
|
||
const d = new Date(t)
|
||
if (!isNaN(d.getTime())) {
|
||
return `${d.getFullYear()}-${padRxN(d.getMonth() + 1)}-${padRxN(d.getDate())} ${padRxN(d.getHours())}:${padRxN(d.getMinutes())}`
|
||
}
|
||
}
|
||
return v.prescription_date || '—'
|
||
})
|
||
|
||
const rxSerialText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return '—'
|
||
const sn = String(v.sn ?? '').trim()
|
||
if (sn) return sn
|
||
const vn = String(v.visit_no ?? '').trim()
|
||
if (vn) return vn
|
||
return v.id ? `G${v.id}` : '—'
|
||
})
|
||
|
||
const rxDoseCount = computed(() => {
|
||
const n = Number(savedPrescription.value?.dose_count)
|
||
return Number.isFinite(n) && n > 0 ? n : 1
|
||
})
|
||
|
||
const rxPerDoseAmount = computed(() => {
|
||
const sum = savedSlipHerbsList.value.reduce((acc: number, h: any) => acc + (Number(h?.dosage) || 0), 0)
|
||
return formatRxNum(sum)
|
||
})
|
||
|
||
const rxHerbTotal = (dosage: number | string) => {
|
||
const d = Number(dosage) || 0
|
||
return formatRxNum(d * rxDoseCount.value)
|
||
}
|
||
|
||
const rxTypeText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return '—'
|
||
const t = v.prescription_type || '浓缩水丸'
|
||
if (t === '饮片') return '饮片'
|
||
return /丸|散|膏|片/.test(t) ? `浓缩丸-${t}` : t
|
||
})
|
||
|
||
const rxUsageText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return '—'
|
||
if (v.usage_text) return v.usage_text
|
||
return buildUsageSegmentText(v)
|
||
})
|
||
|
||
const rxAdviceText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return ''
|
||
return v.medical_advice || ''
|
||
})
|
||
|
||
const rxRemarkText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return ''
|
||
const parts: string[] = []
|
||
parts.push(`共${savedSlipHerbsList.value.length}味药`)
|
||
if (v.usage_notes) parts.push(String(v.usage_notes))
|
||
if (v.remark) parts.push(String(v.remark))
|
||
return parts.join(' ')
|
||
})
|
||
|
||
const rxPharmacyRemarkText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return ''
|
||
return v.pharmacy_remark || v.pharmacy_note || ''
|
||
})
|
||
|
||
/** 笺面用药天数:与 consumer/prescription/order_list 一致 */
|
||
const rxSlipMedicationDaysText = computed(() => {
|
||
const v = savedPrescription.value as any
|
||
if (!v) return '—'
|
||
const md = v.medication_days ?? v.usage_days
|
||
if (md != null && String(md).trim() !== '') return `${md} 天`
|
||
const dc = v.dose_count
|
||
if (dc != null && String(dc).trim() !== '' && Number(dc) > 0) return `${dc} 天`
|
||
return '—'
|
||
})
|
||
|
||
/** 主方出丸:用药疗程 medication_days → usage_days → dose_count;出丸 = 每袋用量×袋数×每天次数×服用天数 */
|
||
const slipPillGrams = computed(() => {
|
||
const d = savedPrescription.value as any
|
||
if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null
|
||
const days = Number(d.medication_days ?? d.usage_days ?? d.dose_count)
|
||
const times = Number(d.times_per_day)
|
||
const doseG = Number(d.dosage_amount)
|
||
const bags = Number(d.dosage_bag_count) > 0 ? Number(d.dosage_bag_count) : 1
|
||
if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null
|
||
if (days <= 0 || times <= 0 || doseG <= 0) return null
|
||
return days * times * doseG * bags
|
||
})
|
||
|
||
/** 辅方出丸:公式同主方,用 aux_usage 字段 */
|
||
const slipAuxPillGrams = computed(() => {
|
||
const d = savedPrescription.value as any
|
||
if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null
|
||
if (!savedAuxHerbs.value.length) return null
|
||
const aux = normalizeAuxUsageForm(d.aux_usage, d.prescription_type || '浓缩水丸')
|
||
const days = Number(d.medication_days ?? aux.usage_days)
|
||
const times = Number(aux.times_per_day)
|
||
const doseG = Number(aux.dosage_amount)
|
||
const bags = Number(aux.dosage_bag_count) > 0 ? Number(aux.dosage_bag_count) : 1
|
||
if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null
|
||
if (days <= 0 || times <= 0 || doseG <= 0) return null
|
||
return days * times * doseG * bags
|
||
})
|
||
|
||
/** 出丸:优先 slipPillGrams + slipAuxPillGrams,否则 out_pellet / 药材总量×剂数 */
|
||
const rxOutPelletGrams = computed(() => {
|
||
const main = slipPillGrams.value
|
||
const aux = slipAuxPillGrams.value
|
||
const mainNum = typeof main === 'number' && isFinite(main) && main > 0 ? main : 0
|
||
const auxNum = typeof aux === 'number' && isFinite(aux) && aux > 0 ? aux : 0
|
||
const sum = mainNum + auxNum
|
||
if (sum > 0) return formatRxNum(sum)
|
||
const v = savedPrescription.value as any
|
||
if (!v) return ''
|
||
const explicit = v.out_pellet || v.total_weight
|
||
if (explicit && Number(explicit) > 0) return formatRxNum(Number(explicit))
|
||
const list = savedSlipHerbsList.value
|
||
if (!list.length) return ''
|
||
const perDose = list.reduce((acc: number, h: any) => acc + (Number(h?.dosage) || 0), 0)
|
||
return formatRxNum(perDose * rxDoseCount.value)
|
||
})
|
||
|
||
const rxOutPelletText = computed(() => {
|
||
const out = rxOutPelletGrams.value
|
||
if (!out) return ''
|
||
const main = slipPillGrams.value
|
||
const aux = slipAuxPillGrams.value
|
||
if (
|
||
typeof main === 'number' && isFinite(main) && main > 0 &&
|
||
typeof aux === 'number' && isFinite(aux) && aux > 0
|
||
) {
|
||
return `${out}克(主方 ${formatRxNum(main)}克 + 辅方 ${formatRxNum(aux)}克)`
|
||
}
|
||
return `${out}克`
|
||
})
|
||
|
||
function formatRxNum(n: number): string {
|
||
if (!isFinite(n)) return '0'
|
||
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '')
|
||
}
|
||
|
||
function slipGenderLabel(g: unknown) {
|
||
if (g === 1 || g === '1') return '男'
|
||
if (g === 0 || g === '0') return '女'
|
||
return savedPrescription.value?.gender_desc || '—'
|
||
}
|
||
|
||
function slipAgeText(age: unknown) {
|
||
if (age === null || age === undefined || age === '') return '—'
|
||
return `${age}岁`
|
||
}
|
||
|
||
function formatAuditTime(ts: number | null | undefined) {
|
||
if (!ts) return ''
|
||
const d = new Date(ts * 1000)
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||
}
|
||
|
||
const mainHerbRows = computed(() =>
|
||
formData.herbs
|
||
.map((herb, index) => ({ herb, index }))
|
||
.filter(({ herb }) => normalizeFormulaType(herb.formula_type) === '主方')
|
||
)
|
||
|
||
const auxHerbRows = computed(() =>
|
||
formData.herbs
|
||
.map((herb, index) => ({ herb, index }))
|
||
.filter(({ herb }) => normalizeFormulaType(herb.formula_type) === '辅方')
|
||
)
|
||
|
||
const mainHerbCount = computed(() => mainHerbRows.value.length)
|
||
const auxHerbCount = computed(() => auxHerbRows.value.length)
|
||
|
||
/**
|
||
* 整张处方是否被锁定:只要任一药材来自「禁用修改」模板即锁定。
|
||
* 锁定后:不可增删改任意药材,仅可再次「从处方库导入」覆盖。
|
||
*/
|
||
const herbsLocked = computed(() => formData.herbs.some((h) => (h as Herb).locked === true))
|
||
|
||
const libraryDoctorId = computed(() => {
|
||
const uid = Number(userStore.userInfo?.id)
|
||
return uid > 0 ? uid : 0
|
||
})
|
||
|
||
// 处方库相关
|
||
const showLibraryDialog = ref(false)
|
||
const libraryLoading = ref(false)
|
||
const libraryList = ref<any[]>([])
|
||
const librarySearchName = ref('')
|
||
const librarySearchFormulaType = ref('')
|
||
const libraryImportMode = ref<'replace' | 'append'>('replace')
|
||
|
||
// 防止重复打开
|
||
const isOpening = ref(false)
|
||
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, formula_type: '主方' })
|
||
}
|
||
const skippedUnique = [...new Set(skippedNames.filter(Boolean))]
|
||
if (resolved.length === 0) {
|
||
feedback.msgWarning(
|
||
skippedUnique.length
|
||
? `药品库中无以下完全同名药材,未录入:${skippedUnique.join('、')}`
|
||
: '未能匹配到药品库中的药材'
|
||
)
|
||
return
|
||
}
|
||
if (pasteRecipeImportMode.value === 'replace') {
|
||
const auxKept = formData.herbs.filter((h) => normalizeFormulaType(h.formula_type) === '辅方')
|
||
formData.herbs = [...auxKept, ...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 SLIP_TITLE = '成都双流甄养堂互联网医院 处方笺'
|
||
|
||
const templateConfig = reactive({
|
||
stationName: '成都双流甄养堂互联网医院',
|
||
showDisclaimer: true,
|
||
disclaimer: ''
|
||
})
|
||
|
||
const formData = reactive<PrescriptionForm>({
|
||
diagnosis_id: 0,
|
||
appointment_id: 0,
|
||
prescription_name: '',
|
||
prescription_type: '浓缩水丸',
|
||
dosage_amount: 10,
|
||
dosage_unit: 'g',
|
||
dosage_bag_count: 1,
|
||
need_decoction: false,
|
||
bags_per_dose: 1,
|
||
patient_name: '',
|
||
gender: 0,
|
||
gender_desc: '女',
|
||
age: 0,
|
||
phone: '',
|
||
visit_no: '',
|
||
prescription_date: '',
|
||
pulse: '',
|
||
tongue: '',
|
||
tongue_image: '',
|
||
pulse_condition: '',
|
||
clinical_diagnosis: '',
|
||
case_record: null as any,
|
||
herbs: [],
|
||
dose_count: 1,
|
||
dose_unit: '剂',
|
||
usage_days: 7,
|
||
times_per_day: 2,
|
||
usage_instruction: '',
|
||
usage_time: '饭后',
|
||
usage_way: '温水送服',
|
||
dietary_taboo: [],
|
||
usage_notes: '',
|
||
amount: 0,
|
||
doctor_name: '',
|
||
doctor_signature: '',
|
||
is_shared: 0,
|
||
aux_usage: defaultAuxUsage('浓缩水丸')
|
||
})
|
||
|
||
const userStore = useUserStore()
|
||
const appStore = useAppStore()
|
||
const getImageUrl = (url: any) => {
|
||
const s = typeof url === 'string' ? url : (url?.url ?? url?.uri ?? url?.path ?? '')
|
||
return s ? (String(s).startsWith('http') ? s : appStore.getImageUrl(s)) : ''
|
||
}
|
||
|
||
const doseUnitOptions = ['剂', '丸', '袋', '盒', '瓶', '膏', '贴', '包', '片', '粒']
|
||
|
||
const diagnosisTypeOptions = ref<any[]>([])
|
||
const syndromeTypeOptions = ref<any[]>([])
|
||
const diabetesTypeOptions = ref<any[]>([])
|
||
const pastHistoryOptions = ref<any[]>([])
|
||
const appetiteOptions = ref<any[]>([])
|
||
const waterIntakeOptions = ref<any[]>([])
|
||
const dietConditionOptions = ref<any[]>([])
|
||
const weightChangeOptions = ref<any[]>([])
|
||
const bodyFeelingOptions = ref<any[]>([])
|
||
const sleepConditionOptions = ref<any[]>([])
|
||
const eyeConditionOptions = ref<any[]>([])
|
||
const headFeelingOptions = ref<any[]>([])
|
||
const sweatConditionOptions = ref<any[]>([])
|
||
const skinConditionOptions = ref<any[]>([])
|
||
const urineConditionOptions = ref<any[]>([])
|
||
const stoolConditionOptions = ref<any[]>([])
|
||
const kidneyConditionOptions = ref<any[]>([])
|
||
const fattyLiverDegreeOptions = ref<any[]>([])
|
||
|
||
const getDictLabel = (options: any[], value: string) => {
|
||
if (!value) return ''
|
||
const item = options.find((opt: any) => opt.value === value)
|
||
return item ? item.name : value
|
||
}
|
||
|
||
const loadDictOptions = async () => {
|
||
try {
|
||
const [
|
||
dt, st, diabetesType, pastHistory, appetite, waterIntake, dietCondition,
|
||
weightChange, bodyFeeling, sleepCondition, eyeCondition, headFeeling,
|
||
sweatCondition, skinCondition, urineCondition, stoolCondition, kidneyCondition,
|
||
fattyLiverDegree
|
||
] = await Promise.all([
|
||
getDictData({ type: 'diagnosis_type' }),
|
||
getDictData({ type: 'syndrome_type' }),
|
||
getDictData({ type: 'diabetes_type' }),
|
||
getDictData({ type: 'past_history' }),
|
||
getDictData({ type: 'appetite' }),
|
||
getDictData({ type: 'water_intake' }),
|
||
getDictData({ type: 'diet_condition' }),
|
||
getDictData({ type: 'weight_change' }),
|
||
getDictData({ type: 'body_feeling' }),
|
||
getDictData({ type: 'sleep_condition' }),
|
||
getDictData({ type: 'eye_condition' }),
|
||
getDictData({ type: 'head_feeling' }),
|
||
getDictData({ type: 'sweat_condition' }),
|
||
getDictData({ type: 'skin_condition' }),
|
||
getDictData({ type: 'urine_condition' }),
|
||
getDictData({ type: 'stool_condition' }),
|
||
getDictData({ type: 'kidney_condition' }),
|
||
getDictData({ type: 'fatty_liver_degree' })
|
||
])
|
||
diagnosisTypeOptions.value = dt?.diagnosis_type || []
|
||
syndromeTypeOptions.value = st?.syndrome_type || []
|
||
diabetesTypeOptions.value = diabetesType?.diabetes_type || []
|
||
pastHistoryOptions.value = pastHistory?.past_history || []
|
||
appetiteOptions.value = appetite?.appetite || []
|
||
waterIntakeOptions.value = waterIntake?.water_intake || []
|
||
dietConditionOptions.value = dietCondition?.diet_condition || []
|
||
weightChangeOptions.value = weightChange?.weight_change || []
|
||
bodyFeelingOptions.value = bodyFeeling?.body_feeling || []
|
||
sleepConditionOptions.value = sleepCondition?.sleep_condition || []
|
||
eyeConditionOptions.value = eyeCondition?.eye_condition || []
|
||
headFeelingOptions.value = headFeeling?.head_feeling || []
|
||
sweatConditionOptions.value = sweatCondition?.sweat_condition || []
|
||
skinConditionOptions.value = skinCondition?.skin_condition || []
|
||
urineConditionOptions.value = urineCondition?.urine_condition || []
|
||
stoolConditionOptions.value = stoolCondition?.stool_condition || []
|
||
kidneyConditionOptions.value = kidneyCondition?.kidney_condition || []
|
||
fattyLiverDegreeOptions.value = fattyLiverDegree?.fatty_liver_degree || []
|
||
} catch (_) {}
|
||
}
|
||
|
||
const getMultiLabels = (options: any[], values: string[] | string | undefined) => {
|
||
if (!values || (Array.isArray(values) && values.length === 0)) return '—'
|
||
const arr = Array.isArray(values) ? values : (typeof values === 'string' ? values.split(',') : [])
|
||
const names = arr.map(v => {
|
||
const item = options.find((o: any) => o.value === v)
|
||
return item ? item.name : v
|
||
}).filter(Boolean)
|
||
return names.length ? names.join('、') : '—'
|
||
}
|
||
|
||
const formatLocalDiagnosis = (val: string[] | string | undefined) => {
|
||
if (!val || (Array.isArray(val) && val.length === 0)) return '—'
|
||
const arr = Array.isArray(val) ? val : (typeof val === 'string' ? val.split(',') : [])
|
||
return arr.filter(Boolean).join('、') || '—'
|
||
}
|
||
|
||
const buildClinicalDiagnosis = (diagnosis: any) => {
|
||
if (diagnosis?.symptoms?.trim()) return diagnosis.symptoms.trim()
|
||
const dtLabel = getDictLabel(diagnosisTypeOptions.value, diagnosis?.diagnosis_type)
|
||
const stLabel = getDictLabel(syndromeTypeOptions.value, diagnosis?.syndrome_type)
|
||
const parts = [dtLabel, stLabel].filter(Boolean)
|
||
return parts.length ? parts.join(' ') : ''
|
||
}
|
||
|
||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||
// 防止重复打开
|
||
if (isOpening.value) {
|
||
console.warn('处方正在打开中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
isOpening.value = true
|
||
|
||
try {
|
||
if (options?.stationName) templateConfig.stationName = options.stationName
|
||
|
||
const diagnosis = data.diagnosis || data
|
||
const diagnosisId = data.diagnosis_id ?? diagnosis?.id ?? data.id
|
||
if (!diagnosisId) {
|
||
feedback.msgWarning('缺少诊单信息')
|
||
return
|
||
}
|
||
|
||
await loadDictOptions()
|
||
|
||
const appointmentId = Number(data.id ?? data.appointment_id ?? 0)
|
||
if (appointmentId) {
|
||
try {
|
||
|
||
const existing = await prescriptionGetByAppointment({ appointment_id: appointmentId })
|
||
console.log('-----------',existing)
|
||
if (existing?.id) {
|
||
const detail = await prescriptionDetail({ id: existing.id })
|
||
savedPrescription.value = detail
|
||
visible.value = true
|
||
return
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
// 获取详细诊单作为病历快照(每次开方保存一份)
|
||
let caseRecord = data.case_record && Object.keys(data.case_record).length > 0 ? data.case_record : null
|
||
if (!caseRecord && diagnosisId) {
|
||
try {
|
||
const res = await tcmDiagnosisDetail({ id: Number(diagnosisId) })
|
||
caseRecord = res && typeof res === 'object' ? res : null
|
||
} catch (e) {
|
||
console.warn('获取诊单详情失败:', e)
|
||
}
|
||
}
|
||
|
||
formData.diagnosis_id = Number(diagnosisId)
|
||
formData.appointment_id = appointmentId
|
||
formData.prescription_name = ''
|
||
formData.prescription_type = '浓缩水丸'
|
||
formData.dosage_unit = 'g'
|
||
formData.dosage_amount = 10
|
||
formData.dosage_bag_count = 1
|
||
formData.patient_name = data.patient_name || diagnosis.patient_name || ''
|
||
formData.gender = data.patient_gender ?? diagnosis.gender ?? 0
|
||
formData.gender_desc = formData.gender === 1 ? '男' : '女'
|
||
formData.age = data.patient_age ?? diagnosis.age ?? 0
|
||
formData.phone = data.patient_phone || diagnosis.phone || ''
|
||
formData.visit_no = data.id ? `1K${String(data.id).padStart(8, '0')}` : ''
|
||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||
formData.pulse = diagnosis.pulse || ''
|
||
formData.tongue = diagnosis.tongue_coating || diagnosis.tongue || ''
|
||
formData.tongue_image = ''
|
||
formData.pulse_condition = ''
|
||
formData.clinical_diagnosis = buildClinicalDiagnosis(diagnosis)
|
||
formData.case_record = caseRecord
|
||
formData.herbs = []
|
||
formData.dose_count = 1
|
||
formData.dose_unit = '剂'
|
||
formData.usage_days = 7
|
||
formData.usage_instruction = ''
|
||
formData.usage_time = '饭后'
|
||
formData.usage_way = '温水送服'
|
||
formData.dietary_taboo = []
|
||
formData.usage_notes = ''
|
||
formData.amount = 0
|
||
formData.doctor_name = userStore.userInfo?.name || ''
|
||
formData.doctor_signature = ''
|
||
formData.is_shared = 0
|
||
formData.need_decoction = false
|
||
formData.bags_per_dose = 1
|
||
formData.times_per_day = 2
|
||
Object.assign(formData.aux_usage, defaultAuxUsage('浓缩水丸'))
|
||
|
||
savedPrescription.value = null
|
||
visible.value = true
|
||
setTimeout(() => initSignatureCanvas(), 100)
|
||
} finally {
|
||
// 延迟重置,避免快速连续点击
|
||
setTimeout(() => {
|
||
isOpening.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
type OpenByIdOptions = {
|
||
/** 默认 internal;诊单列表查看传 user 展示处方联 */
|
||
slipType?: 'internal' | 'user'
|
||
/** 为 true 时不展示详细病历 A3 */
|
||
hideCaseRecord?: boolean
|
||
}
|
||
|
||
const openById = async (prescriptionId: number, options?: OpenByIdOptions) => {
|
||
// 防止重复打开
|
||
if (isOpening.value) {
|
||
console.warn('处方正在打开中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
isOpening.value = true
|
||
prescriptionTabType.value = options?.slipType ?? 'internal'
|
||
hideCaseRecordPreview.value = options?.hideCaseRecord ?? false
|
||
|
||
try {
|
||
const res = await prescriptionDetail({ id: prescriptionId })
|
||
savedPrescription.value = res
|
||
if (res?.case_record && Object.keys(res.case_record).length > 0) {
|
||
await loadDictOptions()
|
||
}
|
||
visible.value = true
|
||
} catch (e) {
|
||
feedback.msgError('加载处方失败')
|
||
} finally {
|
||
// 延迟重置,避免快速连续点击
|
||
setTimeout(() => {
|
||
isOpening.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
visible.value = false
|
||
savedPrescription.value = null
|
||
prescriptionTabType.value = 'internal'
|
||
hideCaseRecordPreview.value = false
|
||
}
|
||
|
||
/**
|
||
* 签名坐标:与 initSignatureCanvas 里 ctx.scale(dpr) 后的「逻辑像素」一致(0…SIGNATURE_CSS_W/H),
|
||
* 不能用 canvas.width/rect.width(会把 dpr 乘两次,笔画和鼠标严重错位)。
|
||
*/
|
||
const getSignatureLocalPoint = (e: PointerEvent) => {
|
||
const canvas = signatureCanvasRef.value
|
||
if (!canvas) return { x: 0, y: 0 }
|
||
const rect = canvas.getBoundingClientRect()
|
||
if (rect.width <= 0 || rect.height <= 0) return { x: 0, y: 0 }
|
||
const x = ((e.clientX - rect.left) / rect.width) * SIGNATURE_CSS_W
|
||
const y = ((e.clientY - rect.top) / rect.height) * SIGNATURE_CSS_H
|
||
return {
|
||
x: Math.max(0, Math.min(SIGNATURE_CSS_W, x)),
|
||
y: Math.max(0, Math.min(SIGNATURE_CSS_H, y))
|
||
}
|
||
}
|
||
|
||
const commitSignatureImage = () => {
|
||
const canvas = signatureCanvasRef.value
|
||
if (canvas) {
|
||
formData.doctor_signature = canvas.toDataURL('image/png')
|
||
}
|
||
}
|
||
|
||
const onSignaturePointerDown = (e: PointerEvent) => {
|
||
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||
const canvas = signatureCanvasRef.value
|
||
const ctx = canvas?.getContext('2d')
|
||
if (!canvas || !ctx) return
|
||
canvas.setPointerCapture(e.pointerId)
|
||
signatureActivePointerId.value = e.pointerId
|
||
const { x, y } = getSignatureLocalPoint(e)
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, y)
|
||
}
|
||
|
||
const onSignaturePointerMove = (e: PointerEvent) => {
|
||
if (signatureActivePointerId.value !== e.pointerId) return
|
||
const ctx = signatureCanvasRef.value?.getContext('2d')
|
||
if (!ctx) return
|
||
const { x, y } = getSignatureLocalPoint(e)
|
||
ctx.lineTo(x, y)
|
||
ctx.stroke()
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, y)
|
||
}
|
||
|
||
const onSignaturePointerUp = (e: PointerEvent) => {
|
||
if (signatureActivePointerId.value !== e.pointerId) return
|
||
signatureActivePointerId.value = null
|
||
const canvas = signatureCanvasRef.value
|
||
if (canvas) {
|
||
try {
|
||
canvas.releasePointerCapture(e.pointerId)
|
||
} catch {
|
||
/* 已释放或非当前捕获 */
|
||
}
|
||
}
|
||
commitSignatureImage()
|
||
}
|
||
|
||
const clearSignature = () => {
|
||
initSignatureCanvas()
|
||
formData.doctor_signature = ''
|
||
}
|
||
|
||
const initSignatureCanvas = () => {
|
||
const canvas = signatureCanvasRef.value
|
||
if (!canvas) return
|
||
const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, 2.5)
|
||
canvas.style.width = `${SIGNATURE_CSS_W}px`
|
||
canvas.style.height = `${SIGNATURE_CSS_H}px`
|
||
canvas.width = Math.round(SIGNATURE_CSS_W * dpr)
|
||
canvas.height = Math.round(SIGNATURE_CSS_H * dpr)
|
||
const ctx = canvas.getContext('2d')
|
||
if (!ctx) return
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||
ctx.scale(dpr, dpr)
|
||
ctx.fillStyle = '#fff'
|
||
ctx.fillRect(0, 0, SIGNATURE_CSS_W, SIGNATURE_CSS_H)
|
||
ctx.strokeStyle = '#333'
|
||
ctx.lineWidth = 2.5
|
||
ctx.lineCap = 'round'
|
||
ctx.lineJoin = 'round'
|
||
}
|
||
|
||
/** 药名去首尾空格后比较,避免「 黄芪 」与「黄芪」判成不同 */
|
||
function normalizeHerbName(name: string | undefined): string {
|
||
return (name ?? '').trim()
|
||
}
|
||
|
||
/** 当前行药名是否与其他行重复(非空且出现次数大于 1) */
|
||
function isDuplicateHerbName(index: number): boolean {
|
||
const herbs = formData.herbs
|
||
const key = normalizeHerbName(herbs[index]?.name)
|
||
if (!key) return false
|
||
let count = 0
|
||
for (let i = 0; i < herbs.length; i++) {
|
||
if (normalizeHerbName(herbs[i]?.name) === key) count++
|
||
}
|
||
return count > 1
|
||
}
|
||
|
||
function findDuplicateHerbNames(): string[] {
|
||
const map = new Map<string, number>()
|
||
for (const h of formData.herbs) {
|
||
const key = normalizeHerbName(h?.name)
|
||
if (!key) continue
|
||
map.set(key, (map.get(key) ?? 0) + 1)
|
||
}
|
||
return [...map.entries()].filter(([, c]) => c > 1).map(([name]) => name)
|
||
}
|
||
|
||
const handleAddHerb = (formulaType: FormulaType = '主方') => {
|
||
if (herbsLocked.value) {
|
||
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,请重新从处方库导入覆盖')
|
||
return
|
||
}
|
||
formData.herbs.push({ name: '', dosage: 6, formula_type: formulaType })
|
||
}
|
||
|
||
const handleRemoveHerb = (index: number) => {
|
||
if (herbsLocked.value) {
|
||
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,不可删除')
|
||
return
|
||
}
|
||
formData.herbs.splice(index, 1)
|
||
}
|
||
|
||
// 处方库相关方法
|
||
const loadLibraryList = async () => {
|
||
const doctorId = libraryDoctorId.value
|
||
if (!doctorId) {
|
||
feedback.msgWarning('无法确定开方医师,无法加载处方库')
|
||
libraryList.value = []
|
||
libraryTotal.value = 0
|
||
return
|
||
}
|
||
libraryLoading.value = true
|
||
try {
|
||
const res = await prescriptionLibraryLists({
|
||
page_no: libraryPage.value,
|
||
page_size: libraryPageSize.value,
|
||
prescription_name: librarySearchName.value,
|
||
formula_type: librarySearchFormulaType.value,
|
||
prescribing_creator_id: doctorId
|
||
})
|
||
libraryList.value = res.lists || []
|
||
libraryTotal.value = res.count || 0
|
||
} catch (error) {
|
||
console.error('加载处方库失败:', error)
|
||
feedback.msgError('加载处方库失败')
|
||
} finally {
|
||
libraryLoading.value = false
|
||
}
|
||
}
|
||
|
||
const searchLibrary = () => {
|
||
libraryPage.value = 1
|
||
loadLibraryList()
|
||
}
|
||
|
||
const openLibraryDialog = () => {
|
||
if (!libraryDoctorId.value) {
|
||
feedback.msgWarning('无法确定开方医师,无法从处方库导入')
|
||
return
|
||
}
|
||
showLibraryDialog.value = true
|
||
}
|
||
|
||
const handleSelectLibrary = (row: any) => {
|
||
// 点击行也可以导入
|
||
handleImportLibrary(row)
|
||
}
|
||
|
||
const handleImportLibrary = (row: any) => {
|
||
if (!row.herbs || row.herbs.length === 0) {
|
||
feedback.msgWarning('该处方没有药材信息')
|
||
return
|
||
}
|
||
|
||
const formulaType = normalizeFormulaType(row.formula_type)
|
||
// 「禁用修改」模板:导入的药材打上锁定标记 → 整张处方锁定(不可增删改,只能再次导入覆盖)
|
||
const isLockedTemplate = Number(row?.disable_edit) === 1
|
||
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => {
|
||
const herb = normalizeHerbRow(h)
|
||
herb.formula_type = formulaType
|
||
if (isLockedTemplate) {
|
||
herb.locked = true
|
||
} else {
|
||
delete herb.locked
|
||
}
|
||
return herb
|
||
})
|
||
if (libraryImportMode.value === 'replace') {
|
||
const kept = formData.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType)
|
||
formData.herbs = [...kept, ...imported]
|
||
} else {
|
||
formData.herbs.push(...imported)
|
||
}
|
||
|
||
const modeHint = libraryImportMode.value === 'append' ? '(已追加)' : ''
|
||
if (findDuplicateHerbNames().length) {
|
||
feedback.msgWarning(
|
||
`已导入处方「${row.prescription_name}」${modeHint},共${imported.length}味药材。存在重复药名,请合并剂量或删除多余行`
|
||
)
|
||
} else {
|
||
feedback.msgSuccess(`已导入处方「${row.prescription_name}」${modeHint},共${imported.length}味药材`)
|
||
}
|
||
showLibraryDialog.value = false
|
||
}
|
||
|
||
// 监听处方库弹窗打开,自动加载数据
|
||
watch(showLibraryDialog, (newVal) => {
|
||
if (newVal) {
|
||
if (!libraryDoctorId.value) {
|
||
feedback.msgWarning('无法确定开方医师,无法从处方库导入')
|
||
showLibraryDialog.value = false
|
||
return
|
||
}
|
||
librarySearchName.value = ''
|
||
librarySearchFormulaType.value = ''
|
||
libraryImportMode.value = 'replace'
|
||
libraryPage.value = 1
|
||
loadLibraryList()
|
||
}
|
||
})
|
||
|
||
// 监听处方类型变化,自动调整用量单位和默认值
|
||
watch(() => formData.prescription_type, (newType) => {
|
||
if (newType === '浓缩水丸') {
|
||
formData.dosage_unit = 'g'
|
||
formData.dosage_amount = 10
|
||
formData.dosage_bag_count = 1
|
||
formData.need_decoction = false
|
||
formData.bags_per_dose = 1
|
||
} else if (newType === '饮片') {
|
||
formData.dosage_unit = 'ml'
|
||
formData.dosage_amount = 50
|
||
formData.dosage_bag_count = 1
|
||
formData.bags_per_dose = 1
|
||
// need_decoction 保持用户选择
|
||
} else {
|
||
formData.dosage_unit = 'g'
|
||
formData.dosage_amount = undefined
|
||
formData.dosage_bag_count = 1
|
||
formData.need_decoction = false
|
||
formData.bags_per_dose = 1
|
||
}
|
||
Object.assign(formData.aux_usage, defaultAuxUsage(newType))
|
||
})
|
||
|
||
const handleSave = async () => {
|
||
if (!formData.clinical_diagnosis?.trim()) {
|
||
feedback.msgWarning('请输入临床诊断')
|
||
return
|
||
}
|
||
if (formData.herbs.length === 0) {
|
||
feedback.msgWarning('请至少添加一味中药')
|
||
return
|
||
}
|
||
const dupNames = findDuplicateHerbNames()
|
||
if (dupNames.length) {
|
||
feedback.msgWarning(`存在重复药名:${dupNames.join('、')},请合并剂量或删除多余行后再保存`)
|
||
return
|
||
}
|
||
for (const h of formData.herbs) {
|
||
if (!h.name?.trim()) {
|
||
feedback.msgWarning('请填写中药名称')
|
||
return
|
||
}
|
||
if (!h.dosage || h.dosage <= 0) {
|
||
feedback.msgWarning(`请填写「${h.name}」的剂量`)
|
||
return
|
||
}
|
||
}
|
||
|
||
if (!formData.doctor_signature?.trim()) {
|
||
feedback.msgWarning('请使用电子签名板手写医师签名')
|
||
return
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
const res = await prescriptionAdd({
|
||
diagnosis_id: formData.diagnosis_id,
|
||
appointment_id: formData.appointment_id,
|
||
prescription_name: formData.prescription_name,
|
||
prescription_type: formData.prescription_type,
|
||
patient_name: formData.patient_name,
|
||
gender: formData.gender,
|
||
age: formData.age,
|
||
phone: formData.phone,
|
||
visit_no: formData.visit_no,
|
||
prescription_date: formData.prescription_date,
|
||
pulse: formData.pulse,
|
||
tongue: formData.tongue,
|
||
tongue_image: formData.tongue_image,
|
||
pulse_condition: formData.pulse_condition,
|
||
clinical_diagnosis: formData.clinical_diagnosis,
|
||
case_record: formData.case_record,
|
||
herbs: formData.herbs.map((h) => normalizeHerbRow(h)),
|
||
aux_usage: auxHerbRows.value.length > 0 ? { ...formData.aux_usage } : null,
|
||
dose_count: formData.dose_count,
|
||
dose_unit: formData.dose_unit,
|
||
dosage_amount: formData.dosage_amount,
|
||
dosage_unit: formData.dosage_unit,
|
||
dosage_bag_count: formData.dosage_bag_count,
|
||
need_decoction: formData.need_decoction,
|
||
bags_per_dose: formData.bags_per_dose,
|
||
usage_days: formData.usage_days,
|
||
times_per_day: formData.times_per_day,
|
||
usage_instruction: formData.usage_instruction,
|
||
usage_time: formData.usage_time,
|
||
usage_way: formData.usage_way,
|
||
dietary_taboo: formData.dietary_taboo,
|
||
usage_notes: formData.usage_notes,
|
||
amount: formData.amount,
|
||
doctor_name: formData.doctor_name,
|
||
doctor_signature: formData.doctor_signature,
|
||
is_shared: formData.is_shared
|
||
})
|
||
const id = res?.id
|
||
if (id) {
|
||
const detail = await prescriptionDetail({ id })
|
||
savedPrescription.value = detail
|
||
emit('success')
|
||
} else {
|
||
feedback.msgError('保存失败')
|
||
}
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const formatVoidTime = (ts: number | null | undefined) => {
|
||
if (!ts) return '—'
|
||
const d = new Date(ts * 1000)
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
|
||
}
|
||
|
||
const handleVoid = async () => {
|
||
const id = savedPrescription.value?.id
|
||
if (!id) return
|
||
if (Number(savedPrescription.value?.has_prescription_order) === 1) {
|
||
feedback.msgWarning('该处方已存在业务订单,无法作废')
|
||
return
|
||
}
|
||
try {
|
||
await feedback.confirm('确定要作废该处方吗?作废后不可恢复。')
|
||
voiding.value = true
|
||
await prescriptionVoid({ id })
|
||
feedback.msgSuccess('作废成功')
|
||
const detail = await prescriptionDetail({ id })
|
||
savedPrescription.value = detail
|
||
emit('success')
|
||
} catch (e: any) {
|
||
if (e !== 'cancel' && e?.message !== 'cancel') feedback.msgError(e?.msg || '作废失败')
|
||
} finally {
|
||
voiding.value = false
|
||
}
|
||
}
|
||
|
||
const fetchCaseRecord = async (diagnosisId: number) => {
|
||
if (!diagnosisId) return null
|
||
try {
|
||
const res = await tcmDiagnosisDetail({ id: diagnosisId })
|
||
return res && typeof res === 'object' ? res : null
|
||
} catch (e) {
|
||
console.warn('获取诊单详情失败:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
const handleNewPrescription = async () => {
|
||
const saved = savedPrescription.value
|
||
if (saved) {
|
||
// 将患者信息、诊断信息填入表单,新建处方时保留
|
||
formData.diagnosis_id = saved.diagnosis_id ?? formData.diagnosis_id
|
||
formData.appointment_id = saved.appointment_id ?? formData.appointment_id
|
||
formData.prescription_name = ''
|
||
formData.prescription_type = saved.prescription_type || '浓缩水丸'
|
||
if (formData.prescription_type === '浓缩水丸') {
|
||
formData.dosage_unit = 'g'
|
||
formData.dosage_amount = 10
|
||
formData.dosage_bag_count = 1
|
||
} else if (formData.prescription_type === '饮片') {
|
||
formData.dosage_unit = 'ml'
|
||
formData.dosage_amount = 50
|
||
formData.dosage_bag_count = 1
|
||
} else {
|
||
formData.dosage_unit = 'g'
|
||
formData.dosage_amount = undefined
|
||
formData.dosage_bag_count = 1
|
||
}
|
||
formData.patient_name = saved.patient_name || ''
|
||
formData.gender = saved.gender ?? 0
|
||
formData.gender_desc = saved.gender === 1 ? '男' : '女'
|
||
formData.age = saved.age ?? 0
|
||
formData.phone = saved.phone || ''
|
||
formData.visit_no = saved.visit_no || ''
|
||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||
formData.pulse = saved.pulse || ''
|
||
formData.tongue = saved.tongue || ''
|
||
formData.tongue_image = ''
|
||
formData.pulse_condition = ''
|
||
formData.clinical_diagnosis = saved.clinical_diagnosis || ''
|
||
formData.dose_count = 1
|
||
formData.dose_unit = saved.dose_unit || '剂'
|
||
formData.usage_days = 7
|
||
formData.usage_instruction = ''
|
||
formData.usage_time = '饭后'
|
||
formData.usage_way = '温水送服'
|
||
formData.dietary_taboo = []
|
||
formData.usage_notes = ''
|
||
formData.amount = 0
|
||
formData.doctor_name = userStore.userInfo?.name || saved.doctor_name || ''
|
||
formData.doctor_signature = ''
|
||
formData.is_shared = 0
|
||
formData.herbs = []
|
||
Object.assign(formData.aux_usage, defaultAuxUsage(formData.prescription_type || '浓缩水丸'))
|
||
// 新建处方时重新获取病历(旧处方可能没有 case_record)
|
||
formData.case_record = saved.case_record && Object.keys(saved.case_record).length > 0
|
||
? saved.case_record
|
||
: await fetchCaseRecord(Number(formData.diagnosis_id))
|
||
setTimeout(() => initSignatureCanvas(), 100)
|
||
}
|
||
savedPrescription.value = null
|
||
}
|
||
|
||
const captureElement = async (): Promise<HTMLCanvasElement> => {
|
||
const el = prescriptionPrintRef.value
|
||
if (!el) throw new Error('未找到处方内容')
|
||
return html2canvas(el, {
|
||
scale: 2,
|
||
useCORS: true,
|
||
logging: false,
|
||
backgroundColor: '#ffffff'
|
||
})
|
||
}
|
||
|
||
/** 将元素内图片转为 base64,确保 html2canvas 能正确绘制 */
|
||
const preloadImagesToDataUrl = async (el: HTMLElement): Promise<void> => {
|
||
const imgs = el.querySelectorAll<HTMLImageElement>('img[src^="http"]')
|
||
const tasks = Array.from(imgs).map(async (img) => {
|
||
try {
|
||
const res = await fetch(img.src, { mode: 'cors' })
|
||
if (!res.ok) return
|
||
const blob = await res.blob()
|
||
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||
const r = new FileReader()
|
||
r.onload = () => resolve(r.result as string)
|
||
r.onerror = reject
|
||
r.readAsDataURL(blob)
|
||
})
|
||
img.src = dataUrl
|
||
} catch {
|
||
// 跨域失败时保留原 src,依赖 useCORS
|
||
}
|
||
})
|
||
await Promise.allSettled(tasks)
|
||
}
|
||
|
||
const captureCaseRecordElement = async (): Promise<HTMLCanvasElement | null> => {
|
||
const el = caseRecordPrintRef.value
|
||
if (!el) return null
|
||
el.scrollIntoView({ behavior: 'instant', block: 'nearest' })
|
||
await preloadImagesToDataUrl(el)
|
||
await new Promise((r) => setTimeout(r, 200))
|
||
return html2canvas(el, {
|
||
scale: 2,
|
||
useCORS: true,
|
||
logging: false,
|
||
backgroundColor: '#ffffff'
|
||
})
|
||
}
|
||
|
||
const handleDownloadImage = async () => {
|
||
try {
|
||
const canvas = await captureElement()
|
||
const link = document.createElement('a')
|
||
link.download = `处方_${savedPrescription.value?.patient_name || '未命名'}_${Date.now()}.png`
|
||
link.href = canvas.toDataURL('image/png')
|
||
link.click()
|
||
feedback.msgSuccess('图片已下载')
|
||
} catch (e) {
|
||
feedback.msgError('下载图片失败')
|
||
}
|
||
}
|
||
|
||
const handleDownloadPdf = async () => {
|
||
try {
|
||
const canvas = await captureElement()
|
||
const imgData = canvas.toDataURL('image/png')
|
||
const pdf = new jsPDF({
|
||
orientation: 'portrait',
|
||
unit: 'mm',
|
||
format: 'a5'
|
||
})
|
||
const pageW = pdf.internal.pageSize.getWidth()
|
||
const pageH = pdf.internal.pageSize.getHeight()
|
||
const scale = Math.min(pageW / canvas.width, pageH / canvas.height)
|
||
const imgW = canvas.width * scale
|
||
const imgH = canvas.height * scale
|
||
pdf.addImage(imgData, 'PNG', 0, 0, imgW, imgH)
|
||
|
||
// 若有详细病历,单独一页A3
|
||
const caseCanvas = await captureCaseRecordElement()
|
||
if (caseCanvas) {
|
||
pdf.addPage('a3', 'p')
|
||
const a3W = 297
|
||
const a3H = 420
|
||
const caseScale = Math.min(a3W / caseCanvas.width, a3H / caseCanvas.height)
|
||
const caseImgW = caseCanvas.width * caseScale
|
||
const caseImgH = caseCanvas.height * caseScale
|
||
pdf.addImage(caseCanvas.toDataURL('image/png'), 'PNG', 0, 0, caseImgW, caseImgH)
|
||
}
|
||
|
||
pdf.save(`处方_${savedPrescription.value?.patient_name || '未命名'}_${Date.now()}.pdf`)
|
||
feedback.msgSuccess('PDF已下载')
|
||
} catch (e) {
|
||
feedback.msgError('下载PDF失败')
|
||
}
|
||
}
|
||
|
||
defineExpose({
|
||
open,
|
||
openById
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.prescription-form {
|
||
.info-section {
|
||
margin-bottom: 20px;
|
||
padding: 16px;
|
||
background: #f5f7fa;
|
||
border-radius: 6px;
|
||
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
color: #303133;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
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__lock-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.herb-formula-block__head {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.usage-formula-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin: 8px 0 4px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.usage-formula-title--aux {
|
||
color: #e6a23c;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.herbs-editor-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.herb-editor-card {
|
||
min-width: 0;
|
||
padding: 10px;
|
||
background: #fff;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.herb-editor-card--dup {
|
||
border-color: var(--el-color-warning);
|
||
background: var(--el-color-warning-light-9);
|
||
}
|
||
|
||
.herb-editor-card__title {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.herb-editor-card__select {
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.herb-editor-card__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.herb-editor-card__dose {
|
||
flex: 1;
|
||
min-width: 88px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.signature-pad-wrap {
|
||
width: 100%;
|
||
max-width: 600px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
.signature-canvas {
|
||
display: block;
|
||
cursor: crosshair;
|
||
touch-action: none;
|
||
}
|
||
|
||
.signature-actions {
|
||
padding: 8px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
|
||
/* 处方笺(A4 药房联)— 与 consumer/prescription/index.vue 查看样式一致 */
|
||
.rx-wrap {
|
||
min-height: 160px;
|
||
background: #f5f6f8;
|
||
padding: 12px;
|
||
margin: -20px;
|
||
}
|
||
|
||
.rx-slip-tabs {
|
||
margin-bottom: 0;
|
||
z-index: 1;
|
||
position: relative;
|
||
background: #fff;
|
||
padding: 0 4px;
|
||
border-radius: 6px 6px 0 0;
|
||
}
|
||
|
||
.rx-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: #fff;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 10px;
|
||
|
||
.flex-1 {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.rx-status {
|
||
margin-bottom: 10px;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.rx-status-danger {
|
||
background: #fef0f0;
|
||
color: #b91c1c;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
.rx-paper {
|
||
width: 210mm;
|
||
min-height: 297mm;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
box-sizing: border-box;
|
||
padding: 8mm 10mm;
|
||
color: #1f1f1f;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
font-family: 'Microsoft YaHei', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
box-shadow: 0 0 0 1px #d6d6d6, 0 6px 24px rgba(0, 0, 0, 0.06);
|
||
overflow: visible;
|
||
}
|
||
|
||
.rx-title {
|
||
text-align: center;
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
margin: 10px 0 35px;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
|
||
.rx-notice {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f3f4f6;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 6px 10px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
color: #1f1f1f;
|
||
margin-bottom: 6px;
|
||
|
||
.rx-notice-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.rx-notice-meta {
|
||
display: inline-flex;
|
||
gap: 24px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.rx-info {
|
||
border: 1px solid #c8c8c8;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.rx-info-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
border-bottom: 1px solid #c8c8c8;
|
||
}
|
||
|
||
.rx-info-row-full {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.rx-info-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
font-size: 13px;
|
||
border-right: 1px solid #c8c8c8;
|
||
min-height: 32px;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.rx-key {
|
||
color: #1f1f1f;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.rx-val {
|
||
flex: 1;
|
||
min-width: 0;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.rx-rp {
|
||
position: relative;
|
||
border-left: 1px solid #c8c8c8;
|
||
border-right: 1px solid #c8c8c8;
|
||
padding: 8px 10px 16px;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
}
|
||
|
||
.rx-watermark {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 88px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.06);
|
||
transform: rotate(-22deg);
|
||
letter-spacing: 0.18em;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.rx-rp-head {
|
||
display: grid;
|
||
grid-template-columns: 36px 1fr;
|
||
align-items: end;
|
||
column-gap: 8px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid #d4d4d4;
|
||
margin-bottom: 6px;
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rx-rp-label {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #1f1f1f;
|
||
line-height: 1;
|
||
}
|
||
|
||
.rx-rp-cols-head {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
column-gap: 24px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rx-rp-col-head {
|
||
display: grid;
|
||
grid-template-columns: 1fr 64px;
|
||
column-gap: 8px;
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
min-width: 0;
|
||
|
||
& > span:last-child {
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.rx-herbs {
|
||
padding-left: 44px;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
column-gap: 24px;
|
||
row-gap: 6px;
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rx-herb-section-label {
|
||
grid-column: 1 / -1;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #409eff;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.rx-herb-section-label--aux {
|
||
color: #e6a23c;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.rx-herb-cell {
|
||
display: grid;
|
||
grid-template-columns: 1fr 64px;
|
||
column-gap: 8px;
|
||
font-size: 13px;
|
||
line-height: 1.85;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rx-herb-name {
|
||
color: #1f1f1f;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rx-herb-total {
|
||
color: #1f1f1f;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rx-text {
|
||
border: 1px solid #c8c8c8;
|
||
border-top: none;
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
line-height: 1.85;
|
||
|
||
p {
|
||
margin: 0;
|
||
}
|
||
|
||
.rx-text-warn {
|
||
color: #d72424;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.rx-bottom {
|
||
border: 1px solid #c8c8c8;
|
||
border-top: none;
|
||
}
|
||
|
||
.rx-bot-row {
|
||
display: grid;
|
||
border-bottom: 1px solid #c8c8c8;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.rx-bot-row-1 {
|
||
grid-template-columns: 1.4fr 0.6fr 0.6fr 0.6fr;
|
||
min-height: 70px;
|
||
}
|
||
|
||
.rx-bot-row-1--user {
|
||
grid-template-columns: 1.4fr 0.6fr;
|
||
}
|
||
|
||
.rx-hospital-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: flex-start;
|
||
gap: 2px;
|
||
padding: 10px 12px;
|
||
min-height: 52px;
|
||
text-align: left;
|
||
}
|
||
|
||
.rx-hospital-line {
|
||
font-size: 12px;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.rx-bot-cell {
|
||
border-right: 1px solid #c8c8c8;
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
color: #1f1f1f;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
min-height: 32px;
|
||
box-sizing: border-box;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
}
|
||
|
||
.rx-bot-stack {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.rx-bot-label {
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.rx-bot-doctor {
|
||
.rx-bot-doctor-body {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.rx-bot-doctor-name {
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
}
|
||
|
||
.rx-bot-sign-img {
|
||
max-height: 40px;
|
||
max-width: 110px;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.rx-bot-meta {
|
||
flex-wrap: nowrap;
|
||
|
||
.rx-bot-meta-key {
|
||
color: #1f1f1f;
|
||
margin-right: 6px;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.rx-bot-meta-val {
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
min-width: 0;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
.rx-audit {
|
||
margin-top: 12px;
|
||
padding: 8px 12px;
|
||
background: #fafafa;
|
||
border: 1px dashed #d4d4d4;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.prescription-preview-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 详细病历 - 单独A3纸张 */
|
||
.case-record-a3 {
|
||
width: 297mm;
|
||
min-height: 420mm;
|
||
padding: 20px;
|
||
background: #fff;
|
||
font-size: 13px;
|
||
color: #333;
|
||
margin: 0 auto;
|
||
box-sizing: border-box;
|
||
border: 1px solid #dcdfe6;
|
||
page-break-before: always;
|
||
|
||
.case-record-a3-inner {
|
||
width: 100%;
|
||
}
|
||
|
||
.case-record-a3-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
margin: 0 0 20px 0;
|
||
padding-bottom: 12px;
|
||
border-bottom: 2px solid #333;
|
||
}
|
||
|
||
.case-record-a3-body {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.cr-section {
|
||
margin-bottom: 16px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #eee;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.cr-section-title {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
margin-bottom: 10px;
|
||
color: #303133;
|
||
}
|
||
|
||
.cr-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px 16px;
|
||
}
|
||
|
||
.cr-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
line-height: 1.6;
|
||
|
||
&.cr-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.cr-label {
|
||
flex-shrink: 0;
|
||
color: #909399;
|
||
min-width: 90px;
|
||
}
|
||
|
||
.cr-images {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.cr-img {
|
||
width: 160px;
|
||
height: 160px;
|
||
object-fit: cover;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
</style>
|