Files
zyt/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue
T
2026-06-15 10:37:10 +08:00

1451 lines
70 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!--
处方业务订单详情抽屉共享组件
使用方
1. consumer/prescription/order_list.vue —— 完整版header 插槽注入审核/发货等操作
2. tcm/diagnosis/components/PatientOrderList.vue —— readonly 受限版
注意
- 详情展示的任何字段/区块改动都必须改这里不要回到页面里复制模板
- readonly 版是选择性受限展示」:流程步骤条顶部告警挂号关联内部成本
药材明细收款方式京东接口更新等仅完整版展示v-if="!readonly" 门控);
金额概览含退款金额/需代收)、用量与服用方式主方辅方等基础信息两版共享
-->
<el-drawer
v-model="detailVisible"
class="po-detail-drawer"
size="80%"
destroy-on-close
:close-on-click-modal="false"
:append-to-body="appendToBody"
>
<template #header>
<div class="po-detail-drawer-head flex items-center justify-between">
<div class="flex items-center">
<span class="po-detail-drawer-title text-gray-800 font-semibold text-lg">业务订单详情</span>
<el-tag v-if="detailData?.order_no" type="primary" effect="plain" round class="ml-3">{{ detailData.order_no }}</el-tag>
<el-tag
v-if="detailData?.gancao_reciperl_order_no"
type="success"
effect="plain"
round
class="ml-2"
>甘草 {{ detailData.gancao_reciperl_order_no }}</el-tag>
<!-- 完整版发货类型切换等 -->
<slot v-if="detailData" name="header-extra" :detail="detailData" />
</div>
<div v-if="detailData" class="flex items-center gap-2 mr-6">
<!-- 完整版处方审核 / 支付审核 / 发货 / 退款等操作按钮 -->
<slot name="header-actions" :detail="detailData" />
</div>
</div>
</template>
<el-skeleton v-if="detailLoading" :rows="12" animated class="px-6 py-4" />
<div v-else-if="detailData" class="po-detail-body px-6 pb-6 h-full overflow-y-auto">
<!-- 收货手机与处方手机不一致告警仅完整版 -->
<el-alert
v-if="!readonly && 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>
<!-- 物流轨迹含拒收/退回等抽屉内强提示仅完整版 -->
<el-alert
v-if="!readonly && detailLogisticsUrgent.show && String(detailData?.tracking_number || '').trim()"
type="error"
effect="dark"
:closable="false"
show-icon
class="mb-4 !items-start ring-2 ring-red-600/90 shadow-lg"
title="物流异常:轨迹中出现拒收 / 退回等字样,请优先跟进处理"
>
<template #default>
<div class="text-sm leading-relaxed space-y-1.5">
<div v-if="detailLogisticsUrgent.keywords.length" class="flex flex-wrap gap-1.5 items-center">
<span class="text-red-100 text-xs">命中关键词</span>
<el-tag
v-for="k in detailLogisticsUrgent.keywords"
:key="k"
type="danger"
effect="plain"
size="small"
class="!border-white/40 !text-white"
>
{{ k }}
</el-tag>
</div>
<ul v-if="detailLogisticsUrgent.lines.length" class="list-disc pl-5 space-y-1 text-red-50">
<li v-for="(line, idx) in detailLogisticsUrgent.lines" :key="idx">{{ line }}</li>
</ul>
<div class="text-xs text-red-100/90 pt-1 border-t border-white/15">
请核实是否需改址重发拦截退件或协调患者签收并在备注中记录处理结果
</div>
</div>
</template>
</el-alert>
<!-- 审核与履约可视化步骤条仅完整版 -->
<div v-if="!readonly" 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)" />
<el-step title="处方单双审" :status="workflowRxStatus">
<template #description>
<span v-if="workflowRxStatus === 'error'" class="text-red-500 font-medium">处方被驳回</span>
<span v-else-if="workflowRxStatus === 'wait'">等待审核</span>
<span v-else-if="workflowRxStatus === 'success'" class="text-green-600">双审通过</span>
<span v-else class="text-gray-400">进行中</span>
</template>
</el-step>
<el-step title="支付单审核" :status="workflowPayStatus">
<template #description>
<span v-if="workflowPayStatus === 'error'" class="text-red-500 font-medium">支付审核驳回</span>
<span v-else-if="workflowPayStatus === 'wait'">等待审核</span>
<span v-else-if="workflowPayStatus === 'success'" class="text-green-600">审核通过</span>
<span v-else class="text-gray-400">进行中</span>
</template>
</el-step>
<el-step title="发货与履约" :status="workflowFulfillStatus">
<template #description>
<span
v-if="workflowFulfillStatus === 'process' && Number(detailData?.fulfillment_status) === 7"
class="text-amber-600 font-medium"
>进行中</span>
<span v-else-if="workflowFulfillStatus === 'process'" class="text-primary font-medium">等待发货/补充单号</span>
<span v-else-if="workflowFulfillStatus === 'wait'">待转入履约</span>
<span v-else-if="workflowFulfillStatus === 'success'" class="text-green-600">
{{ fulfillmentText(detailData?.fulfillment_status) }}
</span>
<span
v-else-if="workflowFulfillStatus === 'error'"
class="text-red-500 font-medium"
>
{{ fulfillmentText(detailData?.fulfillment_status) }}
</span>
</template>
</el-step>
<el-step title="订单完成" />
</el-steps>
</div>
<!-- 顶部数据概览卡片 -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-5">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between mb-1">
<div class="stat-title text-gray-500 text-xs">总金额</div>
<el-button
v-if="!readonly && canUpdateAmount(detailData)"
v-perms="['tcm.prescriptionOrder/updateAmount']"
type="primary"
size="small"
link
@click="emit('update-amount')"
>
修改
</el-button>
</div>
<div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">退款金额</div>
<div
class="stat-value font-bold text-xl"
:class="Number(detailData.refund_amount) > 0 ? 'text-rose-500' : 'text-gray-400'"
>
¥{{ formatMoney(detailData.refund_amount || 0) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单 -->
<el-card shadow="never" class="po-panel po-panel-rx border-gray-100 h-full relative overflow-hidden">
<!-- 处方审核印章 -->
<div v-if="detailData.prescription_audit_status === 1" class="audit-stamp stamp-pass">
<div class="stamp-inner">审核通过</div>
</div>
<div v-if="detailData.prescription_audit_status === 2" class="audit-stamp stamp-reject">
<div class="stamp-inner">已驳回</div>
</div>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-[15px]">处方详情</span>
<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="
!readonly &&
detailData.prescription_id &&
detailPrescription &&
!String(detailData.prescription_detail_error || '').trim()
"
v-perms="['tcm.prescriptionOrder/patchPrescriptionPatient']"
type="default"
size="small"
link
@click="emit('patch-rx-patient')"
>
改患者信息
</el-button>
<el-button
v-if="!readonly && detailData.prescription_id && showPrescriptionAuditFilter"
type="primary"
size="small"
link
@click="emit('view-prescription')"
>
详细
</el-button>
<el-button
v-if="!readonly && detailData.id && showPrescriptionAuditFilter"
type="warning"
size="small"
link
:loading="gancaoPreviewLoading"
:icon="Search"
@click="emit('test-gancao-preview')"
>
测试价格
</el-button>
</div>
<span class="text-xs text-gray-400">ID: {{ detailData.prescription_id || '—' }}</span>
</div>
</template>
<el-alert
v-if="!readonly && String(detailData.prescription_detail_error || '').trim()"
:title="detailData.prescription_detail_error"
type="warning"
:closable="false"
show-icon
class="mb-3"
/>
<template v-else-if="detailPrescription">
<el-descriptions :column="2" border size="small" class="po-desc bg-white">
<el-descriptions-item label="处方编号" :span="2">{{ detailPrescription.sn || '—' }}</el-descriptions-item>
<el-descriptions-item label="患者">{{ detailPrescription.patient_name || '—' }}</el-descriptions-item>
<el-descriptions-item label="性别 / 年龄">
{{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}岁
</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>
<el-descriptions-item label="临床诊断" :span="2">
{{ detailPrescription.clinical_diagnosis || '—' }}
</el-descriptions-item>
<el-descriptions-item label="剂数 / 用法" :span="2" v-if="showPrescriptionAuditFilter">
{{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} ·
{{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }}
</el-descriptions-item>
<el-descriptions-item label="处方金额">
<span class="text-red-500 font-medium">¥{{ detailPrescription.amount ?? '—' }}</span>
</el-descriptions-item>
<el-descriptions-item label="消费者审核">
<el-tag :type="consumerRxAuditTag(detailPrescription.audit_status)" size="small">
{{ consumerRxAuditText(detailPrescription.audit_status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用量">
<div class="flex flex-col gap-1 text-sm leading-relaxed">
<div>
<span v-if="detailHasAuxHerbs" class="text-gray-500 mr-1">主方:</span>
<template v-if="detailPrescription.dosage_amount">
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}袋
</template>
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
</span>
</template>
<template v-else>—</template>
</div>
<div v-if="detailHasAuxHerbs && detailAuxUsage">
<span class="text-gray-500 mr-1">辅方:</span>
<template v-if="detailAuxUsage.dosage_amount != null && detailAuxUsage.dosage_amount !== 0">
{{ detailAuxUsage.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
· {{ Number(detailAuxUsage.dosage_bag_count) > 0 ? Number(detailAuxUsage.dosage_bag_count) : 1 }}袋
</template>
<span v-if="detailPrescription.prescription_type === '饮片'" class="ml-2 text-gray-500">
({{ detailAuxUsage.need_decoction ? '代煎' : '不代煎' }})
</span>
</template>
<template v-else>—</template>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="服用方式">
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
<template v-if="detailHasAuxHerbs">
<div>
<span class="text-gray-500 mr-1">主方:</span>
每天
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '— 次' }}
· 处方开立
{{
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
? detailPrescription.usage_days + ' 天'
: '— 天'
}}
</div>
<div v-if="detailAuxUsage">
<span class="text-gray-500 mr-1">辅方:</span>
每天
{{ detailAuxUsage.times_per_day ? detailAuxUsage.times_per_day + ' 次' : '— 次' }}
· 处方开立
{{
detailAuxUsage.usage_days != null && Number(detailAuxUsage.usage_days) > 0
? detailAuxUsage.usage_days + ' 天'
: '— 天'
}}
</div>
</template>
<template v-else>
<div>
<span class="text-gray-500">每天次数:</span>
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
</div>
<div>
<span class="text-gray-500">处方开立:</span>
{{
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
? detailPrescription.usage_days + ' 天'
: '—'
}}
</div>
</template>
<div>
<span class="text-gray-500">订单设置:</span>
{{
detailData.medication_days != null &&
String(detailData.medication_days).trim() !== ''
? detailData.medication_days + ' 天'
: '—'
}}
</div>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="detailDietaryText" label="忌口" :span="2">
{{ detailDietaryText }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">
{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<div v-if="!readonly && detailHerbsVisible" class="mt-4">
<div class="text-xs font-medium text-gray-500 mb-2 px-1">药材明细</div>
<el-table v-if="detailRxHerbs.length" :data="detailRxHerbs" size="small" border stripe max-height="260">
<el-table-column type="index" label="#" width="48" align="center" />
<el-table-column prop="name" label="药名" min-width="120" show-overflow-tooltip />
<el-table-column prop="dosage" label="用量(g)" width="96" align="right" />
</el-table>
<el-empty v-else description="无处方药材明细" :image-size="64" />
</div>
</template>
<el-empty v-else description="无处方数据" :image-size="72" />
</el-card>
<!-- 右侧:未关联 + 已关联支付单 -->
<div class="flex flex-col gap-4 h-full">
<!-- 未关联的收款记录 -->
<el-card shadow="never" class="po-panel border-gray-100">
<template #header>
<div class="flex items-center justify-between">
<span class="font-medium text-[15px]">未关联的收款记录</span>
<span class="text-xs text-gray-400">本诊单下未被占用的支付单</span>
</div>
</template>
<el-table
v-if="detailUnlinkedPayOrders.length"
:data="detailUnlinkedPayOrders"
size="small"
border
stripe
class="po-pay-table"
max-height="280"
>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
<el-table-column label="类型" width="90">
<template #default="{ row }">
{{ row.order_type_desc || feeTypeText(row.order_type) }}
</template>
</el-table-column>
<el-table-column label="金额" width="86" align="right">
<template #default="{ row }">
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="!readonly && row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="方式" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ formatPayOrderSource(row) }}
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
<el-table-column label="创建时间" min-width="136">
<template #default="{ row }">
{{ formatOrderTime(row.create_time) }}
</template>
</el-table-column>
</el-table>
<el-empty v-else description="无未关联的收款单" :image-size="60" />
</el-card>
<!-- 已关联的收款记录 -->
<el-card shadow="never" class="po-panel po-panel-pay border-gray-100 relative overflow-hidden">
<!-- 支付单审核印章 -->
<div v-if="detailData.payment_slip_audit_status === 1" class="audit-stamp stamp-pass">
<div class="stamp-inner">审核通过</div>
</div>
<div v-if="detailData.payment_slip_audit_status === 2" class="audit-stamp stamp-reject">
<div class="stamp-inner">已驳回</div>
</div>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-[15px]">关联收款记录</span>
<span v-if="detailData.creator_name" class="text-xs text-gray-500">
创建人:<span class="text-primary font-medium">{{ detailData.creator_name }}</span>
</span>
</div>
</div>
</template>
<el-table
v-if="detailLinkedPayOrders.length"
:data="detailLinkedPayOrders"
size="small"
border
stripe
class="po-pay-table"
max-height="280"
>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
<el-table-column label="类型" width="90">
<template #default="{ row }">
{{ row.order_type_desc || feeTypeText(row.order_type) }}
</template>
</el-table-column>
<el-table-column label="金额" width="86" align="right">
<template #default="{ row }">
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="!readonly && row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="方式" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ formatPayOrderSource(row) }}
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
<el-table-column label="创建时间" min-width="136">
<template #default="{ row }">
{{ formatOrderTime(row.create_time) }}
</template>
</el-table-column>
</el-table>
<el-empty v-else description="未关联收款单" :image-size="60" />
</el-card>
</div>
</div>
<!-- 底部:履约与收货 -->
<el-card shadow="never" class="po-panel po-panel-biz border-gray-100 mb-4">
<template #header>
<span class="font-medium text-[15px]">履约与收货信息</span>
</template>
<el-descriptions :column="3" border size="small" class="po-desc">
<el-descriptions-item label="诊单ID" :span="3">
<span class="font-mono">{{ detailData.diagnosis_id }}</span>
<el-button
v-if="!readonly && Number(detailData.diagnosis_id) > 0"
type="primary"
link
size="small"
class="ml-2 align-baseline"
@click="emit('view-patient')"
>
查看患者信息
</el-button>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="1">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
<el-descriptions-item label="业务订单创建人" :span="2">
<div v-if="Number(detailData.creator_id) > 0 || detailData.creator_name" class="text-[13px] space-y-1">
<div>
<span class="font-medium text-gray-900">{{ detailData.creator_name || '—' }}</span>
<span v-if="!readonly && Number(detailData.creator_id) > 0" class="text-gray-500 ml-2 font-mono text-xs"
>ID {{ detailData.creator_id }}</span
>
</div>
<div v-if="detailOrderCreatorMetaLine" class="text-gray-600 text-xs leading-relaxed">
{{ detailOrderCreatorMetaLine }}
</div>
</div>
<span v-else class="text-gray-400">—</span>
</el-descriptions-item>
<el-descriptions-item label="创建人所属部门" :span="3">
<div v-if="detailOrderCreatorDeptBreadcrumbs.length" class="space-y-1.5">
<div
v-for="(segments, idx) in detailOrderCreatorDeptBreadcrumbs"
:key="idx"
class="min-w-0"
>
<el-breadcrumb
v-if="segments.length"
class="po-diagnosis-creator-dept-breadcrumb text-[13px] leading-relaxed"
separator="/"
>
<el-breadcrumb-item v-for="(name, j) in segments" :key="j">
<span class="text-gray-800 break-all">{{ name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<span v-else class="text-gray-400">—</span>
</el-descriptions-item>
<el-descriptions-item label="收货人">{{ detailData.recipient_name }}</el-descriptions-item>
<el-descriptions-item v-if="!readonly" 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="detailLinkedAppointmentChannelText" class="text-gray-500"
>渠道:{{ detailLinkedAppointmentChannelText }}</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>
<el-descriptions-item label="用药疗程">{{ detailData.medication_days ? detailData.medication_days + ' 天' : '—' }}</el-descriptions-item>
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item v-if="showInternalCost" label="内部成本">
¥{{ detailData.internal_cost }}
</el-descriptions-item>
<el-descriptions-item label="快递单号" :span="showInternalCost ? 1 : 2">{{ detailData.tracking_number || '—' }}</el-descriptions-item>
<el-descriptions-item label="快递公司">
{{ expressCompanyLabel(detailData.express_company) }}
</el-descriptions-item>
<el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item>
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
<el-descriptions-item label="处方审核意见" :span="3">
<span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span>
</el-descriptions-item>
<el-descriptions-item label="支付审核意见" :span="3">
<span class="po-audit-remark">{{ detailData.payment_slip_audit_remark || '—' }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 物流轨迹 -->
<el-card
v-if="String(detailData.tracking_number || '').trim()"
shadow="never"
class="po-panel po-panel-logistics"
:class="
!readonly && detailLogisticsUrgent.show
? '!border-2 !border-red-500 shadow-md shadow-red-500/10'
: 'border-gray-100'
"
>
<template #header>
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-[15px]">物流轨迹</span>
<span class="text-xs text-gray-400 font-mono">{{ detailData.tracking_number }}</span>
<el-tag
v-if="!readonly && detailLogisticsUrgent.show"
type="danger"
effect="dark"
size="small"
>
拒收/退回 · 待处理
</el-tag>
</div>
</template>
<p v-if="!readonly" class="po-logistics-tip text-gray-400 text-xs mb-3 font-normal">
优先读库,无记录时走快递100。顺丰会校验单号+电话(快递100要求完整手机号更易通过);默认预填订单收货手机,若仍提示验证码错误请改成与顺丰面单一致的号码。
</p>
<div class="flex flex-wrap items-center gap-3 mb-4">
<el-input
v-model="logisticsTracePhoneTail"
clearable
maxlength="20"
placeholder="收件手机(建议11位,与面单一致)"
class="w-[180px]"
@keyup.enter="fetchLogisticsTrace"
/>
<el-select v-model="detailLogisticsExpress" placeholder="承运商" style="width: 140px">
<el-option label="自动识别" value="auto" />
<el-option label="顺丰速运" value="sf" />
<el-option label="京东快递" value="jd" />
<el-option label="极兔速递" value="jt" />
</el-select>
<el-button
v-perms="['tcm.prescriptionOrder/logisticsTrace']"
type="primary"
plain
size="default"
:loading="logisticsTraceLoading"
@click="fetchLogisticsTrace"
>
<template #icon><el-icon><Refresh /></el-icon></template>
刷新轨迹
</el-button>
<el-button
v-if="!readonly"
v-perms="['tcm.prescriptionOrder/logisticsTrace']"
type="warning"
plain
size="default"
:loading="jdLogisticsUpdating"
@click="updateJdLogistics"
>
<template #icon><el-icon><Van /></el-icon></template>
京东接口更新
</el-button>
<div v-if="!readonly" class="ml-2 flex items-center gap-4 border-l pl-4 border-gray-200">
<el-link :href="detailOfficialUrls.sf" target="_blank" type="primary" :underline="false" class="text-[13px]">顺丰官网</el-link>
<el-link :href="detailOfficialUrls.jd" target="_blank" type="primary" :underline="false" class="text-[13px]">京东物流</el-link>
<el-link :href="detailOfficialUrls.jt" target="_blank" type="primary" :underline="false" class="text-[13px]">极兔</el-link>
</div>
</div>
<el-alert
v-if="!readonly && String(logisticsTracePayload?.hint || '').trim()"
:title="String(logisticsTracePayload?.hint || '')"
type="info"
:closable="false"
class="mb-4"
show-icon
/>
<div v-if="logisticsTraceLoading && !logisticsTracePayload" class="py-6 text-center text-sm text-gray-500">
<el-icon class="is-loading mr-1"><Loading /></el-icon>正在加载物流轨迹...
</div>
<div v-else-if="logisticsTracePayload" class="logistics-content bg-gray-50/50 p-4 rounded-md">
<div v-if="logisticsTracePayload?.state_text" class="text-sm mb-4 font-medium flex items-center gap-2">
<span class="text-gray-800">状态:{{ logisticsTracePayload.state_text }}</span>
<el-tag v-if="logisticsTracePayload?.carrier_label" size="small" type="info">{{ logisticsTracePayload.carrier_label }}</el-tag>
<span v-if="!readonly && logisticsTracePayload?.source" class="text-xs text-gray-400 ml-1">
[{{ logisticsTracePayload.source === 'database' ? '数据库缓存' : 'API拉取' }}]
</span>
<span v-if="!readonly && logisticsTracePayload?.last_query_time" class="text-xs text-gray-400">
更新时间:{{ logisticsTracePayload.last_query_time }}
</span>
</div>
<el-timeline v-if="logisticsTraceList.length" class="mt-2 pl-2">
<el-timeline-item
v-for="(t, idx) in logisticsTraceList"
:key="idx"
:type="idx === 0 ? 'primary' : 'info'"
:hollow="idx !== 0"
:timestamp="t.time"
placement="top"
>
<span
:class="[
idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500',
!readonly && logisticsTraceLineUrgent(t.context)
? '!text-red-700 font-semibold bg-red-50 px-1.5 py-0.5 rounded'
: ''
]"
>{{ t.context }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
</div>
</el-card>
<!-- 操作日志 -->
<el-card
v-perms="['tcm.prescriptionOrder/logs']"
shadow="never"
class="po-panel border-gray-100 mt-4"
>
<template #header>
<span class="font-medium text-[15px]">操作日志</span>
</template>
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
<el-timeline-item
v-for="(log, idx) in detailLogs"
:key="log.id"
:type="idx === 0 ? 'primary' : 'info'"
:hollow="idx !== 0"
:timestamp="formatTime(log.create_time)"
placement="top"
>
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-[13px]">{{ log.admin_name }}</span>
<el-tag size="small" type="info">{{ logActionText(log.action) }}</el-tag>
</div>
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ log.summary }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无操作日志" :image-size="64" />
</el-card>
</div>
</el-drawer>
</template>
<script lang="ts" setup name="PrescriptionOrderDetailDrawer">
import { computed, onMounted, ref } from 'vue'
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
import {
prescriptionOrderDetail,
prescriptionOrderLogs,
prescriptionOrderLogisticsTrace,
prescriptionOrderLogisticsJdUpdate,
prescriptionOrderPaidPayOrders
} from '@/api/tcm'
import { getDictData } from '@/api/app'
import feedback from '@/utils/feedback'
import useUserStore from '@/stores/modules/user'
import {
TCM_ASSISTANT_ROLE_ID,
PRESCRIPTION_AUDIT_ROLE_IDS,
formatTime,
formatMoney,
formatOrderTime,
feeTypeText,
fulfillmentText,
orderStatusText,
payOrderStatusTag,
consumerRxAuditText,
consumerRxAuditTag,
expressCompanyLabel,
logActionText,
formatPayOrderSource,
normalizeBizPhone,
recipientVsPrescriptionPhoneMismatch,
normalizeSlipHerbs,
normalizeSlipAuxUsageForm,
prescriptionHasAuxFormula,
logisticsTraceLineUrgent,
analyzeLogisticsPayloadUrgent,
parseLogisticsTracePayload,
canUpdateAmount,
formatDietaryTaboo
} from './prescription-order-utils'
const props = withDefaults(
defineProps<{
/** 只读模式:隐藏修改金额、改患者信息、查看处方笺、测试价格、查看患者信息等操作入口 */
readonly?: boolean
/** 「测试价格」按钮 loading(由父页面甘草预览流程驱动) */
gancaoPreviewLoading?: boolean
/** 嵌套在其他抽屉内时需要 append-to-body */
appendToBody?: boolean
}>(),
{
readonly: false,
gancaoPreviewLoading: false,
appendToBody: false
}
)
defineSlots<{
/** 抽屉头部左侧扩展(完整版的发货类型切换等) */
'header-extra'?: (props: { detail: any }) => any
/** 抽屉头部右侧操作按钮(审核/发货/退款等) */
'header-actions'?: (props: { detail: any }) => any
}>()
const emit = defineEmits<{
(e: 'update-amount'): void
(e: 'patch-rx-patient'): void
(e: 'view-prescription'): void
(e: 'test-gancao-preview'): void
(e: 'view-patient'): void
}>()
const userStore = useUserStore()
/**
* 是否展示「处方审核」相关字段:纯医助(含角色 2 且不具备处方审核角色)不展示剂数/用法、处方笺等
*/
const showPrescriptionAuditFilter = computed(() => {
const u = userStore.userInfo
if (!u || Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
if (!ids.includes(TCM_ASSISTANT_ROLE_ID)) return true
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
})
// ─── 详情数据 ───
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<Record<string, any> | null>(null)
const detailLogs = ref<any[]>([])
const detailUnlinkedPayOrders = ref<any[]>([])
const detailPrescription = computed(() => {
const p = detailData.value?.prescription
return p && typeof p === 'object' ? p : null
})
const detailDietaryText = computed(() => formatDietaryTaboo(detailPrescription.value?.dietary_taboo))
const detailLinkedPayOrders = computed(() => {
const d = detailData.value
if (!d) return []
const arr = d.linked_pay_orders
return Array.isArray(arr) ? arr : []
})
/** 需代收:优先用关联时记录的固定快照值;历史单(无快照)回退实时计算 总金额 − 关联已付总额 */
const detailAgencyToCollect = computed(() => {
const d = detailData.value
if (!d) return 0
const snap = d.agency_collect_amount
if (snap !== null && snap !== undefined && snap !== '') {
return Number(snap) || 0
}
const total = Number(d.amount) || 0
const paid = Number(d.linked_pay_paid_total) || 0
return Math.round((total - paid) * 100) / 100
})
// --- 订单可视化审批履约流程逻辑 ---
const workflowActiveStep = computed(() => {
if (!detailData.value) return 0
const fs = Number(detailData.value.fulfillment_status)
const rx = Number(detailData.value.prescription_audit_status)
const pay = Number(detailData.value.payment_slip_audit_status)
if (fs === 3 || [8, 9, 10, 11, 12].includes(fs)) return 5 // 订单完成 / 业务结案
if (fs === 6) return 4 // 已签收(仍停留在"已发货/送达"环节)
if (fs === 5) return 4 // 已发货
if (fs === 7) return 4 // 进行中
if (fs === 4) { // 已取消
if (rx === 2) return 1
if (pay === 2) return 2
return 3
}
if (fs === 2) return 3 // 履约中
// fs === 1 (待双审通过)
if (rx === 0 || rx === 2) return 1
if (pay === 0 || pay === 2) return 2
return 3
})
const workflowRxStatus = computed(() => {
const rx = Number(detailData.value?.prescription_audit_status)
if (rx === 1) return 'success'
if (rx === 2) return 'error'
if (workflowActiveStep.value === 1) return 'process'
return 'wait'
})
const workflowPayStatus = computed(() => {
const pay = Number(detailData.value?.payment_slip_audit_status)
if (pay === 1) return 'success'
if (pay === 2) return 'error'
if (workflowActiveStep.value === 2) return 'process'
return 'wait'
})
const workflowFulfillStatus = computed(() => {
const fs = Number(detailData.value?.fulfillment_status)
if (fs === 3 || fs === 5 || fs === 6 || fs === 8 || fs === 11 || fs === 12) return 'success'
if (fs === 4 || fs === 9 || fs === 10) return 'error'
if (fs === 7) return 'process'
if (workflowActiveStep.value === 3) return 'process'
return 'wait'
})
// --- end 可视化流程逻辑 ---
/** 详情抽屉:业务收货手机 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 || '—'
})
/** 详情:业务订单创建人部门面包屑(后端 order_creator_dept_path / diagnosis_creator_dept_path */
const detailOrderCreatorDeptBreadcrumbs = computed(() => {
const raw = String(
(detailData.value as Record<string, unknown> | null)?.order_creator_dept_path ??
(detailData.value as Record<string, unknown> | null)?.diagnosis_creator_dept_path ??
''
).trim()
if (!raw) {
return [] as string[][]
}
return raw
.split('')
.map((s) => s.trim())
.filter(Boolean)
.map((p) => {
if (p.includes(' / ')) {
return p
.split(' / ')
.map((x) => x.trim())
.filter(Boolean)
}
return p
.split('/')
.map((x) => x.trim())
.filter(Boolean)
})
.filter((segs) => segs.length > 0)
})
const detailOrderCreatorMetaLine = computed(() => {
const d = detailData.value as Record<string, unknown> | null
if (!d) return ''
const account = String(d.creator_account ?? '').trim()
const mobile = String(d.creator_mobile ?? '').trim()
const parts: string[] = []
if (account) parts.push(`登录账号:${account}`)
if (mobile) parts.push(`手机:${mobile}`)
return parts.join(' · ')
})
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 ''
})
/** 渠道展示:后端 linked_appointment.channel_source_desc(字典名),缺省回退原值 */
const detailLinkedAppointmentChannelText = computed(() => {
const a = detailLinkedAppointment.value
if (!a) return ''
const desc = String(a.channel_source_desc ?? '').trim()
if (desc) return desc
return String(a.channel_source ?? '').trim()
})
const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs))
const detailHasAuxHerbs = computed(() => prescriptionHasAuxFormula(detailPrescription.value as any))
/** 详情:辅方用法(aux_usage JSON 经规范化后的对象,与处方类型一致) */
const detailAuxUsage = computed(() => {
const rx = detailPrescription.value as any
if (!rx || !prescriptionHasAuxFormula(rx)) return null
const raw = rx.aux_usage
if (raw == null || raw === '' || (Array.isArray(raw) && raw.length === 0)) return null
return normalizeSlipAuxUsageForm(raw, rx.prescription_type || '浓缩水丸')
})
/** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */
const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false)
/** 内部成本(财务字段):仅完整版展示 */
const showInternalCost = computed(
() => !props.readonly && detailData.value?.internal_cost != null && detailData.value?.internal_cost !== ''
)
// 完整收货地址(省市区 + 详细地址)
const detailFullAddress = computed(() => {
const d = detailData.value
if (!d) return '—'
const parts = []
if (d.shipping_province) parts.push(d.shipping_province)
if (d.shipping_city) parts.push(d.shipping_city)
if (d.shipping_district) parts.push(d.shipping_district)
if (d.shipping_address) parts.push(d.shipping_address)
return parts.length > 0 ? parts.join(' ') : '—'
})
// ─── 服务套餐字典 ───
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
async function loadServicePackageOptions() {
try {
const data: any = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
} catch {
servicePackageOptions.value = []
}
}
function formatServicePackage(value: any): string {
if (!value) return '—'
let packages: string[] = []
if (Array.isArray(value)) {
packages = value
} else if (typeof value === 'string') {
packages = value.split(',').filter((v) => v.trim() !== '')
}
if (packages.length === 0) return '—'
const names = packages.map((val) => {
const option = servicePackageOptions.value.find((opt) => opt.value === val)
return option ? option.name : val
})
return names.join('、')
}
onMounted(() => {
loadServicePackageOptions()
})
// ─── 操作日志 ───
function canViewPrescriptionOrderLogs() {
const p = userStore.perms || []
return p.includes('*') || p.includes('tcm.prescriptionOrder/logs')
}
async function fetchLogs(id: number) {
if (!canViewPrescriptionOrderLogs()) {
detailLogs.value = []
return
}
try {
const res: any = await prescriptionOrderLogs({ id })
detailLogs.value = res?.data ?? res ?? []
} catch {
detailLogs.value = []
}
}
// ─── 未关联支付单 ───
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
if (!diagnosisId) {
detailUnlinkedPayOrders.value = []
return
}
try {
const res: any = await prescriptionOrderPaidPayOrders({
diagnosis_id: diagnosisId,
prescription_order_id: prescriptionOrderId
})
const lists = res?.lists ?? res?.data?.lists ?? []
// 列表已由服务端限制;此处再过滤已关联项
const filtered = Array.isArray(lists)
? lists.filter((item: any) => !linkedIds.includes(item.id))
: []
detailUnlinkedPayOrders.value = filtered
} catch {
detailUnlinkedPayOrders.value = []
}
}
// ─── 物流轨迹 ───
const detailLogisticsExpress = ref<string>('auto')
const logisticsTraceLoading = ref(false)
const jdLogisticsUpdating = ref(false)
const logisticsTracePayload = ref<Record<string, any> | null>(null)
/** 顺丰/快递100:与运单一致的收件手机后四位(可覆盖订单收货手机) */
const logisticsTracePhoneTail = ref('')
/** 物流轨迹请求序号:同手机多运单或快速切换订单时,丢弃过期的异步响应 */
let logisticsTraceRequestSeq = 0
type LogisticsTraceRequestContext = {
requestSeq: number
orderId: number
trackingNumber: string
}
function bumpLogisticsTraceRequestToken() {
logisticsTraceRequestSeq += 1
logisticsTracePayload.value = null
}
function isCurrentLogisticsTraceContext(ctx: LogisticsTraceRequestContext): boolean {
if (ctx.requestSeq !== logisticsTraceRequestSeq) return false
if (Number(detailData.value?.id) !== ctx.orderId) return false
return String(detailData.value?.tracking_number || '').trim() === ctx.trackingNumber
}
const detailOfficialUrls = computed(() => {
const n = String(detailData.value?.tracking_number || '').trim()
if (!n) {
return { sf: 'javascript:void(0)', jd: 'javascript:void(0)', jt: 'javascript:void(0)' }
}
const enc = encodeURIComponent(n)
return {
sf: `https://www.sf-express.com/cn/sc/dynamic_function/waybill/#search/bill-number/${enc}`,
jd: `https://www.jdl.com/#/trackQuery?waybillCode=${enc}`,
jt: `https://www.jtexpress.com.cn/index/query/gzquery.html?bills=${enc}`
}
})
const logisticsTraceList = computed(() => {
const p = logisticsTracePayload.value
if (!p || !Array.isArray(p.traces)) return []
return p.traces as Array<{ time: string; context: string }>
})
/** 详情抽屉:轨迹/状态中是否出现拒收等字样,用于顶部与物流卡片强提示 */
const detailLogisticsUrgent = computed(() => analyzeLogisticsPayloadUrgent(logisticsTracePayload.value))
async function fetchLogisticsTrace() {
const orderId = Number(detailData.value?.id)
const trackingNumber = String(detailData.value?.tracking_number || '').trim()
if (!orderId || !trackingNumber) return
const requestSeq = ++logisticsTraceRequestSeq
const ctx: LogisticsTraceRequestContext = { requestSeq, orderId, trackingNumber }
logisticsTraceLoading.value = true
try {
const digits = String(logisticsTracePhoneTail.value || '').replace(/\D/g, '')
const params: { id: number; express_company?: string; phone_tail?: string } = {
id: orderId,
express_company: detailLogisticsExpress.value
}
if (digits.length >= 4) {
params.phone_tail = digits
}
const res: any = await prescriptionOrderLogisticsTrace(params)
if (!isCurrentLogisticsTraceContext(ctx)) return
logisticsTracePayload.value = parseLogisticsTracePayload(res, trackingNumber, orderId)
} catch {
if (!isCurrentLogisticsTraceContext(ctx)) return
logisticsTracePayload.value = null
} finally {
if (isCurrentLogisticsTraceContext(ctx)) {
logisticsTraceLoading.value = false
}
}
}
// 直接请求京东官方物流接口,刷新并落库轨迹后重新读取展示
async function updateJdLogistics() {
const orderId = Number(detailData.value?.id)
const trackingNumber = String(detailData.value?.tracking_number || '').trim()
if (!orderId || !trackingNumber) {
feedback.msgWarning('当前订单未填写快递单号')
return
}
jdLogisticsUpdating.value = true
try {
const res: any = await prescriptionOrderLogisticsJdUpdate({ id: orderId })
feedback.msgSuccess(res?.message || '京东物流轨迹已更新')
await fetchLogisticsTrace()
} catch (e: any) {
feedback.msgError(e?.message || '京东物流更新失败')
} finally {
jdLogisticsUpdating.value = false
}
}
// ─── 打开 / 刷新 ───
async function open(id: number) {
// 显式彻底清空缓存,防止前一次弹窗的数据残留
detailData.value = null
detailUnlinkedPayOrders.value = []
bumpLogisticsTraceRequestToken()
detailLogisticsExpress.value = 'auto'
logisticsTracePhoneTail.value = ''
detailLogs.value = []
detailVisible.value = true
detailLoading.value = true
try {
const res: any = await prescriptionOrderDetail({ id })
const d = res?.data ?? res ?? null
detailData.value = d
if (d) {
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
const dig = String(d.recipient_phone || '').replace(/\D/g, '')
logisticsTracePhoneTail.value = dig.length >= 4 ? dig : ''
if (String(d.tracking_number || '').trim()) {
fetchLogisticsTrace()
}
fetchLogs(id)
// 加载未关联的支付单
const diagId = d.diagnosis_id
const linkedIds = d.pay_order_ids || []
if (diagId) {
void loadDetailUnlinkedPayOrders(diagId, id, linkedIds)
}
}
} catch (e: any) {
feedback.msgError(e?.message || '加载失败')
detailVisible.value = false
} finally {
detailLoading.value = false
}
}
/** 静默刷新当前详情(含日志/未关联支付单;快递单号变化时重拉轨迹),供父页面操作成功后调用 */
async function refresh() {
const id = Number(detailData.value?.id)
if (!id) return
try {
const res: any = await prescriptionOrderDetail({ id })
const d = res?.data ?? res ?? null
if (!d) return
const prevTracking = String(detailData.value?.tracking_number || '').trim()
detailData.value = d
const nextTracking = String(d.tracking_number || '').trim()
if (nextTracking && nextTracking !== prevTracking) {
bumpLogisticsTraceRequestToken()
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
void fetchLogisticsTrace()
}
void fetchLogs(id)
const diagId = d.diagnosis_id
if (diagId) {
void loadDetailUnlinkedPayOrders(diagId, id, d.pay_order_ids || [])
}
} catch {
/* 静默失败,避免打断当前抽屉 */
}
}
/** 抽屉开着且正展示指定订单时刷新(父页面列表操作后的常用模式) */
async function refreshIfCurrent(id: number | string | undefined) {
const n = Number(id)
if (!detailVisible.value || !(n > 0) || Number(detailData.value?.id) !== n) return
await refresh()
}
function close() {
detailVisible.value = false
}
defineExpose({
open,
refresh,
refreshIfCurrent,
close,
/** 当前详情数据(父页面操作弹窗/槽内按钮可读取,亦可原地修补字段) */
detail: detailData,
visible: detailVisible
})
</script>
<style scoped lang="scss">
.po-detail-drawer :deep(.el-drawer__header) {
margin-bottom: 0;
padding: 16px 24px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.stat-card {
border-radius: 8px;
}
.stat-title {
font-weight: 500;
}
.po-panel {
border-radius: 8px;
transition: all 0.3s;
}
.po-panel :deep(.el-card__header) {
padding: 14px 16px;
background-color: var(--el-bg-color-page);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.po-panel :deep(.el-card__body) {
padding: 16px;
}
.po-desc :deep(.el-descriptions__label) {
width: 120px;
color: var(--el-text-color-regular);
}
.po-audit-remark {
color: var(--el-color-danger);
font-weight: 600;
white-space: pre-wrap;
word-break: break-word;
}
/* 动态审核印章 */
.audit-stamp {
position: absolute;
top: 18px;
right: -14px;
width: 72px;
height: 72px;
border: 3px solid currentColor;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(20deg);
opacity: 0.8;
pointer-events: none;
z-index: 10;
user-select: none;
font-weight: 700;
font-size: 13px;
letter-spacing: 1px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
.audit-stamp::after {
content: '';
position: absolute;
top: 4px;
left: 4px;
right: 4px;
bottom: 4px;
border: 1px double currentColor;
border-radius: 50%;
opacity: 0.6;
}
.audit-stamp .stamp-inner {
text-align: center;
line-height: 1.1;
}
.stamp-pass {
color: var(--el-color-success);
}
.stamp-reject {
color: var(--el-color-danger);
}
.po-diagnosis-creator-dept-breadcrumb {
:deep(.el-breadcrumb__item) {
display: inline-flex;
float: none;
}
:deep(.el-breadcrumb__separator) {
margin: 0 2px 0 4px;
}
}
</style>