This commit is contained in:
zlwu committed 2026-06-24 10:39:55 +08:00
1 parent 93c28759d0
commit b23a2eb350
129 files changed
+28511 -17769

No files matched your search

@@ -0,0 +1,262 @@
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): 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, '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)
if (dt) result.sampleCollectTime = dt
break
}
case 'maternalBloodCollectTime':
if (isMaternalBloodEmpty(kv.value)) {
result.maternalBloodExplicitEmpty = true
} else {
const dt = parseDatetime(kv.value)
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 === ''
}