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