Files
zkxw/web-admin/src/utils/donation-standard-import-parse.ts
T
2026-06-29 09:21:09 +08:00

265 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { parseChineseDatetimeText } from '@/utils/chinese-datetime-parse'
import { extractNumericPart } from '@/utils/numeric-input-parse'
export type DonationStandardImportParsed = {
sampleTypeHeader: string
motherName?: string
newbornSex?: '1' | '0'
newbornWeightGram?: number | null
apgarScore?: number | null
newbornDeliveryTime?: string
sampleCollectTime?: string
maternalBloodCollectTime?: string
maternalBloodExplicitEmpty?: boolean
deliveryMode?: string
birthOrder?: number | null
medicalStaff?: string
hospitalName?: string
transportExpressName?: string
salesUserName?: string
}
const KEY_ALIASES: Record<string, keyof Omit<DonationStandardImportParsed, 'sampleTypeHeader'>> = {
产妇姓名: 'motherName',
母亲姓名: 'motherName',
新生儿性别: 'newbornSex',
新生儿体重: 'newbornWeightGram',
'新生儿体重(克)': 'newbornWeightGram',
新生儿评分: 'apgarScore',
'新生儿评分1分钟': 'apgarScore',
'新生儿评分5分钟': 'apgarScore',
分娩时间: 'newbornDeliveryTime',
新生儿分娩时间: 'newbornDeliveryTime',
造血干细胞采集时间: 'sampleCollectTime',
样本采集时间: 'sampleCollectTime',
母血采集时间: 'maternalBloodCollectTime',
分娩方式: 'deliveryMode',
胎次: 'birthOrder',
采集医护: 'medicalStaff',
采集医院: 'hospitalName',
运输方式: 'transportExpressName',
销售人员: 'salesUserName',
采集专员: 'salesUserName'
}
const MATERNAL_BLOOD_EMPTY_MARKERS = ['未采集', '无', '暂无', '—', '-', '未采']
function normalizeLine(line: string): string {
return line
.replace(/^[\s【】]+|[\s【】]+$/g, '')
.trim()
}
function splitKeyValue(line: string): { key: string; value: string } | null {
const match = line.match(/^([^::]+)[::]\s*(.*)$/)
if (!match) return null
return { key: match[1].trim(), value: match[2].trim() }
}
function parseSex(value: string): '1' | '0' | undefined {
const v = value.trim()
if (!v) return undefined
if (/^男/.test(v)) return '1'
if (/^女/.test(v)) return '0'
return undefined
}
function parseDeliveryMode(value: string): string | undefined {
const v = value.trim()
if (!v) return undefined
if (/剖宫产|剖腹产/.test(v)) return 'pgc'
if (/顺产|自然分娩/.test(v)) return 'sc'
return undefined
}
function parseWeightGram(value: string): number | null | undefined {
const v = value.trim()
if (!v) return undefined
const num = extractNumericPart(v)
if (num === '' || num == null) return undefined
const n = Number(num)
return Number.isFinite(n) ? n : undefined
}
function parseScore(value: string): number | null | undefined {
const v = value.trim()
if (!v) return undefined
const num = extractNumericPart(v)
if (num === '' || num == null) return undefined
const n = Number(num)
return Number.isFinite(n) ? n : undefined
}
function parseDatetime(value: string, minutePrecision = false): string | undefined {
const v = value.trim()
if (!v) return undefined
if (MATERNAL_BLOOD_EMPTY_MARKERS.some((m) => v === m || v.includes(m))) {
return undefined
}
return (
parseChineseDatetimeText(v, minutePrecision ? 'datetime-minute' : 'datetime') || undefined
)
}
function isMaternalBloodEmpty(value: string): boolean {
const v = value.trim()
if (!v) return true
return MATERNAL_BLOOD_EMPTY_MARKERS.some((m) => v === m || v.includes(m))
}
/** 解析捐赠协议单一键导入标准文本 */
export function parseDonationStandardImportText(text: string): DonationStandardImportParsed | null {
const raw = (text || '').trim()
if (!raw) return null
const lines = raw
.split(/\r?\n/)
.map(normalizeLine)
.filter(Boolean)
if (!lines.length) return null
const result: DonationStandardImportParsed = {
sampleTypeHeader: lines[0]
}
for (let i = 1; i < lines.length; i++) {
const kv = splitKeyValue(lines[i])
if (!kv) continue
const field = KEY_ALIASES[kv.key]
if (!field) continue
switch (field) {
case 'motherName':
result.motherName = kv.value
break
case 'newbornSex': {
const sex = parseSex(kv.value)
if (sex) result.newbornSex = sex
break
}
case 'newbornWeightGram': {
const w = parseWeightGram(kv.value)
if (w !== undefined) result.newbornWeightGram = w
break
}
case 'apgarScore': {
const s = parseScore(kv.value)
if (s !== undefined) result.apgarScore = s
break
}
case 'newbornDeliveryTime': {
const dt = parseDatetime(kv.value)
if (dt) result.newbornDeliveryTime = dt
break
}
case 'sampleCollectTime': {
const dt = parseDatetime(kv.value, true)
if (dt) result.sampleCollectTime = dt
break
}
case 'maternalBloodCollectTime':
if (isMaternalBloodEmpty(kv.value)) {
result.maternalBloodExplicitEmpty = true
} else {
const dt = parseDatetime(kv.value, true)
if (dt) result.maternalBloodCollectTime = dt
}
break
case 'deliveryMode': {
const mode = parseDeliveryMode(kv.value)
if (mode) result.deliveryMode = mode
break
}
case 'birthOrder': {
const n = parseScore(kv.value)
if (n !== undefined) result.birthOrder = n
break
}
case 'medicalStaff':
result.medicalStaff = kv.value
break
case 'hospitalName':
if (!result.hospitalName) result.hospitalName = kv.value
break
case 'transportExpressName':
result.transportExpressName = kv.value
break
case 'salesUserName':
result.salesUserName = kv.value
break
default:
break
}
}
return result
}
type CellSampleTypeOption = { id: string; typeName: string }
const SAMPLE_TYPE_SEGMENTS = ['围产期', '造血', '干细胞', '脐带', '间充质', '胎盘', '亚全能', '捐献']
/** 根据首行文本与细胞样本类型列表做语义匹配 */
export function matchCellSampleType(
headerLine: string,
types: CellSampleTypeOption[]
): CellSampleTypeOption | null {
const header = normalizeLine(headerLine)
if (!header || !types.length) return null
let best: CellSampleTypeOption | null = null
let bestScore = 0
for (const t of types) {
const name = t.typeName || ''
let score = 0
for (const seg of SAMPLE_TYPE_SEGMENTS) {
if (header.includes(seg) && name.includes(seg)) score += 2
}
if (header.includes('围产期') && name.includes('围产期')) score += 3
if (header.includes('捐献') && name.includes('造血')) score += 2
if (header.includes('间充质') && name.includes('间充质')) score += 3
if (header.includes('胎盘') && name.includes('胎盘')) score += 3
if (header.includes('亚全能') && name.includes('亚全能')) score += 2
if (score > bestScore) {
bestScore = score
best = t
}
}
return bestScore > 0 ? best : null
}
/** 在快递公司列表中匹配名称(优先完全匹配,否则包含匹配) */
export function matchExpressCompany(
name: string,
companies: Array<{ id: string; expressName: string }>
): { id: string; expressName: string } | null {
const target = (name || '').trim()
if (!target || !companies.length) return null
const exact = companies.find((c) => c.expressName === target)
if (exact) return exact
const normalized = target.replace(/快递$/, '')
const contains = companies.filter(
(c) => c.expressName.includes(target) || c.expressName.includes(normalized)
)
if (contains.length === 1) return contains[0]
const sf = companies.find((c) => /顺丰/.test(c.expressName) && /顺丰/.test(target))
if (sf) return sf
return contains[0] || null
}
export function isBlankString(val: unknown): boolean {
return val == null || String(val).trim() === ''
}
export function isEmptyNumber(val: unknown): boolean {
return val == null || val === ''
}