Merge branch '20260610-long'
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* 处方业务订单——纯展示/格式化工具
|
||||||
|
*
|
||||||
|
* 唯一数据源:order_list.vue(处方业务订单)、PatientOrderList.vue(诊单业务订单 Tab)
|
||||||
|
* 与 PrescriptionOrderDetailDrawer.vue(共享详情抽屉)共用,禁止在页面内重复定义同名函数,
|
||||||
|
* 否则两个详情页面会再次出现文案/口径漂移。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 与诊间医助角色 ID 一致(server 角色表) */
|
||||||
|
export const TCM_ASSISTANT_ROLE_ID = 2
|
||||||
|
/** 与 server/config/project.php prescription_audit_roles 默认一致,可处方审核的角色 */
|
||||||
|
export const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3, 6]
|
||||||
|
|
||||||
|
export function formatTime(v: unknown) {
|
||||||
|
if (v === null || v === undefined || v === '') return '—'
|
||||||
|
if (typeof v === 'number' && v > 1e9 && v < 1e11) {
|
||||||
|
const d = new Date(v * 1000)
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMoney(v: unknown) {
|
||||||
|
const n = Number(v)
|
||||||
|
if (Number.isNaN(n)) return '—'
|
||||||
|
return n.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOrderTime(v: unknown) {
|
||||||
|
if (v === null || v === undefined || v === '') return '—'
|
||||||
|
if (typeof v === 'string' && String(v).includes('-')) return String(v)
|
||||||
|
return formatTime(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function feeTypeText(t: number | undefined) {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
1: '挂号费',
|
||||||
|
2: '问诊费',
|
||||||
|
3: '药品费用',
|
||||||
|
4: '首付费用',
|
||||||
|
5: '尾款费用',
|
||||||
|
6: '其他费用',
|
||||||
|
7: '全部费用',
|
||||||
|
8: '驼奶费用'
|
||||||
|
}
|
||||||
|
return m[Number(t)] ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fulfillmentText(s: number | undefined) {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
1: '待双审通过',
|
||||||
|
2: '待发货',
|
||||||
|
3: '已完成',
|
||||||
|
4: '已取消',
|
||||||
|
5: '已发货',
|
||||||
|
6: '已签收',
|
||||||
|
7: '进行中',
|
||||||
|
8: '暂不制药',
|
||||||
|
9: '拒收',
|
||||||
|
10: '退款',
|
||||||
|
11: '保留药方',
|
||||||
|
12: '制药缓发'
|
||||||
|
}
|
||||||
|
return m[Number(s)] ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fulfillmentTagType(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||||
|
const n = Number(s)
|
||||||
|
if (n === 3) return 'success'
|
||||||
|
if (n === 6) return 'success'
|
||||||
|
if (n === 5) return 'primary'
|
||||||
|
if (n === 4) return 'danger'
|
||||||
|
if (n === 2) return 'warning'
|
||||||
|
if (n === 7) return 'warning'
|
||||||
|
if (n === 8 || n === 11 || n === 12) return 'info'
|
||||||
|
if (n === 9 || n === 10) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orderStatusText(s: number | undefined) {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
1: '待支付',
|
||||||
|
2: '已支付',
|
||||||
|
3: '已取消',
|
||||||
|
4: '已退款',
|
||||||
|
5: '待审核'
|
||||||
|
}
|
||||||
|
return m[Number(s)] ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||||
|
const n = Number(s)
|
||||||
|
if (n === 2) return 'success'
|
||||||
|
if (n === 1) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumerRxAuditText(s: number | undefined) {
|
||||||
|
const n = Number(s)
|
||||||
|
if (n === 1) return '已通过'
|
||||||
|
if (n === 2) return '已驳回'
|
||||||
|
return '待审核'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||||
|
const n = Number(s)
|
||||||
|
if (n === 1) return 'success'
|
||||||
|
if (n === 2) return 'danger'
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expressCompanyLabel(v: unknown) {
|
||||||
|
const s = String(v || '').toLowerCase()
|
||||||
|
if (s === 'sf') return '顺丰速运'
|
||||||
|
if (s === 'jd') return '京东快递'
|
||||||
|
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
|
||||||
|
return '自动识别'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logActionText(act: string) {
|
||||||
|
const m: Record<string, string> = {
|
||||||
|
create: '创建',
|
||||||
|
edit: '编辑',
|
||||||
|
audit_rx_approve: '处方审核',
|
||||||
|
audit_rx_reject: '处方审核',
|
||||||
|
audit_pay_approve: '支付审核',
|
||||||
|
audit_pay_reject: '支付审核',
|
||||||
|
fill_tracking: '填快递单',
|
||||||
|
ship: '确认发货',
|
||||||
|
withdraw: '撤销',
|
||||||
|
link_pay_order: '关联支付单',
|
||||||
|
completion_request: '完单申请',
|
||||||
|
auto_complete: '自动完成',
|
||||||
|
revoke_rx_audit: '撤回处方审核',
|
||||||
|
revoke_pay_audit: '撤回支付审核',
|
||||||
|
gancao_submit: '甘草下单',
|
||||||
|
patch_rx_patient: '处方患者信息',
|
||||||
|
update_amount: '修改订单金额',
|
||||||
|
complete: '完成订单',
|
||||||
|
refund: '退款'
|
||||||
|
}
|
||||||
|
return m[act] || act
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支付单来源/方式:企微对外收款、付呗等创建链路 + 支付方式回退 */
|
||||||
|
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
|
||||||
|
const createType = String(row?.create_type || '')
|
||||||
|
if (createType === 'wechat_work') return '企业微信对外收款'
|
||||||
|
if (createType === 'fubei') return '付呗'
|
||||||
|
const paymentMethod = String(row?.payment_method || '')
|
||||||
|
const methodMap: Record<string, string> = {
|
||||||
|
alipay: '支付宝',
|
||||||
|
wechat: '微信',
|
||||||
|
wechat_work: '企业微信',
|
||||||
|
fubei: '付呗',
|
||||||
|
manual: '手动确认到账'
|
||||||
|
}
|
||||||
|
if (paymentMethod && methodMap[paymentMethod]) {
|
||||||
|
return methodMap[paymentMethod]
|
||||||
|
}
|
||||||
|
return '普通订单'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBizPhone(v: unknown): string {
|
||||||
|
if (v === null || v === undefined) return ''
|
||||||
|
return String(v).replace(/\s/g, '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recipientVsPrescriptionPhoneMismatch(recipient: unknown, rxPhone: unknown): boolean {
|
||||||
|
const a = normalizeBizPhone(recipient)
|
||||||
|
const b = normalizeBizPhone(rxPhone)
|
||||||
|
if (!a || !b) return false
|
||||||
|
return a !== b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSlipHerbs(raw: unknown): Array<{ name: string; dosage: number }> {
|
||||||
|
if (!raw) return []
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.map((x: any) => ({
|
||||||
|
name: String(x?.name ?? '').trim(),
|
||||||
|
dosage: Number(x?.dosage) || 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlipFormulaType = '主方' | '辅方'
|
||||||
|
|
||||||
|
export interface SlipAuxUsageForm {
|
||||||
|
dosage_amount?: number
|
||||||
|
dosage_bag_count: number
|
||||||
|
need_decoction: boolean
|
||||||
|
bags_per_dose: number
|
||||||
|
times_per_day: number
|
||||||
|
usage_days: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSlipFormulaType(v: unknown): SlipFormulaType {
|
||||||
|
return v === '辅方' ? '辅方' : '主方'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSlipAuxUsage(prescriptionType = '浓缩水丸'): SlipAuxUsageForm {
|
||||||
|
if (prescriptionType === '饮片') {
|
||||||
|
return {
|
||||||
|
dosage_amount: 50,
|
||||||
|
dosage_bag_count: 1,
|
||||||
|
need_decoction: false,
|
||||||
|
bags_per_dose: 1,
|
||||||
|
times_per_day: 3,
|
||||||
|
usage_days: 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prescriptionType === '浓缩水丸') {
|
||||||
|
return {
|
||||||
|
dosage_amount: 5,
|
||||||
|
dosage_bag_count: 1,
|
||||||
|
need_decoction: false,
|
||||||
|
bags_per_dose: 1,
|
||||||
|
times_per_day: 3,
|
||||||
|
usage_days: 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dosage_amount: 1,
|
||||||
|
dosage_bag_count: 1,
|
||||||
|
need_decoction: false,
|
||||||
|
bags_per_dose: 1,
|
||||||
|
times_per_day: 3,
|
||||||
|
usage_days: 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSlipAuxUsageForm(raw: unknown, prescriptionType: string): SlipAuxUsageForm {
|
||||||
|
const base = defaultSlipAuxUsage(prescriptionType)
|
||||||
|
if (!raw || typeof raw !== 'object') return { ...base }
|
||||||
|
const o = raw as Record<string, unknown>
|
||||||
|
return {
|
||||||
|
dosage_amount:
|
||||||
|
o.dosage_amount !== null && o.dosage_amount !== undefined && o.dosage_amount !== ''
|
||||||
|
? Number(o.dosage_amount)
|
||||||
|
: base.dosage_amount,
|
||||||
|
dosage_bag_count: o.dosage_bag_count != null ? Number(o.dosage_bag_count) || 1 : base.dosage_bag_count,
|
||||||
|
need_decoction: o.need_decoction === 1 || o.need_decoction === true,
|
||||||
|
bags_per_dose: o.bags_per_dose != null ? Number(o.bags_per_dose) || 1 : base.bags_per_dose,
|
||||||
|
times_per_day: o.times_per_day != null ? Number(o.times_per_day) || 3 : base.times_per_day,
|
||||||
|
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否含辅方药材(仅 formula_type=辅方;医助无药材权限时 herbs 被剥离,用后端 has_aux_formula) */
|
||||||
|
export function prescriptionHasAuxFormula(rx: Record<string, unknown> | null | undefined): boolean {
|
||||||
|
if (!rx) return false
|
||||||
|
const herbs = rx.herbs
|
||||||
|
if (Array.isArray(herbs)) {
|
||||||
|
return herbs.some((h: any) => normalizeSlipFormulaType(h?.formula_type) === '辅方')
|
||||||
|
}
|
||||||
|
return Number(rx.has_aux_formula) === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物流轨迹中需人工立即跟进的常见异常关键词 */
|
||||||
|
export const LOGISTICS_URGENT_KEYWORDS = [
|
||||||
|
'拒收',
|
||||||
|
'拒签',
|
||||||
|
'退件',
|
||||||
|
'客户拒收',
|
||||||
|
'拦截退回',
|
||||||
|
'退回发件',
|
||||||
|
'派件退回',
|
||||||
|
'无人签收',
|
||||||
|
'退回快件'
|
||||||
|
]
|
||||||
|
|
||||||
|
export function logisticsStringHasUrgentKeyword(s: unknown): boolean {
|
||||||
|
const t = String(s ?? '')
|
||||||
|
return LOGISTICS_URGENT_KEYWORDS.some((k) => t.includes(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logisticsTraceLineUrgent(context: unknown): boolean {
|
||||||
|
return logisticsStringHasUrgentKeyword(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据单次拉取的物流 payload 判断是否含拒收/退回等(详情抽屉、完成订单弹窗共用) */
|
||||||
|
export function analyzeLogisticsPayloadUrgent(p: Record<string, any> | null | undefined): {
|
||||||
|
show: boolean
|
||||||
|
keywords: string[]
|
||||||
|
lines: string[]
|
||||||
|
} {
|
||||||
|
if (!p) {
|
||||||
|
return { show: false, keywords: [], lines: [] }
|
||||||
|
}
|
||||||
|
const traces = Array.isArray(p.traces)
|
||||||
|
? (p.traces as Array<{ time?: string; context?: string }>)
|
||||||
|
: []
|
||||||
|
const chunks: string[] = []
|
||||||
|
if (p.state_text) chunks.push(String(p.state_text))
|
||||||
|
if (p.hint) chunks.push(String(p.hint))
|
||||||
|
for (const row of traces) {
|
||||||
|
if (row.context) chunks.push(String(row.context))
|
||||||
|
}
|
||||||
|
const joined = chunks.join('\n')
|
||||||
|
const keywords = LOGISTICS_URGENT_KEYWORDS.filter((k) => joined.includes(k))
|
||||||
|
if (!keywords.length) {
|
||||||
|
return { show: false, keywords: [], lines: [] }
|
||||||
|
}
|
||||||
|
const lines: string[] = []
|
||||||
|
for (const row of traces) {
|
||||||
|
const ctx = String(row.context || '')
|
||||||
|
if (logisticsStringHasUrgentKeyword(ctx)) {
|
||||||
|
lines.push(ctx)
|
||||||
|
if (lines.length >= 5) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lines.length && logisticsStringHasUrgentKeyword(p.state_text)) {
|
||||||
|
lines.push(`最新状态:${p.state_text}`)
|
||||||
|
}
|
||||||
|
if (!lines.length && logisticsStringHasUrgentKeyword(p.hint)) {
|
||||||
|
lines.push(String(p.hint))
|
||||||
|
}
|
||||||
|
return { show: true, keywords, lines }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验物流轨迹响应与请求上下文匹配(防止快速切换订单时旧响应串单) */
|
||||||
|
export function parseLogisticsTracePayload(
|
||||||
|
res: unknown,
|
||||||
|
expectedTrackingNumber: string,
|
||||||
|
expectedOrderId: number
|
||||||
|
): Record<string, any> | null {
|
||||||
|
const raw = (res as { data?: unknown })?.data ?? res
|
||||||
|
if (!raw || typeof raw !== 'object') return null
|
||||||
|
const payload = raw as Record<string, any>
|
||||||
|
const respNum = String(payload.tracking_number || '').trim()
|
||||||
|
if (respNum && respNum !== expectedTrackingNumber) return null
|
||||||
|
const respOrderId = Number(payload.order_id)
|
||||||
|
if (respOrderId > 0 && respOrderId !== expectedOrderId) return null
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) {
|
||||||
|
if (!row?.id) return false
|
||||||
|
const fs = Number(row.fulfillment_status)
|
||||||
|
return fs !== 3 && fs !== 4
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -56,298 +56,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 业务订单详情抽屉(只读,与 order_list.vue 详情保持一致) -->
|
<!-- 业务订单详情抽屉:共享组件的 readonly 受限版(展示范围由组件内 v-if="!readonly" 门控) -->
|
||||||
<el-drawer
|
<prescription-order-detail-drawer ref="detailDrawerRef" readonly append-to-body />
|
||||||
v-model="detailVisible"
|
|
||||||
size="80%"
|
|
||||||
destroy-on-close
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-skeleton v-if="detailLoading" :rows="12" animated class="px-6 py-4" />
|
|
||||||
<div v-else-if="detailData" class="px-6 pb-6 overflow-y-auto">
|
|
||||||
|
|
||||||
<!-- 顶部数据概览 -->
|
|
||||||
<el-row :gutter="16" class="mb-5">
|
|
||||||
<el-col :xs="12" :sm="6">
|
|
||||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
|
||||||
<div class="text-gray-500 text-xs mb-1">总金额</div>
|
|
||||||
<div class="text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="12" :sm="6">
|
|
||||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
|
||||||
<div class="text-gray-500 text-xs mb-1">已付总额</div>
|
|
||||||
<div class="text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="12" :sm="6">
|
|
||||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
|
||||||
<div class="text-gray-500 text-xs mb-1">需代收</div>
|
|
||||||
<div class="font-bold text-xl" :class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'">¥{{ formatMoney(detailAgencyToCollect) }}</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="12" :sm="6">
|
|
||||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
|
||||||
<div class="text-gray-500 text-xs mb-1">已付笔数</div>
|
|
||||||
<div class="text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} 笔</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
||||||
<!-- 左侧:处方单(不含测试价格、改患者信息、药材明细) -->
|
|
||||||
<el-card shadow="never" class="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>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-gray-400">ID: {{ detailData.prescription_id || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="detailPrescription">
|
|
||||||
<el-descriptions :column="2" border size="small" class="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 ? 'text-red-600 font-bold' : ''">{{ detailPrescription.phone || '—' }}</span>
|
|
||||||
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与订单手机不一致</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="用量">
|
|
||||||
<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>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="服用方式">
|
|
||||||
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
|
||||||
<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>
|
|
||||||
<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 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>
|
|
||||||
</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="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 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="82" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
|
||||||
</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="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 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>
|
|
||||||
</template>
|
|
||||||
<el-table v-if="detailLinkedPayOrders.length" :data="detailLinkedPayOrders" size="small" border stripe 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="82" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
|
||||||
</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="border-gray-100 mb-4">
|
|
||||||
<template #header><span class="font-medium text-[15px]">履约与收货信息</span></template>
|
|
||||||
<el-descriptions :column="3" border size="small">
|
|
||||||
<el-descriptions-item label="诊单ID" :span="3"><span class="font-mono">{{ detailData.diagnosis_id }}</span></el-descriptions-item>
|
|
||||||
<el-descriptions-item label="创建时间">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="业务订单创建人" :span="2">
|
|
||||||
<div v-if="detailData.creator_name" class="text-[13px] space-y-1">
|
|
||||||
<div><span class="font-medium text-gray-900">{{ detailData.creator_name || '—' }}</span></div>
|
|
||||||
<div v-if="detailOrderCreatorMetaLine" class="text-gray-600 text-xs">{{ 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">
|
|
||||||
<el-breadcrumb v-if="segments.length" class="text-[13px]" separator="/">
|
|
||||||
<el-breadcrumb-item v-for="(name, j) in segments" :key="j"><span class="text-gray-800">{{ 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 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">与处方手机不一致</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="收货地址">{{ 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 label="快递单号">{{ 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">{{ detailData.prescription_audit_remark || '—' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="支付审核意见" :span="3">{{ detailData.payment_slip_audit_remark || '—' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 物流轨迹(只读) -->
|
|
||||||
<el-card v-if="String(detailData.tracking_number || '').trim()" shadow="never" class="border-gray-100 mb-4">
|
|
||||||
<template #header><span class="font-medium text-[15px]">物流轨迹</span></template>
|
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
||||||
<el-input v-model="logisticsTracePhoneTail" clearable maxlength="20" placeholder="收件手机" 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">刷新轨迹</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="logisticsTraceLoading && !logisticsTracePayload" class="py-6 text-center text-sm text-gray-500">正在加载物流轨迹...</div>
|
|
||||||
<div v-else-if="logisticsTracePayload" class="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>
|
|
||||||
</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'">{{ 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="border-gray-100">
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch, ref, onMounted } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { usePaging } from '@/hooks/usePaging'
|
import { usePaging } from '@/hooks/usePaging'
|
||||||
|
import { prescriptionOrderLists } from '@/api/tcm'
|
||||||
|
import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue'
|
||||||
import {
|
import {
|
||||||
prescriptionOrderLists,
|
formatTime,
|
||||||
prescriptionOrderDetail,
|
fulfillmentText,
|
||||||
prescriptionOrderLogs,
|
fulfillmentTagType
|
||||||
prescriptionOrderLogisticsTrace,
|
} from '@/views/consumer/prescription/components/prescription-order-utils'
|
||||||
prescriptionOrderPaidPayOrders
|
|
||||||
} from '@/api/tcm'
|
|
||||||
import { getDictData } from '@/api/app'
|
|
||||||
import { hasPermission } from '@/utils/perm'
|
|
||||||
import feedback from '@/utils/feedback'
|
|
||||||
import useUserStore from '@/stores/modules/user'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
diagnosisId: number
|
diagnosisId: number
|
||||||
@@ -379,179 +102,11 @@ const buildParams = () => {
|
|||||||
queryParams.scene = 'diagnosis_edit'
|
queryParams.scene = 'diagnosis_edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 诊间医助角色(与 DiagnosisLists 等一致) */
|
// ─── 详情抽屉(共享组件,数据拉取/展示全部在组件内) ───
|
||||||
const TCM_ASSISTANT_ROLE_ID = 2
|
const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>()
|
||||||
/** 与 server/config/project.php prescription_audit_roles 默认一致,可处方审核的角色 */
|
|
||||||
const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3, 6]
|
|
||||||
|
|
||||||
/**
|
function openDetail(id: number) {
|
||||||
* 是否展示「处方审核」相关字段
|
detailDrawerRef.value?.open(id)
|
||||||
*/
|
|
||||||
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<any>(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 detailLinkedPayOrders = computed(() => {
|
|
||||||
const arr = detailData.value?.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 detailRecipientRxPhoneMismatch = computed(() => {
|
|
||||||
const rx = normalizeBizPhone(detailPrescription.value?.phone)
|
|
||||||
const biz = normalizeBizPhone(detailData.value?.recipient_phone)
|
|
||||||
if (!rx || !biz) return false
|
|
||||||
return rx !== biz
|
|
||||||
})
|
|
||||||
|
|
||||||
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 detailOrderCreatorMetaLine = computed(() => {
|
|
||||||
const d = detailData.value
|
|
||||||
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 detailOrderCreatorDeptBreadcrumbs = computed(() => {
|
|
||||||
const raw = String(detailData.value?.order_creator_dept_path ?? detailData.value?.diagnosis_creator_dept_path ?? '').trim()
|
|
||||||
if (!raw) return [] as string[][]
|
|
||||||
return raw.split(';').map((s) => s.trim()).filter(Boolean)
|
|
||||||
.map((p) => p.includes(' / ') ? p.split(' / ').map((x) => x.trim()).filter(Boolean) : p.split('/').map((x) => x.trim()).filter(Boolean))
|
|
||||||
.filter((segs) => segs.length > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ─── 物流轨迹 ───
|
|
||||||
const detailLogisticsExpress = ref<string>('auto')
|
|
||||||
const logisticsTraceLoading = ref(false)
|
|
||||||
const logisticsTracePayload = ref<Record<string, any> | null>(null)
|
|
||||||
const logisticsTracePhoneTail = ref('')
|
|
||||||
|
|
||||||
const logisticsTraceList = computed(() => {
|
|
||||||
const p = logisticsTracePayload.value
|
|
||||||
if (!p || !Array.isArray(p.traces)) return []
|
|
||||||
return p.traces as Array<{ time: string; context: string }>
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchLogisticsTrace() {
|
|
||||||
const id = Number(detailData.value?.id)
|
|
||||||
if (!id) return
|
|
||||||
logisticsTraceLoading.value = true
|
|
||||||
try {
|
|
||||||
const digits = String(logisticsTracePhoneTail.value || '').replace(/\D/g, '')
|
|
||||||
const params: { id: number; express_company?: string; phone_tail?: string } = {
|
|
||||||
id,
|
|
||||||
express_company: detailLogisticsExpress.value
|
|
||||||
}
|
|
||||||
if (digits.length >= 4) params.phone_tail = digits
|
|
||||||
const res: any = await prescriptionOrderLogisticsTrace(params)
|
|
||||||
logisticsTracePayload.value = (res?.data ?? res) as Record<string, any>
|
|
||||||
} catch {
|
|
||||||
logisticsTracePayload.value = null
|
|
||||||
} finally {
|
|
||||||
logisticsTraceLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 打开详情 ───
|
|
||||||
async function openDetail(id: number) {
|
|
||||||
detailData.value = null
|
|
||||||
detailUnlinkedPayOrders.value = []
|
|
||||||
logisticsTracePayload.value = null
|
|
||||||
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)
|
|
||||||
if (d.diagnosis_id) {
|
|
||||||
loadDetailUnlinkedPayOrders(d.diagnosis_id, id, d.pay_order_ids || [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
feedback.msgError(e?.message || '加载详情失败')
|
|
||||||
detailVisible.value = false
|
|
||||||
} finally {
|
|
||||||
detailLoading.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 ?? []
|
|
||||||
detailUnlinkedPayOrders.value = Array.isArray(lists) ? lists.filter((item: any) => !linkedIds.includes(item.id)) : []
|
|
||||||
} catch { detailUnlinkedPayOrders.value = [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
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 normalizeBizPhone(v: unknown): string {
|
|
||||||
if (v === null || v === undefined) return ''
|
|
||||||
return String(v).replace(/\s/g, '').trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatAmount = (value: unknown) => {
|
const formatAmount = (value: unknown) => {
|
||||||
@@ -559,126 +114,6 @@ const formatAmount = (value: unknown) => {
|
|||||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMoney(v: unknown) {
|
|
||||||
const n = Number(v)
|
|
||||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (value: unknown) => {
|
|
||||||
if (value === null || value === undefined || value === '') return '—'
|
|
||||||
if (typeof value === 'number' && value > 1e9 && value < 1e11) {
|
|
||||||
const d = new Date(value * 1000)
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
||||||
}
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatOrderTime(v: unknown) {
|
|
||||||
if (v === null || v === undefined || v === '') return '—'
|
|
||||||
if (typeof v === 'string' && String(v).includes('-')) return String(v)
|
|
||||||
return formatTime(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
function feeTypeText(t: number | undefined) {
|
|
||||||
const m: Record<number, string> = { 1: '药费', 2: '诊费', 3: '药费+诊费', 4: '其他' }
|
|
||||||
return m[Number(t)] ?? '—'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务套餐字典选项(与 order_list.vue 数据源一致:dict 类型 server_order)
|
|
||||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
|
||||||
|
|
||||||
async function loadServicePackageOptions() {
|
|
||||||
try {
|
|
||||||
const data: any = await getDictData({ type: 'server_order' })
|
|
||||||
const list = (data?.server_order || []) as Array<{ name: string; value: string; status?: number }>
|
|
||||||
servicePackageOptions.value = list.filter((item) => item.status !== 0)
|
|
||||||
} catch {
|
|
||||||
servicePackageOptions.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatServicePackage(value: unknown): string {
|
|
||||||
if (value === null || value === undefined || value === '') return '—'
|
|
||||||
let packages: string[] = []
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
packages = value.map((v) => String(v)).filter((v) => v !== '')
|
|
||||||
} else if (typeof value === 'string') {
|
|
||||||
packages = value.split(',').map((v) => v.trim()).filter((v) => v !== '')
|
|
||||||
}
|
|
||||||
if (packages.length === 0) return '—'
|
|
||||||
const names = packages.map((val) => {
|
|
||||||
const opt = servicePackageOptions.value.find((o) => o.value === val)
|
|
||||||
return opt ? opt.name : val
|
|
||||||
})
|
|
||||||
return names.join('、')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadServicePackageOptions()
|
|
||||||
})
|
|
||||||
|
|
||||||
function expressCompanyLabel(v: unknown) {
|
|
||||||
const s = String(v || '').toLowerCase()
|
|
||||||
if (s === 'sf') return '顺丰速运'
|
|
||||||
if (s === 'jd') return '京东快递'
|
|
||||||
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
|
|
||||||
return '自动识别'
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumerRxAuditText(s: number | undefined) {
|
|
||||||
const n = Number(s)
|
|
||||||
if (n === 1) return '已通过'
|
|
||||||
if (n === 2) return '已驳回'
|
|
||||||
return '待审核'
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
|
||||||
const n = Number(s)
|
|
||||||
if (n === 1) return 'success'
|
|
||||||
if (n === 2) return 'danger'
|
|
||||||
return 'warning'
|
|
||||||
}
|
|
||||||
|
|
||||||
function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
|
||||||
const n = Number(s)
|
|
||||||
if (n === 2) return 'success'
|
|
||||||
if (n === 1) return 'warning'
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderStatusText(s: number | undefined) {
|
|
||||||
const m: Record<number, string> = { 0: '待支付', 1: '待支付', 2: '已支付', 3: '已取消' }
|
|
||||||
return m[Number(s)] ?? '—'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fulfillmentText = (s: number | undefined) => {
|
|
||||||
const m: Record<number, string> = { 1: '待双审通过', 2: '待发货', 3: '已完成', 4: '已取消', 5: '已发货', 6: '已签收', 7: '进行中', 8: '暂不制药', 9: '拒收', 10: '退款', 11: '保留药方', 12: '制药缓发' }
|
|
||||||
return m[Number(s)] ?? '—'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fulfillmentTagType = (s: number | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
|
|
||||||
const n = Number(s)
|
|
||||||
if (n === 3 || n === 6) return 'success'
|
|
||||||
if (n === 5) return 'primary'
|
|
||||||
if (n === 4 || n === 9 || n === 10) return 'danger'
|
|
||||||
if (n === 2 || n === 7) return 'warning'
|
|
||||||
if (n === 8 || n === 11 || n === 12) return 'info'
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
function logActionText(act: string) {
|
|
||||||
const m: Record<string, string> = {
|
|
||||||
create: '创建', edit: '编辑', audit_rx_approve: '处方审核', audit_rx_reject: '处方审核',
|
|
||||||
audit_pay_approve: '支付审核', audit_pay_reject: '支付审核', fill_tracking: '填快递单',
|
|
||||||
ship: '确认发货', withdraw: '撤销', link_pay_order: '关联支付单',
|
|
||||||
revoke_rx_audit: '撤回处方审核', revoke_pay_audit: '撤回支付审核',
|
|
||||||
gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息',
|
|
||||||
update_amount: '修改订单金额', complete: '完成订单'
|
|
||||||
}
|
|
||||||
return m[act] || act
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.diagnosisId, patientIdNum.value] as const,
|
() => [props.diagnosisId, patientIdNum.value] as const,
|
||||||
() => {
|
() => {
|
||||||
@@ -699,22 +134,4 @@ defineExpose({ refresh: () => getLists() })
|
|||||||
.po-empty-tip {
|
.po-empty-tip {
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
.audit-stamp {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
right: 16px;
|
|
||||||
transform: rotate(15deg);
|
|
||||||
opacity: 0.15;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.stamp-inner {
|
|
||||||
border: 3px solid currentColor;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.stamp-pass { color: #22c55e; }
|
|
||||||
.stamp-reject { color: #ef4444; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user