yxh
yxh
7 天以前 09dc74e264b6262cbe41c9c8476e24705871d4a6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import dayjs from 'dayjs'
import { calculateAge } from '@/utils/dateUtil'
 
/**
 * 患者信息展示的公共格式化工具。
 *
 * 背景(需求1):患者信息需在**每个界面**展示 18 项:
 *   流水号、排队序号、姓名、性别、出生日期、年龄、项目、病区、病床号、
 *   登记时间、状态、患者电话、开单日期、距今天数、来源(门急住体检)、
 *   门诊号、医嘱号、开单医师
 *
 * 其中 **年龄** 与 **距今天数** 数据库无对应列,必须实时计算。
 * 原先该算法内联在 RoutinePanel.vue 中,多个界面若各自复制会造成口径不一致
 * (例如一个按 doctorTime 算、另一个按 createTime 算),故抽到此处统一。
 *
 * 注意:
 *   · 后端返回的生日/日期字段多为字符串("2026-09-29" / "2026-09-29 10:00:00"),
 *     故这里统一用 dayjs 兼容解析,避免 Date 构造在不同浏览器下的差异。
 *   · 拿不到值时统一返回 ''(而不是 "NaN岁" / "NaN天")。
 */
 
/**
 * 年龄:由生日实时计算,返回形如 "65岁";无生日/生日非法时返回 ''
 *
 * 注意:库中 `pat_birthday` 可能是 `LocalDate`("2000-01-01")或
 * 时间戳/ISO 字符串;也可能存在脏值(如 "0000-00-00")。
 * dayjs 对脏值返回 Invalid,`diff` 会得到 NaN,故这里先显式校验。
 */
export function formatAge(birthday?: string | number | Date | null): string {
  if (birthday === undefined || birthday === null || birthday === '') return ''
  const d = dayjs(birthday as any)
  if (!d.isValid()) return ''
  const age = calculateAge(birthday as unknown as Date)
  if (!Number.isFinite(age) || age < 0 || age > 150) return ''
  return `${age}岁`
}
 
/**
 * 距今天数:按**开单日期**(doctorTime)计算,返回形如 "3天";无值时返回 ''
 *
 * 说明:需求1 的"距今天数"是给医生判断"这张单子压了多久",
 *       因此基准必须是**开单日期**,而不是创建时间/登记时间。
 */
export function formatDaysFromOrder(orderTime?: string | number | Date | null): string {
  if (orderTime === undefined || orderTime === null || orderTime === '') return ''
  const d = dayjs(orderTime)
  if (!d.isValid()) return ''
  const days = dayjs().startOf('day').diff(d.startOf('day'), 'day')
  if (!Number.isFinite(days)) return ''
  return `${days}天`
}
 
/**
 * 距今天数(数值版,便于排序/条件样式)
 */
export function daysFromOrder(orderTime?: string | number | Date | null): number | null {
  if (orderTime === undefined || orderTime === null || orderTime === '') return null
  const d = dayjs(orderTime)
  if (!d.isValid()) return null
  return dayjs().startOf('day').diff(d.startOf('day'), 'day')
}
 
/**
 * 日期时间格式化(空值安全),默认 "YYYY-MM-DD HH:mm:ss"
 */
export function formatDateTime(
  value?: string | number | Date | null,
  format = 'YYYY-MM-DD HH:mm:ss'
): string {
  if (value === undefined || value === null || value === '') return ''
  const d = dayjs(value)
  return d.isValid() ? d.format(format) : ''
}
 
/**
 * 日期格式化(空值安全),默认 "YYYY-MM-DD"
 */
export function formatDateOnly(
  value?: string | number | Date | null,
  format = 'YYYY-MM-DD'
): string {
  return formatDateTime(value, format)
}
 
/**
 * 空值安全展示:null/undefined/'' 统一显示为 "-"
 */
