/** * 处方业务订单——纯展示/格式化工具 * * 唯一数据源: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 isRemoteSnapshotLocked(row: Record | null | undefined): boolean { if (!row) return false if (String(row.gancao_reciperl_order_no || '').trim()) return true if (String(row.ej_pharmacy_order_no || '').trim()) return true if (Number(row.gancao_submit_time || 0) > 0 || Number(row.ej_pharmacy_submit_time || 0) > 0) { return true } return ['PENDING', 'UNKNOWN', 'PENDING_RECONCILE', 'SUCCESS'].includes( String(row.pharmacy_claim_status || '').toUpperCase() ) } export type SupplyMode = 'gancao' | 'direct' | 'self' export function supplyModeKey(row: Record | null | undefined): SupplyMode { if ( String(row?.ship_mode || '') .trim() .toLowerCase() === 'direct' ) return 'direct' if (String(row?.gancao_reciperl_order_no || '').trim()) return 'gancao' return 'self' } export function supplyModeLabel(row: Record | null | undefined): string { const mode = supplyModeKey(row) if (mode === 'direct') return '洛阳直发' return mode === 'gancao' ? '甘草' : '自营' } export function supplyModeTagType( row: Record | null | undefined ): 'success' | 'warning' | 'info' { const mode = supplyModeKey(row) if (mode === 'direct') return 'warning' return mode === 'gancao' ? 'success' : 'info' } 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: '甘草下单', ej_pharmacy_submit: '洛阳药房下单', ej_pharmacy_callback: '洛阳药房状态', patch_rx_patient: '处方患者信息', patch_rx_usage: '服用参数', update_amount: '修改订单金额', complete: '完成订单', refund: '退款', manual_log: '手工备注', assign_assistant: '改派医助', add_pay_order: '补齐支付单', set_ship_mode: '发货类型' } return m[act] || act } /** 处方/支付单审核状态文案(0 待审核 / 1 已通过 / 2 已驳回) */ export function auditStatusText(s: number | undefined) { if (s === 1) return '已通过' if (s === 2) return '已驳回' return '待审核' } /** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */ 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 '付呗' if (createType === 'express_cod') return '快递代收' const paymentMethod = String(row?.payment_method || '') const methodMap: Record = { alipay: '支付宝', wechat: '微信', wechat_work: '企业微信', fubei: '付呗', express_cod: '快递代收', 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 } /** 处方忌口:库内逗号分隔字符串或前端多选数组,统一为「、」连接展示 */ export function formatDietaryTaboo(raw: unknown): string { if (Array.isArray(raw)) { return raw.map((v) => String(v ?? '').trim()).filter(Boolean).join('、') } if (typeof raw === 'string' && raw.trim()) { return raw .split(',') .map((s) => s.trim()) .filter(Boolean) .join('、') } return '' } /** 服务套餐 dict:server_order */ export type ServicePackageOption = { name: string; value: string; status?: number } export function normalizeServicePackageValue(v: unknown): string { return String(v ?? '').trim() } /** 解析订单 service_package(逗号串 / 数组 / 单值数字) */ export function parseServicePackageValues(raw: unknown): string[] { if (raw == null || raw === '') return [] if (Array.isArray(raw)) { return raw.map(normalizeServicePackageValue).filter(Boolean) } if (typeof raw === 'string') { return raw.split(',').map((v) => v.trim()).filter(Boolean) } const one = normalizeServicePackageValue(raw) return one ? [one] : [] } export function servicePackageValueEquals(a: unknown, b: unknown): boolean { const sa = normalizeServicePackageValue(a) const sb = normalizeServicePackageValue(b) if (!sa || !sb) return false if (sa === sb) return true const na = Number(sa) const nb = Number(sb) return Number.isFinite(na) && Number.isFinite(nb) && na === nb } export function normalizeServicePackageOptions(raw: unknown): ServicePackageOption[] { if (!Array.isArray(raw)) return [] return raw .map((item: any) => ({ name: String(item?.name ?? '').trim() || normalizeServicePackageValue(item?.value), value: normalizeServicePackageValue(item?.value), status: Number(item?.status ?? 1) })) .filter((item) => item.value !== '') } export function findServicePackageOption( options: ServicePackageOption[], value: unknown ): ServicePackageOption | undefined { const key = normalizeServicePackageValue(value) if (!key) return undefined return options.find((opt) => servicePackageValueEquals(opt.value, key)) } /** 展示用:value → 字典 name,多选用「、」连接 */ export function formatServicePackageLabels( value: unknown, options: ServicePackageOption[], emptyText = '—' ): string { const packages = parseServicePackageValues(value) if (packages.length === 0) return emptyText const names = packages.map((val) => { const option = findServicePackageOption(options, val) return option?.name || val }) return names.join('、') } /** 编辑下拉:字典项 + 当前已选但字典缺失的兜底项 */ export function mergeServicePackageSelectOptions( options: ServicePackageOption[], selected: unknown[] ): ServicePackageOption[] { const known = new Set(options.map((o) => o.value)) const extras: ServicePackageOption[] = [] for (const raw of selected) { const val = normalizeServicePackageValue(raw) if (!val || known.has(val)) continue const matched = findServicePackageOption(options, val) extras.push(matched ?? { name: val, value: val, status: 0 }) known.add(val) } return extras.length ? [...options, ...extras] : options }