import { parseChineseDatetimeText } from '@/utils/chinese-datetime-parse' import { parseBirthOrder } from '@/utils/chinese-numeral-parse' import { isBlankString, matchCellSampleType, resolveTransportType } from '@/utils/donation-standard-import-parse' import { extractNumericPart } from '@/utils/numeric-input-parse' import { SAMPLE_TYPE_PERINATAL_HSC, SAMPLE_TYPE_UC_MSC } from '@/utils/sample-production-reception-fields' export type ProtocolSampleImportHint = { typeKey: 'hsc' | 'uc_msc' typeName: string sampleCode?: string collectTime?: string } export type ProtocolPaymentTransferHint = { transferAmount: number transferTime: string } export type ProtocolStandardImportParsed = { sampleTypeHeader: string sampleHints: ProtocolSampleImportHint[] motherName?: string motherPhone?: 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 salesUserName?: string payMode: 'dj' | 'fq' | 'djfq' contractAmount?: number | null transferTypeLabel?: string paymentTransfers: ProtocolPaymentTransferHint[] transportModeText?: string transportType?: 'kdys' | 'ryxd' carrierName?: string etaTime?: string remark?: string } const MATERNAL_BLOOD_EMPTY_MARKERS = ['未采集', '无', '暂无', '—', '-', '未采'] const SAMPLE_CODE_RE = /^ZK[A-Z0-9]+$/i const KEY_ALIASES: Record = { 产妇姓名: 'motherName', 母亲姓名: 'motherName', 孕妇电话: 'motherPhone', 母亲联系电话: 'motherPhone', 新生儿性别: 'newbornSex', 新生儿体重: 'newbornWeightGram', '新生儿体重(克)': 'newbornWeightGram', 新生儿评分: 'apgarScore', 分娩时间: 'newbornDeliveryTime', 新生儿分娩时间: 'newbornDeliveryTime', 母血采集时间: 'maternalBloodCollectTime', 分娩方式: 'deliveryMode', 胎次: 'birthOrder', 采集医护: 'medicalStaff', 采集医院: 'hospitalName', 销售人员: 'salesUserName', 采集专员: 'salesUserName', 缴费方式: 'payModeText', 转账方式: 'transferTypeText', 合同金额: 'contractAmount', 转账金额: 'transferAmount', 转账时间: 'transferTime', 运输方式: 'transportModeText', 预计送达时间: 'etaTime', 备注: 'remark' } 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 normalizeChineseDatetimeLiteral(value: string): string { let v = (value || '').trim() v = v.replace( /^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/, (_, y, mo, d, h, mi, s) => `${y}年${Number(mo)}月${Number(d)}日${Number(h)}时${Number(mi)}分${s ? Number(s) + '秒' : ''}` ) return v } function parseDatetime(value: string, minutePrecision = false): string | undefined { const v = normalizeChineseDatetimeLiteral(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 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 parseMoney(value: string): number | null | undefined { const num = extractNumericPart(value) if (num == null || num === '') return undefined const n = Number(num) return Number.isFinite(n) ? n : undefined } function parseScore(value: string): number | null | undefined { return parseMoney(value) } 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)) } function resolveSampleCollectHint(key: string): Pick | null { const k = key.trim() if (/脐带间充质.*采集时间/.test(k) || k === '脐带采集时间') { return { typeKey: 'uc_msc', typeName: SAMPLE_TYPE_UC_MSC } } if (/造血干细胞.*采集时间/.test(k) || k === '样本采集时间') { return { typeKey: 'hsc', typeName: SAMPLE_TYPE_PERINATAL_HSC } } return null } function resolveSampleCodeHint( key: string, value: string ): (Pick & { sampleCode: string }) | null { const code = value.trim().toUpperCase() if (!SAMPLE_CODE_RE.test(code)) return null const k = key.trim() if (/脐带间充质/.test(k)) { return { typeKey: 'uc_msc', typeName: SAMPLE_TYPE_UC_MSC, sampleCode: code } } if (/造血干细胞/.test(k)) { return { typeKey: 'hsc', typeName: SAMPLE_TYPE_PERINATAL_HSC, sampleCode: code } } return null } function upsertSampleHint( hints: ProtocolSampleImportHint[], partial: Partial & Pick ) { let hint = hints.find((h) => h.typeKey === partial.typeKey) if (!hint) { hint = { typeKey: partial.typeKey, typeName: partial.typeName } hints.push(hint) } if (partial.sampleCode) hint.sampleCode = partial.sampleCode if (partial.collectTime) hint.collectTime = partial.collectTime } function earliestCollectTime(hints: ProtocolSampleImportHint[]): string | undefined { const times = hints.map((h) => h.collectTime).filter(Boolean) as string[] if (!times.length) return undefined return times.sort((a, b) => a.localeCompare(b))[0] } /** 缴费方式:趸交+分期 → djfq;仅「分期」→ fq;其余 → dj */ export function parsePayModeFromText(value: string): { payMode: 'dj' | 'fq' | 'djfq' | 'djfq' transferLabel?: string } { const v = (value || '').trim() if (/趸交\s*[++]\s*分期|趸交.*分期/.test(v)) return { payMode: 'djfq' } if (/分期/.test(v)) return { payMode: 'fq' } if (/微信|支付宝|银行卡|银行|现金/.test(v)) { return { payMode: 'dj', transferLabel: v } } return { payMode: 'dj' } } /** 解析转账时间行(支持多笔:…2000元,…付20880元) */ export function parsePaymentTransferSegments( text: string, fallbackAmount?: number | null ): ProtocolPaymentTransferHint[] { const raw = (text || '').trim() if (!raw) { if (fallbackAmount != null && fallbackAmount > 0) { return [{ transferAmount: fallbackAmount, transferTime: '' }] } return [] } const segments = raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean) const results: ProtocolPaymentTransferHint[] = [] let defaultYear: number | undefined for (const seg of segments) { const amountMatch = seg.match(/(?:付)?(\d+(?:\.\d+)?)\s*元\s*$/) if (!amountMatch) continue const transferAmount = Number(amountMatch[1]) if (!Number.isFinite(transferAmount)) continue let dtRaw = seg.slice(0, seg.length - amountMatch[0].length).trim() dtRaw = normalizeChineseDatetimeLiteral(dtRaw) if (/^\d{1,2}月/.test(dtRaw) && defaultYear) { dtRaw = `${defaultYear}年${dtRaw}` } const transferTime = parseChineseDatetimeText(dtRaw, 'datetime') if (transferTime) { if (!defaultYear) defaultYear = Number(transferTime.slice(0, 4)) results.push({ transferAmount, transferTime }) } } if (results.length) return results const transferTime = parseChineseDatetimeText(normalizeChineseDatetimeLiteral(raw), 'datetime') if (fallbackAmount != null && fallbackAmount > 0) { return [{ transferAmount: fallbackAmount, transferTime: transferTime || '' }] } if (transferTime) { return [{ transferAmount: fallbackAmount ?? 0, transferTime }] } return [] } /** 字典 label → transfer_type 值(wxzz 等) */ export function resolveTransferTypeCode( label: string, options: Array<{ value: string; label: string }> ): string { const v = (label || '').trim() if (!v || !options.length) return '' const exact = options.find((o) => o.label === v || o.value === v) if (exact) return exact.value const contains = options.filter((o) => o.label.includes(v) || v.includes(o.label)) if (contains.length === 1) return contains[0].value const keywordMap: Array<[RegExp, string]> = [ [/微信/, '微信'], [/支付宝/, '支付宝'], [/银行/, '银行'], [/现金/, '现金'] ] for (const [re, kw] of keywordMap) { if (!re.test(v)) continue const hit = options.find((o) => o.label.includes(kw)) if (hit) return hit.value } return '' } /** 从运输描述中提取携带人姓名(如「露露帮助…」) */ export function extractCarrierNameFromTransport(text: string): string | undefined { const v = (text || '').trim() const m = v.match(/^([\u4e00-\u9fa5]{1,4})(?:帮助|协助|取)/) return m?.[1] } /** 解析采集协议单一键导入标准文本 */ export function parseProtocolStandardImportText(text: string): ProtocolStandardImportParsed | 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: ProtocolStandardImportParsed = { sampleTypeHeader: lines[0], sampleHints: [], payMode: 'dj', paymentTransfers: [] } let transferTimeRaw = '' let transferAmountSingle: number | null | undefined for (let i = 1; i < lines.length; i++) { const kv = splitKeyValue(lines[i]) if (!kv) continue const codeHint = resolveSampleCodeHint(kv.key, kv.value) if (codeHint) { upsertSampleHint(result.sampleHints, codeHint) continue } const collectMeta = resolveSampleCollectHint(kv.key) if (collectMeta) { const dt = parseDatetime(kv.value, true) if (dt) upsertSampleHint(result.sampleHints, { ...collectMeta, collectTime: dt }) continue } const field = KEY_ALIASES[kv.key] if (!field) continue switch (field) { case 'motherName': result.motherName = kv.value break case 'motherPhone': result.motherPhone = kv.value.replace(/\s+/g, '') break case 'newbornSex': { const sex = parseSex(kv.value) if (sex) result.newbornSex = sex break } case 'newbornWeightGram': { const w = parseMoney(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 '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 = parseBirthOrder(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 'salesUserName': result.salesUserName = kv.value break case 'payModeText': { const pm = parsePayModeFromText(kv.value) result.payMode = pm.payMode if (pm.transferLabel && !result.transferTypeLabel) { result.transferTypeLabel = pm.transferLabel } break } case 'transferTypeText': result.transferTypeLabel = kv.value break case 'contractAmount': { const amt = parseMoney(kv.value) if (amt !== undefined) result.contractAmount = amt break } case 'transferAmount': { const amt = parseMoney(kv.value) if (amt !== undefined) transferAmountSingle = amt break } case 'transferTime': transferTimeRaw = kv.value break case 'transportModeText': { result.transportModeText = kv.value result.transportType = resolveTransportType(kv.value) ?? undefined result.carrierName = extractCarrierNameFromTransport(kv.value) break } case 'etaTime': { const dt = parseDatetime(kv.value) if (dt) result.etaTime = dt break } case 'remark': result.remark = kv.value break default: break } } result.paymentTransfers = parsePaymentTransferSegments(transferTimeRaw, transferAmountSingle ?? null) if (result.sampleHints.length) { result.sampleCollectTime = earliestCollectTime(result.sampleHints) } return result } export { matchCellSampleType, isBlankString }