export function displayOr(value?: string | number | null, placeholder = '-'): string {
  if (value === undefined || value === null || value === '') return placeholder
  return String(value)
}
 
/** 就诊卡号的解析结果:值 + 实际来源标注 */
export interface CardNoResult {
  /** 展示用值(无任何候选时为 '-') */
  value: string
  /** 值的真实来源;有真卡号时为空串(无需标注) */
  from: '' | '门诊号' | '身份证' | '住院号'
}
 
/**
 * 判断一个值是否"像身份证号"(18 位数字末位可为 X,或 15 位纯数字)。
 *
 * 为什么需要按**值的形态**判断,而不是按字段名:
 *   实测 `appointment.outpatient_no` 混合存放两类内容 ——
 *     18 位身份证格式 103604 行(65.2%)
 *     12 位真门诊号    54204 行(34.1%,形如 882609282283)
 *   若一律按字段名标注为「门诊号」,那 65% 的身份证号依然被错标。
 */
export function looksLikeIdCard(v?: string | null): boolean {
  if (!v) return false
  const s = String(v).trim()
  return /^[0-9]{17}[0-9Xx]$/.test(s) || /^[0-9]{15}$/.test(s)
}
 
/**
 * 解析「门诊号」列的展示值与来源。
 *
 * 【背景】实测 HIS 在 S0201ECG 报文的 `patientPerson.id.item[1]`
 * (root `2.16.156.10011.1.15`,官方文档注释即「医保卡号」)
 * **恒为空**(222/222 条),因此 `medical_card` 全库为 NULL —— 详见《部署清单》5.1。
 *
 * 【列名变更(经确认)】由于该列**实际展示的是 `outpatient_no`**
 * (实测 159,116 行:65% 形如身份证、35% 为门诊号;`medical_card` 无一非空),
 * 页面标签已由「就诊卡号」改为「**门诊号**」,避免医护人员误读。
 *
 * 回退优先级与标注规则:
 *   1. `medicalCard`               → 无标注(HIS 补齐卡号后会走此分支,标注自动消失)
 *   2. `outpatientNo` 非身份证形态  → 标注「门诊号」
 *   3. `outpatientNo` 是身份证形态  → 标注「身份证」
 *   4. `patIdentityId`             → 标注「身份证」
 *   5. `hospitalNo`(住院号)       → 标注「住院号」
 *   6. 都没有                      → '-'
 *
 * > 标注的作用:`outpatient_no` 是**混合列**,里面既有真门诊号也有身份证号,
 * > 故按**值形态**(而非字段名)标注,便于医护人员区分。
 *
 * @param row 患者/预约/队列/租赁数据对象(兼容平铺字段与 patDetails)
 */
export function resolveCardNo(row: any): CardNoResult {
  if (row === undefined || row === null) return { value: '-', from: '' }
  const pd = row.patDetails || {}
 
  const medical = firstNonEmpty(row.medicalCard, pd.medicalCard)
  if (medical !== undefined) return { value: medical, from: '' }
 
  const outpatient = firstNonEmpty(row.outpatientNo, pd.outpatientNo)
  if (outpatient !== undefined) {
    // 该列实测混合了身份证号,按形态标注而非按字段名
    return { value: outpatient, from: looksLikeIdCard(outpatient) ? '身份证' : '门诊号' }
  }
 
  const identity = firstNonEmpty(row.patIdentityId, pd.patIdentityId)
  if (identity !== undefined) return { value: identity, from: '身份证' }
 
  const hospital = firstNonEmpty(row.hospitalNo, pd.hospitalNo)
  if (hospital !== undefined) return { value: hospital, from: '住院号' }
 
  return { value: '-', from: '' }
}
 
/** 取第一个非空(null/undefined/'' 均视为空)值,全空时返回 undefined */
export function firstNonEmpty(...vals: any[]): string | undefined {
  for (const v of vals) {
    if (v !== undefined && v !== null && v !== '') return String(v)
  }
  return undefined
}