This commit is contained in:
Your Name
2026-04-30 14:04:12 +08:00
parent eb782305e6
commit d3003ce0ac
19 changed files with 1560 additions and 313 deletions
@@ -437,16 +437,12 @@
<div class="rx-bot-doctor-name">{{ slipView.doctor_name || '—' }}</div>
</div>
</div>
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">审方</div></div>
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">配药</div></div>
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">复核</div></div>
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">发药</div></div>
<div class="rx-bot-cell rx-bot-meta">
<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-key">:</span>
<span class="rx-bot-meta-val">{{ slipView.dose_count || '—' }}</span>
</div>
<div class="rx-bot-cell rx-bot-meta">
@@ -454,32 +450,7 @@
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}</span>
</div>
</div>
<div class="rx-bot-row rx-bot-row-2">
<div class="rx-bot-cell rx-fee">
<span class="rx-fee-key">医药费</span>
<span class="rx-fee-val">{{ rxFee.medicine }}</span>
</div>
<div class="rx-bot-cell rx-fee">
<span class="rx-fee-key">诊金:</span>
<span class="rx-fee-val">{{ rxFee.consult }}</span>
</div>
<div class="rx-bot-cell rx-fee">
<span class="rx-fee-key">制作费:</span>
<span class="rx-fee-val">{{ rxFee.making }}</span>
</div>
<div class="rx-bot-cell rx-fee">
<span class="rx-fee-key">运费:</span>
<span class="rx-fee-val">{{ rxFee.shipping }}</span>
</div>
<div class="rx-bot-cell rx-fee">
<span class="rx-fee-key">优惠:</span>
<span class="rx-fee-val">{{ rxFee.discount }}</span>
</div>
<div class="rx-bot-cell rx-fee rx-fee-total">
<span class="rx-fee-key">合计:</span>
<span class="rx-fee-val">{{ rxFee.total }}</span>
</div>
</div>
</div>
</div>
@@ -238,7 +238,20 @@
<el-table-column label="收货人" min-width="100">
<template #default="{ row }">
<div>{{ row.recipient_name || '—' }}</div>
<div class="text-xs text-gray-400">{{ row.recipient_phone || '' }}</div>
<div
class="text-xs"
:class="
recipientVsPrescriptionPhoneMismatch(row.recipient_phone, row.prescription_phone)
? 'text-red-600 font-semibold'
: 'text-gray-400'
"
>
{{ row.recipient_phone || '—' }}
<span
v-if="recipientVsPrescriptionPhoneMismatch(row.recipient_phone, row.prescription_phone)"
class="ml-1"
>与处方手机不一致</span>
</div>
</template>
</el-table-column>
<el-table-column label="费用类别" width="96">
@@ -485,6 +498,29 @@
<div v-else-if="detailData" class="po-detail-body px-6 pb-6 h-full overflow-y-auto">
<!-- 审核与履约可视化步骤条 -->
<el-alert
v-if="detailRecipientRxPhoneMismatch"
type="error"
:closable="false"
show-icon
class="mb-4 !items-start"
title="业务订单收货手机与处方登记手机不一致"
>
<template #default>
<div class="text-sm leading-relaxed">
<div>
处方手机
<span class="font-mono font-semibold text-red-700">{{ detailPrescriptionPhoneDisplay }}</span>
</div>
<div>
订单收货手机
<span class="font-mono font-semibold text-red-700">{{ detailOrderPhoneDisplay }}</span>
</div>
<div class="text-gray-600 mt-1">请核对是否代收货或录入错误避免物流与回访联系错人</div>
</div>
</template>
</el-alert>
<div class="bg-gray-50/80 border border-gray-100 rounded-lg p-5 mb-5 shadow-sm">
<el-steps :active="workflowActiveStep" align-center finish-status="success">
<el-step title="业务订单创建" :description="formatTime(detailData.create_time)" />
@@ -577,6 +613,20 @@
<span v-if="detailData.doctor_name" class="text-xs text-gray-500">
开方人<span class="text-primary font-medium">{{ detailData.doctor_name }}</span>
</span>
<el-button
v-if="
detailData.prescription_id &&
detailPrescription &&
!String(detailData.prescription_detail_error || '').trim()
"
v-perms="['tcm.prescriptionOrder/patchPrescriptionPatient']"
type="default"
size="small"
link
@click="openPatchRxPatientDialog"
>
改患者信息
</el-button>
<el-button
v-if="detailData.prescription_id && showPrescriptionAuditFilter"
type="primary"
@@ -616,7 +666,24 @@
<el-descriptions-item label="性别 / 年龄">
{{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}岁
</el-descriptions-item>
<el-descriptions-item label="手机">{{ detailPrescription.phone || '—' }}</el-descriptions-item>
<el-descriptions-item label="手机">
<span
:class="
detailRecipientRxPhoneMismatch ? 'rx-phone-mismatch text-red-600 font-bold' : ''
"
>
{{ detailPrescription.phone || '—' }}
</span>
<el-tag
v-if="detailRecipientRxPhoneMismatch"
type="danger"
size="small"
effect="dark"
class="ml-2 align-middle"
>
与订单手机不一致
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处方日期">{{ detailPrescription.prescription_date || '—' }}</el-descriptions-item>
<el-descriptions-item label="医师">{{ detailPrescription.doctor_name || '—' }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ detailPrescription.prescription_type || '—' }}</el-descriptions-item>
@@ -814,8 +881,55 @@
<el-descriptions-item label="诊单ID">{{ detailData.diagnosis_id }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
<el-descriptions-item label="收货人">{{ detailData.recipient_name }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ detailData.recipient_phone }}</el-descriptions-item>
<el-descriptions-item label="挂号关联" :span="3">
<template v-if="detailLinkedAppointment">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]">
<span
>预约 <span class="font-mono text-primary">#{{ detailLinkedAppointment.id }}</span></span
>
<span class="text-gray-500"
>时间:{{ detailLinkedAppointment.appointment_date }}
{{ detailLinkedAppointment.period }}
{{ detailLinkedAppointment.appointment_time }}</span
>
<el-tag size="small" type="info">{{ detailLinkedAppointment.appointment_type_desc }}</el-tag>
<el-tag size="small">{{ detailLinkedAppointment.status_desc }}</el-tag>
<span v-if="detailLinkedAppointment.doctor_name"
>接诊:{{detailLinkedAppointment.doctor_name }}</span
>
<span v-if="detailLinkedAppointment.channel_source" class="text-gray-500"
>渠道:{{ detailLinkedAppointment.channel_source }}</span
>
<el-tag v-if="detailLinkedAppointmentResolvedFromTag" size="small" effect="plain">{{
detailLinkedAppointmentResolvedFromTag
}}</el-tag>
</div>
<p v-if="String(detailLinkedAppointment.remark || '').trim()" class="text-xs text-gray-500 mt-1">
备注:{{ detailLinkedAppointment.remark }}
</p>
</template>
<span v-else class="text-gray-400">暂无挂号预约记录(未绑定或诊单下无预约)</span>
</el-descriptions-item>
<el-descriptions-item label="收货手机">
<span
:class="
detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold font-mono' : 'font-mono'
"
>
{{ detailData.recipient_phone || '—' }}
</span>
<el-tag
v-if="detailRecipientRxPhoneMismatch"
type="danger"
size="small"
effect="dark"
class="ml-2 align-middle"
>
与处方手机不一致
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收货地址" :span="2">{{ detailFullAddress }}</el-descriptions-item>
<el-descriptions-item label="复诊">{{ detailData.is_follow_up ? '是' : '否' }}</el-descriptions-item>
@@ -1611,6 +1725,47 @@
</template>
</el-dialog>
<!-- 修改关联处方:患者姓名 / 手机 -->
<el-dialog
v-model="patchRxPatientVisible"
title="修改处方患者信息"
width="440px"
:close-on-click-modal="false"
destroy-on-close
@closed="resetPatchRxPatientForm"
>
<el-form
ref="patchRxPatientFormRef"
:model="patchRxPatientForm"
:rules="patchRxPatientRules"
label-width="96px"
>
<el-form-item label="患者姓名" prop="patient_name">
<el-input
v-model="patchRxPatientForm.patient_name"
maxlength="50"
show-word-limit
placeholder="与处方笺一致"
clearable
/>
</el-form-item>
<el-form-item label="手机" prop="phone">
<el-input
v-model="patchRxPatientForm.phone"
maxlength="20"
placeholder="处方登记手机号"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="patchRxPatientVisible = false">取消</el-button>
<el-button type="primary" :loading="patchRxPatientSaving" @click="submitPatchRxPatient">
保存
</el-button>
</template>
</el-dialog>
<!-- 处方详情查看(处方单样式) -->
<el-drawer
v-model="prescriptionViewVisible"
@@ -1694,12 +1849,25 @@
<span class="rx-notice-text">
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点
</span>
<span class="rx-notice-meta">
<span>日期{{ rxDateText }}</span>
<span>编号{{ rxSerialText }}</span>
</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">
@@ -1770,8 +1938,8 @@
<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-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"
@@ -1779,7 +1947,7 @@
alt="医师签名"
class="rx-bot-sign-img"
/>
<div class="rx-bot-doctor-name">{{ prescriptionViewData.doctor_name || '—' }}</div>
<!-- <span class="rx-bot-doctor-name">{{ prescriptionViewData.doctor_name || '—' }}</span> -->
</div>
</div>
@@ -1788,11 +1956,17 @@
<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">{{ prescriptionViewData.dose_count || '—' }}</span>
<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-key">剂量:</span>
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}克</span>
</div>
</div>
@@ -1980,6 +2154,7 @@ import {
prescriptionOrderComplete,
prescriptionOrderRevokeRxAudit,
prescriptionOrderRevokePayAudit,
prescriptionOrderPatchPrescriptionPatient,
prescriptionOrderLinkPayOrder,
prescriptionOrderSubmitGancaoRecipel,
prescriptionOrderPreviewGancaoRecipel,
@@ -2703,6 +2878,47 @@ const detailPrescription = computed(() => {
return p && typeof p === 'object' ? p : null
})
function normalizeBizPhone(v: unknown): string {
if (v === null || v === undefined) return ''
return String(v).replace(/\s/g, '').trim()
}
function recipientVsPrescriptionPhoneMismatch(recipient: unknown, rxPhone: unknown): boolean {
const a = normalizeBizPhone(recipient)
const b = normalizeBizPhone(rxPhone)
if (!a || !b) return false
return a !== b
}
/** 详情抽屉:业务收货手机 vs 处方登记手机 */
const detailRecipientRxPhoneMismatch = computed(() => {
const rx = detailPrescription.value?.phone
const biz = detailData.value?.recipient_phone
return recipientVsPrescriptionPhoneMismatch(biz, rx)
})
const detailPrescriptionPhoneDisplay = computed(() => {
const s = normalizeBizPhone(detailPrescription.value?.phone)
return s || '—'
})
const detailOrderPhoneDisplay = computed(() => {
const s = normalizeBizPhone(detailData.value?.recipient_phone)
return s || '—'
})
const detailLinkedAppointment = computed(() => {
const a = detailData.value?.linked_appointment
return a && typeof a === 'object' ? a : null
})
const detailLinkedAppointmentResolvedFromTag = computed(() => {
const from = String(detailLinkedAppointment.value?.resolved_from || '')
if (from === 'prescription') return '处方绑定挂号'
if (from === 'diagnosis') return '诊单关联挂号'
return ''
})
const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs))
/** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */
@@ -2790,7 +3006,11 @@ function logActionText(act: string) {
fill_tracking: '填快递单',
ship: '确认发货',
withdraw: '撤销',
add_pay_order: '补齐支付单',
link_pay_order: '关联支付单',
revoke_rx_audit: '撤回处方审核',
revoke_pay_audit: '撤回支付审核',
gancao_submit: '甘草下单',
patch_rx_patient: '处方患者信息',
complete: '完成订单'
}
return m[act] || act
@@ -2840,6 +3060,75 @@ async function fetchLogisticsTrace() {
}
}
const patchRxPatientVisible = ref(false)
const patchRxPatientSaving = ref(false)
const patchRxPatientFormRef = ref<FormInstance>()
const patchRxPatientForm = reactive({
patient_name: '',
phone: ''
})
const patchRxPatientRules: FormRules = {
patient_name: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}
function openPatchRxPatientDialog() {
const rx = detailPrescription.value
const ord = detailData.value
if (!rx || !ord?.id || !ord.prescription_id) {
feedback.msgWarning('无处方数据')
return
}
patchRxPatientForm.patient_name = String(rx.patient_name ?? '').trim()
patchRxPatientForm.phone = String(rx.phone ?? '').trim()
patchRxPatientVisible.value = true
nextTick(() => patchRxPatientFormRef.value?.clearValidate())
}
function resetPatchRxPatientForm() {
patchRxPatientForm.patient_name = ''
patchRxPatientForm.phone = ''
}
async function refreshCurrentPrescriptionOrderDetail() {
const id = detailData.value?.id
if (!id) return
try {
const res: any = await prescriptionOrderDetail({ id })
const d = res?.data ?? res ?? null
if (d) detailData.value = d
} catch {
/* 静默失败,避免打断当前抽屉 */
}
}
async function submitPatchRxPatient() {
const form = patchRxPatientFormRef.value
if (!form) return
try {
await form.validate()
} catch {
return
}
const ordId = detailData.value?.id
if (!ordId) return
patchRxPatientSaving.value = true
try {
await prescriptionOrderPatchPrescriptionPatient({
id: ordId,
patient_name: patchRxPatientForm.patient_name.trim(),
phone: patchRxPatientForm.phone.trim()
})
patchRxPatientVisible.value = false
await refreshCurrentPrescriptionOrderDetail()
await fetchLogs(ordId)
} catch (e) {
console.error(e)
} finally {
patchRxPatientSaving.value = false
}
}
async function openDetail(id: number) {
// 修复 Bug:显式彻底清空缓存,防止前一次弹窗的数据残留
detailData.value = null
@@ -3878,18 +4167,32 @@ const rxDateText = computed(() => {
return v.prescription_date || '—'
})
const rxSerialText = computed(() => {
/** 处方笺「处方编号」:处方 sn,缺省 visit_no */
const rxPrescriptionSnText = computed(() => {
const v = prescriptionViewData.value as any
if (!v) return '—'
return (
v.order_no ||
v.serial_no ||
v.serial_number ||
v.no ||
v.prescription_no ||
v.visit_no ||
(v.id ? `G${v.id}` : '—')
)
const sn = String(v.sn ?? '').trim()
if (sn) return sn
const vn = String(v.visit_no ?? '').trim()
if (vn) return vn
return '—'
})
/** 流转编号(挂号):预约/挂号主键 */
const rxAppointmentFlowText = computed(() => {
const v = prescriptionViewData.value as any
if (!v) return '—'
const aid = Number(v.appointment_id)
if (Number.isFinite(aid) && aid > 0) return String(aid)
return '—'
})
/** 业务订单号(从打开详情的订单行合并,便于笺面核对) */
const rxBusinessOrderNoText = computed(() => {
const v = prescriptionViewData.value as any
if (!v) return ''
const no = String(v.order_no ?? '').trim()
return no
})
const rxRecipientText = computed(() => {
@@ -4085,6 +4388,22 @@ async function openPrescriptionView(row: any) {
if (row.recipient_phone && !merged.recipient_phone) merged.recipient_phone = row.recipient_phone
if (row.shipping_address && !merged.shipping_address) merged.shipping_address = row.shipping_address
if (row.amount != null && merged.order_amount == null) merged.order_amount = row.amount
/** 笺面剂数/单位与业务订单一致(甘草预览与订单详情同口径) */
if (row.dose_count != null && String(row.dose_count).trim() !== '' && Number(row.dose_count) > 0) {
merged.dose_count = Number(row.dose_count)
}
if (row.dose_unit != null && String(row.dose_unit).trim() !== '') {
merged.dose_unit = String(row.dose_unit)
}
const linkedAp = row.linked_appointment
if (
(!merged.appointment_id || Number(merged.appointment_id) <= 0) &&
linkedAp &&
typeof linkedAp === 'object' &&
Number(linkedAp.id) > 0
) {
merged.appointment_id = Number(linkedAp.id)
}
prescriptionViewData.value = merged
} else {
prescriptionViewData.value = null
@@ -4389,25 +4708,84 @@ async function downloadPrescriptionSlipPdf() {
overflow: visible;
}
/* === 顶部告知 === */
/* === 顶部告知上下分块避免左右 flex 把左侧提示挤成竖条=== */
.rx-notice {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column;
align-items: stretch;
gap: 10px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
padding: 16px 10px;
padding: 14px 12px;
border-radius: 2px;
font-size: 12px;
color: #1f1f1f;
margin-bottom: 6px;
.rx-notice-text { flex: 1; }
.rx-notice-text {
flex: none;
width: 100%;
line-height: 1.55;
word-break: break-word;
}
.rx-notice-meta {
display: flex;
flex-flow: row nowrap;
align-items: center;
gap: 4px 14px;
width: 100%;
box-sizing: border-box;
margin-top: 4px;
padding: 8px 2px 2px;
border-top: 1px solid #e5e7eb;
font-size: 10px;
line-height: 2.35;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
.rx-notice-meta .rx-meta-item {
display: inline-flex;
gap: 24px;
flex: 0 0 auto;
align-items: baseline;
gap: 2px;
min-width: 0;
padding: 0;
margin: 0;
background: transparent;
border: none;
border-radius: 0;
white-space: nowrap;
line-height: 2.35;
height:20px;
}
.rx-notice-meta .rx-meta-item:not(:first-child) {
border-left: 1px solid #d1d5db;
padding-left: 14px;
margin-left: 2px;
line-height: 2.35;
}
.rx-notice-meta .rx-meta-label {
font-weight: 600;
color: #6b7280;
line-height: 1.35;
flex-shrink: 0;
}
.rx-notice-meta .rx-meta-label::after {
content: '';
font-weight: 500;
color: #9ca3af;
}
.rx-notice-meta .rx-meta-value {
color: #111827;
line-height: 1.35;
font-variant-numeric: tabular-nums;
}
}
@@ -4663,21 +5041,39 @@ async function downloadPrescriptionSlipPdf() {
margin-bottom: 4px;
}
.rx-bot-label--doctor {
flex-shrink: 0;
margin-bottom: 0;
margin-right: 6px;
line-height: 1.2;
align-self: center;
}
.rx-bot-doctor {
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
.rx-bot-doctor-body {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
min-height: 36px;
justify-content: flex-start;
flex-wrap: nowrap;
gap: 8px;
min-height: 0;
}
.rx-bot-doctor-name {
font-size: 13px;
color: #1f1f1f;
line-height: 1.2;
white-space: nowrap;
}
.rx-bot-sign-img {
flex-shrink: 0;
max-height: 40px;
max-width: 110px;
object-fit: contain;