265 lines
7.9 KiB
TypeScript
265 lines
7.9 KiB
TypeScript
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 === ''
|
||
}
|