From 2e59e883b803237c81430cb5f1e21f135cbfb9b6 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 10 Jun 2026 14:37:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=AF=A6=E6=83=85=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PrescriptionOrderDetailDrawer.vue | 1444 ++++++++++++ .../components/prescription-order-utils.ts | 342 +++ .../consumer/prescription/order_list.vue | 1939 ++--------------- .../diagnosis/components/PatientOrderList.vue | 609 +----- 4 files changed, 1954 insertions(+), 2380 deletions(-) create mode 100644 admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue create mode 100644 admin/src/views/consumer/prescription/components/prescription-order-utils.ts diff --git a/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue b/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue new file mode 100644 index 00000000..d571209b --- /dev/null +++ b/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue @@ -0,0 +1,1444 @@ + + + + + diff --git a/admin/src/views/consumer/prescription/components/prescription-order-utils.ts b/admin/src/views/consumer/prescription/components/prescription-order-utils.ts new file mode 100644 index 00000000..e65cec92 --- /dev/null +++ b/admin/src/views/consumer/prescription/components/prescription-order-utils.ts @@ -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 = { + 1: '挂号费', + 2: '问诊费', + 3: '药品费用', + 4: '首付费用', + 5: '尾款费用', + 6: '其他费用', + 7: '全部费用', + 8: '驼奶费用' + } + return m[Number(t)] ?? '—' +} + +export function fulfillmentText(s: number | undefined) { + const m: Record = { + 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 = { + 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 = { + 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 = { + 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 + 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 | 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 | 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 | null { + const raw = (res as { data?: unknown })?.data ?? res + if (!raw || typeof raw !== 'object') return null + const payload = raw as Record + 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 +} diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 92702407..ec290e62 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -564,899 +564,123 @@ - - + - + 天数: {{ - detailData.medication_days != null && + detailData?.medication_days != null && String(detailData.medication_days).trim() !== '' ? detailData.medication_days + ' 天' : '—' @@ -2896,8 +2120,32 @@