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
|
}
|