Files
zkxw/web-admin/src/utils/protocol-standard-import-parse.ts
T
2026-07-06 19:41:24 +08:00

444 lines
13 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 { 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<string, string> = {
产妇姓名: '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<ProtocolSampleImportHint, 'typeKey' | 'typeName'> | 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<ProtocolSampleImportHint, 'typeKey' | 'typeName'> & { 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<ProtocolSampleImportHint> & Pick<ProtocolSampleImportHint, 'typeKey' | 'typeName'>
) {
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 }