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> = { 产妇姓名: '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 === '' }