This commit is contained in:
Your Name
2026-08-12 14:48:57 +08:00
parent bc9ad1d3cd
commit 6d9fe1bbf8
403 changed files with 1790 additions and 1573 deletions
@@ -0,0 +1,538 @@
<template>
<div ref="paperRef" class="rx-paper">
<div class="rx-notice">
<span class="rx-notice-text">
服药前请核对姓名电话医生等信息以及服法医嘱等要点
</span>
<span class="rx-notice-meta">
<span>日期{{ dateText }}</span>
<span>编号{{ serialText }}</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">{{ data?.patient_name || '—' }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">性别</span>
<span class="rx-val">{{ genderLabel }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">年龄</span>
<span class="rx-val">{{ ageText }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">电话</span>
<span class="rx-val">{{ data?.phone || data?.recipient_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">{{ recipientText }}</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">{{ data?.clinical_diagnosis || '—' }}</span>
</div>
</div>
</div>
<div class="rx-rp">
<div class="rx-watermark">药房联</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>总量</span>
</div>
<div class="rx-rp-col-head">
<span>用药 (单剂)</span>
<span>总量</span>
</div>
</div>
</div>
<div class="rx-herbs">
<template v-if="mainHerbs.length">
<div class="rx-herb-section-label">主方</div>
<div
v-for="(h, i) in mainHerbs"
:key="'main-' + i"
class="rx-herb-cell"
>
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }})</span>
<span class="rx-herb-total">{{ herbTotal(h.dosage) }}</span>
</div>
</template>
<template v-if="auxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
<div
v-for="(h, i) in auxHerbs"
:key="'aux-' + i"
class="rx-herb-cell"
>
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }})</span>
<span class="rx-herb-total">{{ herbTotal(h.dosage) }}</span>
</div>
</template>
</div>
</div>
<div class="rx-text">
<p>主方服法{{ usageText }}</p>
<p v-if="auxUsageText">辅方服法{{ auxUsageText }}</p>
<p v-if="adviceText">医嘱{{ adviceText }}</p>
<p v-if="dietaryText">忌口{{ dietaryText }}</p>
<p v-if="remarkText">备注{{ remarkText }}</p>
<p v-if="pharmacyRemarkText" class="rx-text-warn">
药房备注{{ pharmacyRemarkText }}
</p>
<p v-if="outPelletText" class="rx-text-warn">
出丸{{ outPelletText }}
</p>
</div>
<div class="rx-bottom">
<div class="rx-bot-row rx-bot-row-1">
<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="data?.doctor_signature"
:src="data.doctor_signature"
alt="医师签名"
class="rx-bot-sign-img"
/>
<div v-if="!data?.doctor_signature" class="rx-bot-doctor-name">
{{ data?.doctor_name || '' }}
</div>
</div>
</div>
<div class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">类型:</span>
<span class="rx-bot-meta-val">{{ typeText }}</span>
</div>
<div class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">天数:</span>
<span class="rx-bot-meta-val">{{ data?.dose_count || '—' }}</span>
</div>
<div class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">单剂量:</span>
<span class="rx-bot-meta-val">{{ perDoseAmount }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import {
getAuxHerbs,
getMainHerbs,
rxAdviceText,
rxAuxUsageText,
rxDateText,
rxHerbTotal,
rxOutPelletText,
rxPerDoseAmount,
rxPharmacyRemarkText,
rxRecipientText,
rxRemarkText,
rxSerialText,
rxTypeText,
rxUsageText,
slipAgeText,
slipDietaryText,
slipGenderLabel
} from './utils'
defineOptions({ name: 'PrescriptionSlip' })
const props = defineProps<{
/** 处方详情 / 列表行(字段与 tcm.prescription/detail 对齐) */
data: Record<string, any> | null | undefined
}>()
const paperRef = ref<HTMLElement | null>(null)
const dateText = computed(() => rxDateText(props.data))
const serialText = computed(() => rxSerialText(props.data))
const recipientText = computed(() => rxRecipientText(props.data))
const genderLabel = computed(() => slipGenderLabel(props.data?.gender))
const ageText = computed(() => slipAgeText(props.data?.age))
const mainHerbs = computed(() => getMainHerbs(props.data))
const auxHerbs = computed(() => getAuxHerbs(props.data))
const usageText = computed(() => rxUsageText(props.data))
const auxUsageText = computed(() => rxAuxUsageText(props.data))
const adviceText = computed(() => rxAdviceText(props.data))
const dietaryText = computed(() => slipDietaryText(props.data))
const remarkText = computed(() => rxRemarkText(props.data))
const pharmacyRemarkText = computed(() => rxPharmacyRemarkText(props.data))
const outPelletText = computed(() => rxOutPelletText(props.data))
const typeText = computed(() => rxTypeText(props.data))
const perDoseAmount = computed(() => rxPerDoseAmount(props.data))
function herbTotal(dosage: number | string) {
return rxHerbTotal(props.data, dosage)
}
defineExpose({
/** 供打印 / html2canvas 使用的纸张根节点 */
paperEl: paperRef,
/** 兼容旧用法:直接拿到 DOM */
$el: paperRef
})
</script>
<style lang="scss" scoped>
/* ============================================================
* 处方笺(A4 药房联)样式
* 严格按 A4210mm × 297mmpadding 8mm 10mm
* 唯一视觉基准:consumer/prescription/index.vue
* ============================================================ */
.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-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 0.6fr 1.7fr 0.95fr 1.25fr;
min-height: 70px;
}
.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;
}
}
</style>
<!-- 全局打印样式body * 选择器不能放进 scoped -->
<style>
@media print {
body * {
visibility: hidden !important;
}
.rx-paper,
.rx-paper * {
visibility: visible !important;
}
.rx-paper {
position: fixed !important;
left: 0 !important;
top: 0 !important;
width: 210mm !important;
height: 297mm !important;
margin: 0 !important;
padding: 8mm 10mm !important;
box-shadow: none !important;
page-break-after: always;
}
@page {
size: A4;
margin: 0;
}
}
</style>
@@ -0,0 +1,291 @@
/**
* 处方笺(A4 药房联)字段计算
* 唯一视觉基准:consumer/prescription/index.vue 查看态
*/
export type FormulaType = '主方' | '辅方'
export type SlipHerb = {
medicine_id?: number
name: string
dosage: number
formula_type?: FormulaType | string
}
export type AuxUsageForm = {
dosage_amount: number
dosage_bag_count: number
need_decoction: boolean
bags_per_dose: number
times_per_day: number
usage_days: number
prescription_name?: string
}
export function formatNum(n: number): string {
if (!isFinite(n)) return '0'
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '')
}
export function slipGenderLabel(g: unknown) {
if (g === 1 || g === '1') return '男'
if (g === 0 || g === '0') return '女'
return '—'
}
export function slipAgeText(age: unknown) {
if (age === null || age === undefined || age === '') return '—'
return `${age}`
}
export function normalizeFormulaType(v: unknown): FormulaType {
return v === '辅方' ? '辅方' : '主方'
}
function padN(n: number) {
return String(n).padStart(2, '0')
}
export 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
}
}
export 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,
prescription_name: String(o.prescription_name ?? '').trim()
}
}
export 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(', ')
}
export function getSlipHerbs(data: Record<string, any> | null | undefined): SlipHerb[] {
const h = data?.herbs
return Array.isArray(h) ? (h as SlipHerb[]) : []
}
export function getMainHerbs(data: Record<string, any> | null | undefined): SlipHerb[] {
return getSlipHerbs(data).filter((h) => normalizeFormulaType(h.formula_type) === '主方')
}
export function getAuxHerbs(data: Record<string, any> | null | undefined): SlipHerb[] {
return getSlipHerbs(data).filter((h) => normalizeFormulaType(h.formula_type) === '辅方')
}
export function getDoseCount(data: Record<string, any> | null | undefined): number {
const n = Number(data?.dose_count)
return Number.isFinite(n) && n > 0 ? n : 1
}
export function rxDateText(data: Record<string, any> | null | undefined): string {
if (!data) return '—'
const ts = data.create_time ?? data.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()}-${padN(d.getMonth() + 1)}-${padN(d.getDate())} ${padN(d.getHours())}:${padN(d.getMinutes())}`
}
}
return data.prescription_date || '—'
}
export function rxSerialText(data: Record<string, any> | null | undefined): string {
if (!data) return '—'
return (
data.order_no ||
data.serial_no ||
data.serial_number ||
data.no ||
data.prescription_no ||
data.sn ||
data.visit_no ||
(data.id ? `G${data.id}` : '—')
)
}
export function rxRecipientText(data: Record<string, any> | null | undefined): string {
if (!data) return '—'
if (data.recipient_text) return data.recipient_text
const parts: string[] = []
if (data.recipient_name || data.patient_name) parts.push(data.recipient_name || data.patient_name)
if (data.recipient_phone || data.phone) parts.push(data.recipient_phone || data.phone)
let region = data.region
if (Array.isArray(region)) region = region.filter(Boolean).join('')
if (!region) {
region = [data.shipping_province, data.shipping_city, data.shipping_district]
.filter(Boolean)
.join('')
}
if (region) parts.push(String(region))
if (data.shipping_address) parts.push(String(data.shipping_address))
else if (data.address) parts.push(String(data.address))
return parts.length ? parts.join('') : '—'
}
export function rxPerDoseAmount(data: Record<string, any> | null | undefined): string {
const list = getSlipHerbs(data)
const sum = list.reduce((acc, h) => acc + (Number(h?.dosage) || 0), 0)
return formatNum(sum)
}
export function rxHerbTotal(data: Record<string, any> | null | undefined, dosage: number | string): string {
const d = Number(dosage) || 0
return formatNum(d * getDoseCount(data))
}
export function rxTypeText(data: Record<string, any> | null | undefined): string {
if (!data) return '—'
const t = data.prescription_type || '浓缩水丸'
return /丸|散|膏|片/.test(t) ? `浓缩丸-${t}` : t
}
export function rxUsageText(data: Record<string, any> | null | undefined): string {
if (!data) return '—'
if (data.usage_text) return data.usage_text
return buildUsageSegmentText(data)
}
export function rxAuxUsageText(data: Record<string, any> | null | undefined): string {
if (!data || !getAuxHerbs(data).length) return ''
const aux = normalizeAuxUsageForm(data.aux_usage, data.prescription_type || '浓缩水丸')
return buildUsageSegmentText(
{
prescription_type: data.prescription_type,
dosage_amount: aux.dosage_amount,
dosage_unit: data.dosage_unit,
dosage_bag_count: aux.dosage_bag_count,
times_per_day: aux.times_per_day,
usage_way: data.usage_way,
usage_time: data.usage_time
},
data.usage_way,
data.usage_time
)
}
export function rxAdviceText(data: Record<string, any> | null | undefined): string {
if (!data) return ''
return data.medical_advice || data.doctor_advice || ''
}
export function slipDietaryText(data: Record<string, any> | null | undefined): string {
const d = data?.dietary_taboo
if (Array.isArray(d)) return d.filter(Boolean).join('、')
if (typeof d === 'string' && d.trim()) return d.trim()
return ''
}
export function rxRemarkText(data: Record<string, any> | null | undefined): string {
if (!data) return ''
const parts: string[] = []
parts.push(`${getSlipHerbs(data).length}味药`)
if (data.usage_notes) parts.push(String(data.usage_notes))
if (data.remark) parts.push(String(data.remark))
return parts.join(' ')
}
export function rxPharmacyRemarkText(data: Record<string, any> | null | undefined): string {
if (!data) return ''
return data.pharmacy_remark || data.pharmacy_note || ''
}
function computeOutPellet(data: Record<string, any>): string {
if ((data.prescription_type || '浓缩水丸') === '浓缩水丸') {
const days = Number(data.medication_days ?? data.usage_days ?? data.dose_count)
const times = Number(data.times_per_day)
const doseG = Number(data.dosage_amount)
const bags = Number(data.dosage_bag_count) > 0 ? Number(data.dosage_bag_count) : 1
if (
Number.isFinite(days) &&
Number.isFinite(times) &&
Number.isFinite(doseG) &&
days > 0 &&
times > 0 &&
doseG > 0
) {
return formatNum(days * times * doseG * bags)
}
}
const explicit = data.out_pellet || data.out_pellet_text || data.total_weight
if (explicit && Number(explicit) > 0) return formatNum(Number(explicit))
const list = getSlipHerbs(data)
if (!list.length) return ''
const perDose = list.reduce((acc, h) => acc + (Number(h?.dosage) || 0), 0)
return formatNum(perDose * getDoseCount(data))
}
export function rxOutPelletText(data: Record<string, any> | null | undefined): string {
if (!data) return ''
const out = computeOutPellet(data)
return out ? `${out}` : ''
}
+9 -194
View File
@@ -534,13 +534,6 @@
</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>
@@ -565,190 +558,11 @@
</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>
<PrescriptionSlip
v-if="savedPrescription"
ref="rxSlipRef"
:data="savedPrescription"
/>
<!-- 详细病历 - 单独A3纸张 -->
<div v-if="savedPrescription.case_record && !hideCaseRecordPreview" ref="caseRecordPrintRef" class="case-record-a3">
@@ -1045,6 +859,7 @@ import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-displ
import { prescriptionAdd, prescriptionDetail, prescriptionGetByAppointment, prescriptionVoid, tcmDiagnosisDetail, prescriptionLibraryLists } from '@/api/tcm'
import { medicineLists } from '@/api/medicine'
import { getDictData } from '@/api/app'
import PrescriptionSlip from '@/components/prescription-slip/index.vue'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import useUserStore from '@/stores/modules/user'
@@ -1219,7 +1034,7 @@ const visible = ref(false)
const formRef = ref()
const saving = ref(false)
const voiding = ref(false)
const prescriptionPrintRef = ref<HTMLElement>()
const rxSlipRef = ref<InstanceType<typeof PrescriptionSlip> | null>(null)
const caseRecordPrintRef = ref<HTMLElement>()
const signatureCanvasRef = ref<HTMLCanvasElement>()
/** 当前一笔的 pointerId(配合 setPointerCapture,移出画布仍跟手) */
@@ -1229,7 +1044,7 @@ const SIGNATURE_CSS_W = 560
const SIGNATURE_CSS_H = 168
const savedPrescription = ref<any>(null)
/** 预览联次:药房联 / 处方联(与消费者处方查看一致) */
const prescriptionTabType = ref<'internal' | 'user'>('internal')
const prescriptionTabType = ref<'internal' | 'user'>('internal') // 保留兼容 openPreview options.slipType
/** 诊单列表「查看」仅展示处方联,不展示详细病历 A3 */
const hideCaseRecordPreview = ref(false)
@@ -2397,7 +2212,7 @@ const handleNewPrescription = async () => {
}
const captureElement = async (): Promise<HTMLCanvasElement> => {
const el = prescriptionPrintRef.value
const el = (rxSlipRef.value as { paperEl?: HTMLElement | null } | null)?.paperEl ?? null
if (!el) throw new Error('未找到处方内容')
return html2canvas(el, {
scale: 2,
+11 -534
View File
@@ -342,144 +342,8 @@
<div v-if="slipView.audit_remark" class="mt-1">驳回意见{{ slipView.audit_remark }}</div>
</div>
<!-- A4 处方纸 -->
<div v-if="slipView" ref="rxPaperRef" class="rx-paper">
<!-- 顶部告知栏 -->
<div class="rx-notice">
<span class="rx-notice-text">
服药前请核对姓名电话医生等信息以及服法医嘱等要点
</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">{{ slipView.patient_name || '—' }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">性别</span>
<span class="rx-val">{{ slipGenderLabel(slipView.gender) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">年龄</span>
<span class="rx-val">{{ slipAgeText(slipView.age) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">电话</span>
<span class="rx-val">{{ slipView.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">{{ rxRecipientText }}</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">{{ slipView.clinical_diagnosis || '—' }}</span>
</div>
</div>
</div>
<!-- Rp 区域带水印 -->
<div class="rx-rp">
<div class="rx-watermark">药房联</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>总量</span>
</div>
<div class="rx-rp-col-head">
<span>用药 (单剂)</span>
<span>总量</span>
</div>
</div>
</div>
<div class="rx-herbs">
<template v-if="slipMainHerbs.length">
<div class="rx-herb-section-label">主方</div>
<div
v-for="(h, i) in slipMainHerbs"
: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="slipAuxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
<div
v-for="(h, i) in slipAuxHerbs"
: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>
</div>
</div>
<!-- 服法 / 医嘱 / 备注 / 药房备注 / 出丸 -->
<div class="rx-text">
<p>主方服法{{ rxUsageText }}</p>
<p v-if="rxAuxUsageText">辅方服法{{ rxAuxUsageText }}</p>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
</p>
<p v-if="rxOutPelletText" class="rx-text-warn">
出丸{{ rxOutPelletText }}
</p>
</div>
<!-- 底部表医师/审方/配药/复核/发药 + 类型/剂数/单剂量 -->
<div class="rx-bottom">
<div class="rx-bot-row rx-bot-row-1">
<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="slipView.doctor_signature"
:src="slipView.doctor_signature"
alt="医师签名"
class="rx-bot-sign-img"
/>
<div v-if="!slipView.doctor_signature" class="rx-bot-doctor-name">
{{ slipView.doctor_name || '' }}
</div>
</div>
</div>
<div 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">{{ slipView.dose_count || '—' }}</span>
</div>
<div 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>
</div>
<!-- A4 处方纸统一组件与全端药房联一比一 -->
<PrescriptionSlip v-if="slipView" ref="rxSlipRef" :data="slipView" />
<!-- 审核痕迹不进入打印区 -->
<div
@@ -1719,6 +1583,7 @@ import { hasPermission } from '@/utils/perm'
import type { FormInstance, FormRules } from 'element-plus'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
import PrescriptionSlip from '@/components/prescription-slip/index.vue'
import { Search, Download, Printer, EditPen } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref, watch, nextTick, defineAsyncComponent } from 'vue'
import { useRouter } from 'vue-router'
@@ -2542,9 +2407,14 @@ const slipDietaryText = computed(() => {
/* ============================================================
* A4 处方笺(药房联)相关计算
* ============================================================ */
const rxPaperRef = ref<HTMLElement | null>(null)
const rxSlipRef = ref<InstanceType<typeof PrescriptionSlip> | null>(null)
const downloadingSlip = ref(false)
function getRxPaperEl(): HTMLElement | null {
const exposed = rxSlipRef.value as { paperEl?: HTMLElement | null } | null
return exposed?.paperEl ?? null
}
const padN = (n: number) => String(n).padStart(2, '0')
const rxDateText = computed(() => {
@@ -2752,7 +2622,7 @@ function handlePrintSlip() {
/* === 下载 PDFA4 === */
async function handleDownloadSlip() {
const node = rxPaperRef.value
const node = getRxPaperEl()
if (!node) {
feedback.msgWarning('处方笺尚未渲染完成')
return
@@ -4155,10 +4025,7 @@ onMounted(async () => {
overflow-y: auto;
}
/* ============================================================
* 处方笺A4 药房联样式
* 严格按 A4210mm × 297mmpadding 12mm
* ============================================================ */
/* 查看区外壳A4 纸张样式见 @/components/prescription-slip */
.rx-wrap {
min-height: 160px;
background: #f5f6f8;
@@ -4193,370 +4060,6 @@ onMounted(async () => {
border: 1px solid #fecaca;
}
/* === A4 === */
.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);
/* 防止 html2canvas 把内部表格裁切 */
overflow: visible;
}
/* === 顶部告知 === */
.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;
}
}
/* === Rp 区域 + 水印 === */
.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;
/* 不使用 inset 简写html2canvas inset 解析不稳定
* 会导致水印容器尺寸异常进而影响内层网格对齐 */
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;
}
/* 注意列内 1fr 64px .rx-herb-cell 完全一致
* 配合 .rx-herbs .rx-rp-cols-head 同样的左侧 44px 缩进
* 即可保证用药 (单剂)与药材名总量与总量数字精确对齐 */
.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 {
/* 与 .rx-rp-head 中 Rp. 列(36px) + column-gap(8px) 等宽缩进,
* 这样下方两列的左右边界与表头两列完全对齐。 */
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;
}
.herb-formula-block__head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 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;
}
.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 {
/* 类型/剂数/单剂量 三个 meta 列适当加宽,
* 避免"浓缩丸-浓缩水丸"被强制折行后把整行挤错位 */
grid-template-columns: 1.4fr 0.6fr 0.6fr 0.6fr 0.6fr 1.7fr 0.95fr 1.25fr;
min-height: 70px;
}
.rx-bot-row-2 {
grid-template-columns: repeat(6, 1fr);
}
/* 默认:单行内容左对齐、上下居中(适配 类型/剂数/单剂量/费用 等格子)。
* 需要"标题置顶 + 签名留白"的格子(医师/审方/配药/复核/发药)请加 .rx-bot-stack。
* 这样无需任何 !important 反向覆盖,html2canvas/原生渲染表现一致。 */
.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-fee {
font-size: 13px;
line-height: 1.4;
padding: 8px 10px;
.rx-fee-key {
color: #1f1f1f;
margin-right: 6px;
white-space: nowrap;
}
.rx-fee-val {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
}
.rx-fee-total {
font-weight: 700;
background: #fafafa;
.rx-fee-val { font-weight: 700; }
}
/* === 审核痕迹不打印 === */
.rx-audit {
margin-top: 12px;
@@ -4569,8 +4072,6 @@ onMounted(async () => {
border-radius: 4px;
}
/* 打印控制规则放到非 scoped 的全局 style 中,参见底部 <style> */
/* 创建业务订单弹窗宽松留白默认字号卡片化信息区 */
.create-order-dialog {
:deep(.el-dialog__header) {
@@ -4865,28 +4366,4 @@ onMounted(async () => {
}
</style>
<!-- 全局打印样式不能 scoped否则 body * 不生效 -->
<style>
@media print {
body * { visibility: hidden !important; }
.rx-paper, .rx-paper * { visibility: visible !important; }
.rx-paper {
position: fixed !important;
left: 0 !important;
top: 0 !important;
width: 210mm !important;
height: 297mm !important;
margin: 0 !important;
padding: 8mm 10mm !important;
box-shadow: none !important;
page-break-after: always;
}
@page {
size: A4;
margin: 0;
}
}
</style>
@@ -1770,12 +1770,6 @@
</div>
</template>
<div v-loading="prescriptionViewLoading" class="rx-wrap">
<!-- 处方签签 -->
<el-tabs v-model="prescriptionTabType" class="mx-4 mt-2" style="margin-bottom: 0px; z-index: 1; position: relative;" v-if="prescriptionViewData">
<el-tab-pane label="药房联" name="internal" />
<el-tab-pane label="处方联" name="user" />
</el-tabs>
<!-- 状态条不进入打印/导出范围 -->
<div class="rx-statusbar" v-if="prescriptionViewData">
<el-tag
@@ -1824,205 +1818,11 @@
<div v-if="prescriptionViewData.audit_remark" class="mt-1">驳回意见{{ prescriptionViewData.audit_remark }}</div>
</div>
<!-- A4 处方纸药房联 -->
<div v-if="prescriptionViewData" ref="prescriptionSlipPrintRef" class="rx-paper">
<div class="rx-title">{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}</div>
<div class="rx-notice">
<span class="rx-notice-text">
{{
prescriptionTabType === 'user'
? '服药前请核对姓名、电话、医生等信息以及医嘱等要点'
: '服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点'
}}
</span>
<div class="rx-notice-meta">
<div class="rx-meta-item">
<span class="rx-meta-label">日期</span>
<span class="rx-meta-value">{{ rxDateText }}</span>
</div>
<div class="rx-meta-item">
<span class="rx-meta-label">处方编号</span>
<span class="rx-meta-value">{{ rxPrescriptionSnText }}</span>
</div>
<div class="rx-meta-item">
<span class="rx-meta-label">流转编号挂号</span>
<span class="rx-meta-value">{{ rxAppointmentFlowText }}</span>
</div>
<div v-if="rxBusinessOrderNoText" class="rx-meta-item">
<span class="rx-meta-label">业务单号</span>
<span class="rx-meta-value">{{ rxBusinessOrderNoText }}</span>
</div>
</div>
</div>
<div class="rx-info">
<div class="rx-info-row">
<div class="rx-info-cell">
<span class="rx-key">姓名</span>
<span class="rx-val">{{ prescriptionViewData.patient_name || '—' }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">性别</span>
<span class="rx-val">{{ slipGenderLabel(prescriptionViewData.gender) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">年龄</span>
<span class="rx-val">{{ slipAgeText(prescriptionViewData.age) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">电话</span>
<span class="rx-val">{{ prescriptionViewData.phone || prescriptionViewData.recipient_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">{{ rxRecipientText }}</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">{{ prescriptionViewData.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="slipMainHerbs.length">
<div class="rx-herb-section-label">主方</div>
<div
v-for="(h, i) in slipMainHerbs"
: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="slipAuxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">
辅方<span v-if="slipAuxLibraryName" class="rx-herb-library-name">{{ slipAuxLibraryName }}</span>
</div>
<div
v-for="(h, i) in slipAuxHerbs"
: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="slipMainHerbs.length">
<div class="rx-herb-section-label">主方</div>
<div
v-for="(h, i) in slipMainHerbs"
: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="slipAuxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
<div
v-for="(h, i) in slipAuxHerbs"
: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="rxAuxUsageText">主服法{{ rxUsageText }}</p>
<p v-else>服法{{ rxUsageText }}</p>
<p v-if="rxAuxUsageText">辅服法{{ rxAuxUsageText }}</p>
</template>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="prescriptionTabType === 'internal' && rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
</p>
<p
v-if="prescriptionTabType === 'internal' && rxOutPelletText && prescriptionViewData?.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-doctor">
<div class="rx-bot-label rx-bot-label--doctor">医师</div>
<div class="rx-bot-doctor-body">
<img
v-if="prescriptionViewData.doctor_signature"
:src="prescriptionViewData.doctor_signature"
alt="医师签名"
class="rx-bot-sign-img"
/>
<span
v-if="!prescriptionViewData.doctor_signature"
class="rx-bot-doctor-name"
>{{ prescriptionViewData.doctor_name || '—' }}</span>
</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">
{{
detailData?.medication_days != null &&
String(detailData.medication_days).trim() !== ''
? detailData.medication_days + ' 天'
: '—'
}} </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>
<div class="rx-bot-row rx-hospital-row">
<div class="rx-hospital-line">成都双流甄养堂互联网医院有限公司 联系方式4001667339</div>
<div class="rx-hospital-line">地址四川省成都市双流区黄甲街道黄龙大道二段280号</div>
</div>
</div>
<PrescriptionSlip
v-if="prescriptionViewData"
ref="rxSlipRef"
:data="prescriptionViewData"
/>
<!-- 审核痕迹不进入导出 -->
<div
@@ -2281,6 +2081,7 @@ import {
getDoctors,
getAssistants
} from '@/api/tcm'
import PrescriptionSlip from '@/components/prescription-slip/index.vue'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import { getDictData } from '@/api/app'
@@ -4665,8 +4466,7 @@ const prescriptionViewLoading = ref(false)
const prescriptionViewData = ref<any>(null)
/** 药房联辅方标题:处方库中该辅方模板的 prescription_name(导入或药材匹配解析) */
const slipAuxLibraryName = ref('')
const prescriptionTabType = ref('internal')
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
const rxSlipRef = ref<InstanceType<typeof PrescriptionSlip> | null>(null)
const prescriptionSlipExporting = ref(false)
function buildSlipUsageSegmentText(
@@ -5064,8 +4864,7 @@ async function openPrescriptionView(row: any) {
prescriptionViewLoading.value = true
prescriptionViewData.value = null
slipAuxLibraryName.value = ''
prescriptionTabType.value = 'internal'
try {
const res: any = await prescriptionDetail({ id: row.prescription_id })
const base = res?.data ?? res ?? null
@@ -5135,7 +4934,7 @@ async function preloadSlipImagesToDataUrl(el: HTMLElement): Promise<void> {
async function capturePrescriptionSlipCanvas(): Promise<HTMLCanvasElement> {
await nextTick()
const el = prescriptionSlipPrintRef.value
const el = (rxSlipRef.value as { paperEl?: HTMLElement | null } | null)?.paperEl ?? null
if (!el) throw new Error('未找到处方笺内容')
el.scrollIntoView({ behavior: 'instant', block: 'nearest' })
await preloadSlipImagesToDataUrl(el)
@@ -2390,145 +2390,11 @@
<div v-if="prescriptionViewData.audit_remark" class="mt-1">驳回意见{{ prescriptionViewData.audit_remark }}</div>
</div>
<!-- A4 处方纸药房联 -->
<div v-if="prescriptionViewData" ref="prescriptionSlipPrintRef" class="rx-paper">
<div class="rx-title">{{ SLIP_TITLE }}</div>
<div class="rx-notice">
<span class="rx-notice-text">
服药前请核对姓名电话医生等信息以及服法医嘱等要点
</span>
<div class="rx-notice-meta">
<div class="rx-meta-item">
<span class="rx-meta-label">日期</span>
<span class="rx-meta-value">{{ rxDateText }}</span>
</div>
<div class="rx-meta-item">
<span class="rx-meta-label">处方编号</span>
<span class="rx-meta-value">{{ rxPrescriptionSnText }}</span>
</div>
<div class="rx-meta-item">
<span class="rx-meta-label">流转编号挂号</span>
<span class="rx-meta-value">{{ rxAppointmentFlowText }}</span>
</div>
<div v-if="rxBusinessOrderNoText" class="rx-meta-item">
<span class="rx-meta-label">业务单号</span>
<span class="rx-meta-value">{{ rxBusinessOrderNoText }}</span>
</div>
</div>
</div>
<div class="rx-info">
<div class="rx-info-row">
<div class="rx-info-cell">
<span class="rx-key">姓名</span>
<span class="rx-val">{{ prescriptionViewData.patient_name || '—' }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">性别</span>
<span class="rx-val">{{ slipGenderLabel(prescriptionViewData.gender) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">年龄</span>
<span class="rx-val">{{ slipAgeText(prescriptionViewData.age) }}</span>
</div>
<div class="rx-info-cell">
<span class="rx-key">电话</span>
<span class="rx-val">{{ prescriptionViewData.phone || prescriptionViewData.recipient_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">{{ rxRecipientText }}</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">{{ prescriptionViewData.clinical_diagnosis || '—' }}</span>
</div>
</div>
</div>
<div class="rx-rp">
<div class="rx-watermark">药房联</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>总量</span>
</div>
<div class="rx-rp-col-head">
<span>用药 (单剂)</span>
<span>总量</span>
</div>
</div>
</div>
<div class="rx-herbs">
<div v-for="(h, i) in slipHerbsList" :key="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>
</div>
</div>
<div class="rx-text">
<p>服法{{ rxUsageText }}</p>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
</p>
<p v-if="rxOutPelletText" class="rx-text-warn">
出丸{{ rxOutPelletText }}
</p>
</div>
<div class="rx-bottom">
<div class="rx-bot-row rx-bot-row-1">
<div class="rx-bot-cell rx-bot-doctor">
<div class="rx-bot-label rx-bot-label--doctor">医师</div>
<div class="rx-bot-doctor-body">
<img
v-if="prescriptionViewData.doctor_signature"
:src="prescriptionViewData.doctor_signature"
alt="医师签名"
class="rx-bot-sign-img"
/>
<span
v-if="!prescriptionViewData.doctor_signature"
class="rx-bot-doctor-name"
>{{ prescriptionViewData.doctor_name || '—' }}</span>
</div>
</div>
<div 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">
{{
detailData.medication_days != null &&
String(detailData.medication_days).trim() !== ''
? detailData.medication_days + ' 天'
: '—'
}} </span>
</div>
<div 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>
<div class="rx-bot-row rx-hospital-row">
<div class="rx-hospital-line">成都双流甄养堂互联网医院有限公司 联系方式4001667339</div>
<div class="rx-hospital-line">地址四川省成都市双流区黄甲街道黄龙大道二段280号</div>
</div>
</div>
<PrescriptionSlip
v-if="prescriptionViewData"
ref="rxSlipRef"
:data="prescriptionViewData"
/>
<!-- 审核痕迹不进入导出 -->
<div
@@ -2750,6 +2616,7 @@ import {
supplyModeLabel,
supplyModeTagType
} from './components/prescription-order-utils'
import PrescriptionSlip from '@/components/prescription-slip/index.vue'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import { getDictData } from '@/api/app'
@@ -5429,7 +5296,7 @@ const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄
const prescriptionViewVisible = ref(false)
const prescriptionViewLoading = ref(false)
const prescriptionViewData = ref<any>(null)
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
const rxSlipRef = ref<InstanceType<typeof PrescriptionSlip> | null>(null)
const prescriptionSlipExporting = ref(false)
//
@@ -5763,7 +5630,7 @@ async function preloadSlipImagesToDataUrl(el: HTMLElement): Promise<void> {
async function capturePrescriptionSlipCanvas(): Promise<HTMLCanvasElement> {
await nextTick()
const el = prescriptionSlipPrintRef.value
const el = (rxSlipRef.value as { paperEl?: HTMLElement | null } | null)?.paperEl ?? null
if (!el) throw new Error('未找到处方笺内容')
el.scrollIntoView({ behavior: 'instant', block: 'nearest' })
await preloadSlipImagesToDataUrl(el)