1830 lines
87 KiB
Vue
1830 lines
87 KiB
Vue
<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>
|
||
<template #label>
|
||
<div class="flex items-center gap-1">
|
||
<span>服用方式</span>
|
||
<el-button
|
||
v-if="
|
||
!readonly &&
|
||
detailData.prescription_id &&
|
||
detailPrescription &&
|
||
!String(detailData.prescription_detail_error || '').trim()
|
||
"
|
||
v-perms="['tcm.prescriptionOrder/patchPrescriptionUsage']"
|
||
type="primary"
|
||
size="small"
|
||
link
|
||
@click="openPatchUsageDialog"
|
||
>
|
||
修改
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
<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="服务套餐">{{ detailServicePackageText }}</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>
|
||
<div class="flex items-center justify-between gap-2">
|
||
<span class="font-medium text-[15px]">操作日志</span>
|
||
<el-button
|
||
v-if="canAddPrescriptionOrderLog()"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="openAddLogDialog"
|
||
>
|
||
新增日志
|
||
</el-button>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- 新增操作日志 -->
|
||
<el-dialog
|
||
v-model="addLogVisible"
|
||
title="新增操作日志"
|
||
width="520px"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
append-to-body
|
||
@closed="resetAddLogForm"
|
||
>
|
||
<el-form ref="addLogFormRef" :model="addLogForm" :rules="addLogRules" label-width="108px">
|
||
<el-form-item label="日志内容" prop="summary">
|
||
<el-input
|
||
v-model="addLogForm.summary"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="记录本次操作说明、沟通结果等"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="canSetRxAuditOnAddLog" label="处方审核">
|
||
<el-select v-model="addLogForm.prescription_audit_status" class="w-full" clearable placeholder="不修改">
|
||
<el-option label="待审核" :value="0" />
|
||
<el-option label="已通过" :value="1" />
|
||
<el-option label="已驳回" :value="2" />
|
||
</el-select>
|
||
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
|
||
当前:{{ auditStatusText(detailData.prescription_audit_status) }}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="canSetRxAuditOnAddLog && addLogForm.prescription_audit_status !== '' && addLogForm.prescription_audit_status !== null && addLogForm.prescription_audit_status !== undefined"
|
||
label="处方审核意见"
|
||
>
|
||
<el-input
|
||
v-model="addLogForm.prescription_audit_remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="选填"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="canSetPayAuditOnAddLog" label="支付单审核">
|
||
<el-select v-model="addLogForm.payment_slip_audit_status" class="w-full" clearable placeholder="不修改">
|
||
<el-option label="待审核" :value="0" />
|
||
<el-option label="已通过" :value="1" />
|
||
<el-option label="已驳回" :value="2" />
|
||
</el-select>
|
||
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
|
||
当前:{{ auditStatusText(detailData.payment_slip_audit_status) }}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="canSetPayAuditOnAddLog && addLogForm.payment_slip_audit_status !== '' && addLogForm.payment_slip_audit_status !== null && addLogForm.payment_slip_audit_status !== undefined"
|
||
label="支付审核意见"
|
||
>
|
||
<el-input
|
||
v-model="addLogForm.payment_slip_audit_remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="选填"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="addLogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="addLogSaving" @click="submitAddLog">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 修改服用参数:主方 / 辅方 / 订单设置 -->
|
||
<el-dialog
|
||
v-model="patchUsageVisible"
|
||
title="修改服用参数"
|
||
width="480px"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
@closed="resetPatchUsageForm"
|
||
>
|
||
<el-form
|
||
ref="patchUsageFormRef"
|
||
:model="patchUsageForm"
|
||
:rules="patchUsageRules"
|
||
label-width="108px"
|
||
>
|
||
<div v-if="detailHasAuxHerbs" class="text-xs font-medium text-gray-500 mb-3">主方</div>
|
||
<el-form-item label="每天次数" prop="times_per_day">
|
||
<el-input-number
|
||
v-model="patchUsageForm.times_per_day"
|
||
:min="1"
|
||
:max="6"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="处方开立" prop="usage_days">
|
||
<div class="flex items-center gap-1 w-full">
|
||
<el-input-number
|
||
v-model="patchUsageForm.usage_days"
|
||
:min="1"
|
||
:max="999"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="flex-1 min-w-0"
|
||
/>
|
||
<span class="text-gray-500 shrink-0">天</span>
|
||
</div>
|
||
</el-form-item>
|
||
<template v-if="detailHasAuxHerbs">
|
||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">辅方</div>
|
||
<el-form-item label="每天次数" prop="aux_times_per_day">
|
||
<el-input-number
|
||
v-model="patchUsageForm.aux_times_per_day"
|
||
:min="1"
|
||
:max="6"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="处方开立" prop="aux_usage_days">
|
||
<div class="flex items-center gap-1 w-full">
|
||
<el-input-number
|
||
v-model="patchUsageForm.aux_usage_days"
|
||
:min="1"
|
||
:max="999"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="flex-1 min-w-0"
|
||
/>
|
||
<span class="text-gray-500 shrink-0">天</span>
|
||
</div>
|
||
</el-form-item>
|
||
</template>
|
||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">订单设置</div>
|
||
<el-form-item label="服用天数" prop="medication_days">
|
||
<div class="flex items-center gap-1 w-full">
|
||
<el-input-number
|
||
v-model="patchUsageForm.medication_days"
|
||
:min="1"
|
||
:max="999"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="flex-1 min-w-0"
|
||
/>
|
||
<span class="text-gray-500 shrink-0">天</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="patchUsageVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="patchUsageSaving" @click="submitPatchUsage">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script lang="ts" setup name="PrescriptionOrderDetailDrawer">
|
||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
|
||
import {
|
||
prescriptionOrderDetail,
|
||
prescriptionOrderLogs,
|
||
prescriptionOrderAddLog,
|
||
prescriptionOrderLogisticsTrace,
|
||
prescriptionOrderLogisticsJdUpdate,
|
||
prescriptionOrderPaidPayOrders,
|
||
prescriptionOrderPatchPrescriptionUsage
|
||
} 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,
|
||
auditStatusText,
|
||
formatPayOrderSource,
|
||
normalizeBizPhone,
|
||
recipientVsPrescriptionPhoneMismatch,
|
||
normalizeSlipHerbs,
|
||
normalizeSlipAuxUsageForm,
|
||
prescriptionHasAuxFormula,
|
||
logisticsTraceLineUrgent,
|
||
analyzeLogisticsPayloadUrgent,
|
||
parseLogisticsTracePayload,
|
||
canUpdateAmount,
|
||
formatDietaryTaboo,
|
||
type ServicePackageOption,
|
||
normalizeServicePackageOptions,
|
||
formatServicePackageLabels
|
||
} 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
|
||
(e: 'detail-changed'): 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<ServicePackageOption[]>([])
|
||
|
||
async function loadServicePackageOptions() {
|
||
if (servicePackageOptions.value.length > 0) return
|
||
try {
|
||
const data: any = await getDictData({ type: 'server_order' })
|
||
const opts = normalizeServicePackageOptions(data?.server_order)
|
||
if (opts.length > 0) {
|
||
servicePackageOptions.value = opts
|
||
}
|
||
} catch {
|
||
/* 请求被同参数请求取消或失败时保留现值,open() 时会重试 */
|
||
}
|
||
}
|
||
|
||
const detailServicePackageText = computed(() =>
|
||
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
|
||
)
|
||
|
||
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 = []
|
||
}
|
||
}
|
||
|
||
function hasPerm(perm: string) {
|
||
const p = userStore.perms || []
|
||
return p.includes('*') || p.includes(perm)
|
||
}
|
||
|
||
function canAddPrescriptionOrderLog() {
|
||
return hasPerm('tcm.prescriptionOrder/addLog')
|
||
}
|
||
|
||
/** 与后端 canAuditPrescriptionOrder 同档:超管或 prescription_audit_roles */
|
||
const canSetRxAuditOnAddLog = 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)) : []
|
||
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
|
||
})
|
||
|
||
/** 与后端 canAuditPaymentSlipOrder 同档:超管或 prescription_order_payment_audit_roles 默认 0,3 */
|
||
const canSetPayAuditOnAddLog = 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)) : []
|
||
return ids.some((id) => [0, 3].includes(id))
|
||
})
|
||
|
||
const addLogVisible = ref(false)
|
||
const addLogSaving = ref(false)
|
||
const addLogFormRef = ref<FormInstance>()
|
||
const addLogForm = reactive({
|
||
summary: '',
|
||
prescription_audit_status: '' as number | '',
|
||
payment_slip_audit_status: '' as number | '',
|
||
prescription_audit_remark: '',
|
||
payment_slip_audit_remark: ''
|
||
})
|
||
const addLogRules: FormRules = {
|
||
summary: [{ required: true, message: '请填写日志内容', trigger: 'blur' }]
|
||
}
|
||
|
||
const patchUsageVisible = ref(false)
|
||
const patchUsageSaving = ref(false)
|
||
const patchUsageFormRef = ref<FormInstance>()
|
||
const patchUsageForm = reactive({
|
||
times_per_day: 3 as number | undefined,
|
||
usage_days: 7 as number | undefined,
|
||
aux_times_per_day: 3 as number | undefined,
|
||
aux_usage_days: 7 as number | undefined,
|
||
medication_days: undefined as number | undefined
|
||
})
|
||
const patchUsageRules = computed<FormRules>(() => {
|
||
const rules: FormRules = {
|
||
times_per_day: [{ required: true, message: '请填写主方每天次数', trigger: 'change' }],
|
||
usage_days: [{ required: true, message: '请填写主方开立天数', trigger: 'change' }],
|
||
medication_days: [{ required: true, message: '请填写订单服用天数', trigger: 'change' }]
|
||
}
|
||
if (detailHasAuxHerbs.value) {
|
||
rules.aux_times_per_day = [{ required: true, message: '请填写辅方每天次数', trigger: 'change' }]
|
||
rules.aux_usage_days = [{ required: true, message: '请填写辅方开立天数', trigger: 'change' }]
|
||
}
|
||
return rules
|
||
})
|
||
|
||
function openPatchUsageDialog() {
|
||
const rx = detailPrescription.value
|
||
const ord = detailData.value
|
||
if (!rx || !ord?.id || !ord.prescription_id) {
|
||
feedback.msgWarning('无处方数据')
|
||
return
|
||
}
|
||
const aux = detailAuxUsage.value
|
||
patchUsageForm.times_per_day =
|
||
Number(rx.times_per_day) > 0 ? Number(rx.times_per_day) : 3
|
||
patchUsageForm.usage_days =
|
||
Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
||
patchUsageForm.aux_times_per_day =
|
||
aux && Number(aux.times_per_day) > 0 ? Number(aux.times_per_day) : 3
|
||
patchUsageForm.aux_usage_days =
|
||
aux && Number(aux.usage_days) > 0 ? Number(aux.usage_days) : 7
|
||
const md = Number(ord.medication_days)
|
||
patchUsageForm.medication_days = md > 0 ? md : Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
||
patchUsageVisible.value = true
|
||
nextTick(() => patchUsageFormRef.value?.clearValidate())
|
||
}
|
||
|
||
function resetPatchUsageForm() {
|
||
patchUsageForm.times_per_day = 3
|
||
patchUsageForm.usage_days = 7
|
||
patchUsageForm.aux_times_per_day = 3
|
||
patchUsageForm.aux_usage_days = 7
|
||
patchUsageForm.medication_days = undefined
|
||
}
|
||
|
||
async function submitPatchUsage() {
|
||
const form = patchUsageFormRef.value
|
||
if (!form) return
|
||
try {
|
||
await form.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
const ordId = detailData.value?.id
|
||
if (!ordId) return
|
||
patchUsageSaving.value = true
|
||
try {
|
||
const payload: {
|
||
id: number
|
||
times_per_day: number
|
||
usage_days: number
|
||
medication_days: number
|
||
aux_times_per_day?: number
|
||
aux_usage_days?: number
|
||
} = {
|
||
id: ordId,
|
||
times_per_day: Number(patchUsageForm.times_per_day),
|
||
usage_days: Number(patchUsageForm.usage_days),
|
||
medication_days: Number(patchUsageForm.medication_days)
|
||
}
|
||
if (detailHasAuxHerbs.value) {
|
||
payload.aux_times_per_day = Number(patchUsageForm.aux_times_per_day)
|
||
payload.aux_usage_days = Number(patchUsageForm.aux_usage_days)
|
||
}
|
||
await prescriptionOrderPatchPrescriptionUsage(payload)
|
||
feedback.msgSuccess('保存成功')
|
||
patchUsageVisible.value = false
|
||
await refresh()
|
||
emit('detail-changed')
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
patchUsageSaving.value = false
|
||
}
|
||
}
|
||
|
||
function resetAddLogForm() {
|
||
addLogForm.summary = ''
|
||
addLogForm.prescription_audit_status = ''
|
||
addLogForm.payment_slip_audit_status = ''
|
||
addLogForm.prescription_audit_remark = ''
|
||
addLogForm.payment_slip_audit_remark = ''
|
||
addLogFormRef.value?.clearValidate()
|
||
}
|
||
|
||
function openAddLogDialog() {
|
||
if (!detailData.value?.id) return
|
||
resetAddLogForm()
|
||
const d = detailData.value
|
||
addLogForm.prescription_audit_remark = String(d.prescription_audit_remark || '')
|
||
addLogForm.payment_slip_audit_remark = String(d.payment_slip_audit_remark || '')
|
||
addLogVisible.value = true
|
||
}
|
||
|
||
async function submitAddLog() {
|
||
if (!addLogFormRef.value || !detailData.value?.id) return
|
||
await addLogFormRef.value.validate()
|
||
addLogSaving.value = true
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
id: detailData.value.id,
|
||
summary: addLogForm.summary.trim()
|
||
}
|
||
if (
|
||
canSetRxAuditOnAddLog.value &&
|
||
addLogForm.prescription_audit_status !== '' &&
|
||
addLogForm.prescription_audit_status !== null &&
|
||
addLogForm.prescription_audit_status !== undefined
|
||
) {
|
||
payload.prescription_audit_status = addLogForm.prescription_audit_status
|
||
payload.prescription_audit_remark = addLogForm.prescription_audit_remark
|
||
}
|
||
if (
|
||
canSetPayAuditOnAddLog.value &&
|
||
addLogForm.payment_slip_audit_status !== '' &&
|
||
addLogForm.payment_slip_audit_status !== null &&
|
||
addLogForm.payment_slip_audit_status !== undefined
|
||
) {
|
||
payload.payment_slip_audit_status = addLogForm.payment_slip_audit_status
|
||
payload.payment_slip_audit_remark = addLogForm.payment_slip_audit_remark
|
||
}
|
||
await prescriptionOrderAddLog(payload as any)
|
||
feedback.msgSuccess('日志已添加')
|
||
addLogVisible.value = false
|
||
await refresh()
|
||
emit('detail-changed')
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
addLogSaving.value = false
|
||
}
|
||
}
|
||
|
||
// ─── 未关联支付单 ───
|
||
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) {
|
||
// 页面级同参数字典请求会取消抽屉挂载时的那次(axios 去重取消),打开时兜底重试
|
||
void loadServicePackageOptions()
|
||
// 显式彻底清空缓存,防止前一次弹窗的数据残留
|
||
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>
|