/** * 处方业务订单——纯展示/格式化工具 * * 唯一数据源: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 }