提交
This commit is contained in:
1 parent
038a60b09e
commit
3740a7dd80
45 files changed
+10945
-6192
No files matched your search
@@ -38,7 +38,7 @@ export function parseChineseDatetimeText(
|
||||
}
|
||||
|
||||
const cnMatch = raw.match(
|
||||
/^(\d{4})年(\d{1,2})月(\d{1,2})日(?:\s*(\d{1,2})[时::](\d{1,2})(?:[分::](\d{1,2})[秒]?)?)?/
|
||||
/^(\d{4})年(\d{1,2})月(\d{1,2})日(?:\s*(\d{1,2})[时点::](\d{1,2})(?:[分::](\d{1,2})[秒]?)?)?/
|
||||
)
|
||||
if (!cnMatch) return null
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { extractNumericPart } from '@/utils/numeric-input-parse'
|
||||
|
||||
const CN_DIGIT: Record<string, number> = {
|
||||
零: 0,
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10
|
||||
}
|
||||
|
||||
/** 解析简单中文数字(一~十、十一等),无法解析返回 null */
|
||||
export function parseChineseNumeral(text: string): number | null {
|
||||
const v = (text || '').trim()
|
||||
if (!v) return null
|
||||
|
||||
if (/^\d+$/.test(v)) {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
if (v.length === 1 && CN_DIGIT[v] != null) {
|
||||
return CN_DIGIT[v]
|
||||
}
|
||||
|
||||
if (v.startsWith('十')) {
|
||||
const rest = v.slice(1)
|
||||
if (!rest) return 10
|
||||
const tail = CN_DIGIT[rest]
|
||||
return tail != null ? 10 + tail : null
|
||||
}
|
||||
|
||||
if (v.endsWith('十')) {
|
||||
const head = CN_DIGIT[v.slice(0, 1)]
|
||||
return head != null ? head * 10 : null
|
||||
}
|
||||
|
||||
const shiIdx = v.indexOf('十')
|
||||
if (shiIdx > 0) {
|
||||
const head = CN_DIGIT[v.slice(0, shiIdx)]
|
||||
const tail = CN_DIGIT[v.slice(shiIdx + 1)] ?? 0
|
||||
if (head != null) return head * 10 + tail
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** 解析胎次:阿拉伯数字、中文数字、「N胎」 */
|
||||
export function parseBirthOrder(value: string): number | null | undefined {
|
||||
const v = (value || '').trim()
|
||||
if (!v) return undefined
|
||||
|
||||
if (/头胎|第一胎/.test(v)) return 1
|
||||
|
||||
const num = extractNumericPart(v)
|
||||
if (num != null && num !== '') {
|
||||
const n = Number(num)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
|
||||
const taiMatch = v.match(/([一二三四五六七八九十两]+)胎/)
|
||||
if (taiMatch) {
|
||||
const n = parseChineseNumeral(taiMatch[1])
|
||||
if (n != null) return n
|
||||
}
|
||||
|
||||
const cn = parseChineseNumeral(v)
|
||||
if (cn != null) return cn
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
import { parseChineseDatetimeText } from '@/utils/chinese-datetime-parse'
|
||||
import { parseBirthOrder } from '@/utils/chinese-numeral-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 DonationSampleImportHint = {
|
||||
typeKey: 'hsc' | 'uc_msc'
|
||||
typeName: string
|
||||
collectTime: string
|
||||
}
|
||||
|
||||
export type DonationStandardImportParsed = {
|
||||
sampleTypeHeader: string
|
||||
sampleHints: DonationSampleImportHint[]
|
||||
motherName?: string
|
||||
newbornSex?: '1' | '0'
|
||||
newbornWeightGram?: number | null
|
||||
apgarScore?: number | null
|
||||
newbornDeliveryTime?: string
|
||||
/** 协议级样本采集时间:多样本时为各 hint 最早时间 */
|
||||
sampleCollectTime?: string
|
||||
maternalBloodCollectTime?: string
|
||||
maternalBloodExplicitEmpty?: boolean
|
||||
@@ -15,11 +28,19 @@ export type DonationStandardImportParsed = {
|
||||
birthOrder?: number | null
|
||||
medicalStaff?: string
|
||||
hospitalName?: string
|
||||
/** @deprecated 使用 transportModeText */
|
||||
transportExpressName?: string
|
||||
transportModeText?: string
|
||||
transportType?: 'kdys' | 'ryxd'
|
||||
salesUserName?: string
|
||||
}
|
||||
|
||||
const KEY_ALIASES: Record<string, keyof Omit<DonationStandardImportParsed, 'sampleTypeHeader'>> = {
|
||||
type CommonFieldKey = keyof Omit<
|
||||
DonationStandardImportParsed,
|
||||
'sampleTypeHeader' | 'sampleHints' | 'transportExpressName' | 'transportModeText' | 'transportType'
|
||||
>
|
||||
|
||||
const KEY_ALIASES: Record<string, CommonFieldKey> = {
|
||||
产妇姓名: 'motherName',
|
||||
母亲姓名: 'motherName',
|
||||
新生儿性别: 'newbornSex',
|
||||
@@ -30,14 +51,12 @@ const KEY_ALIASES: Record<string, keyof Omit<DonationStandardImportParsed, 'samp
|
||||
'新生儿评分5分钟': 'apgarScore',
|
||||
分娩时间: 'newbornDeliveryTime',
|
||||
新生儿分娩时间: 'newbornDeliveryTime',
|
||||
造血干细胞采集时间: 'sampleCollectTime',
|
||||
样本采集时间: 'sampleCollectTime',
|
||||
母血采集时间: 'maternalBloodCollectTime',
|
||||
分娩方式: 'deliveryMode',
|
||||
胎次: 'birthOrder',
|
||||
采集医护: 'medicalStaff',
|
||||
采集医院: 'hospitalName',
|
||||
运输方式: 'transportExpressName',
|
||||
运输方式: 'transportModeText',
|
||||
销售人员: 'salesUserName',
|
||||
采集专员: 'salesUserName'
|
||||
}
|
||||
@@ -56,18 +75,29 @@ function splitKeyValue(line: string): { key: string; value: string } | null {
|
||||
return { key: match[1].trim(), value: match[2].trim() }
|
||||
}
|
||||
|
||||
function resolveSampleCollectHint(key: string): Omit<DonationSampleImportHint, 'collectTime'> | null {
|
||||
const k = key.trim()
|
||||
if (/脐带采集时间/.test(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 parseSex(value: string): '1' | '0' | undefined {
|
||||
const v = value.trim()
|
||||
if (!v) return undefined
|
||||
if (/^男/.test(v)) return '1'
|
||||
if (/^女/.test(v)) return '0'
|
||||
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 'pgc'
|
||||
if (/顺产|自然分娩/.test(v)) return 'sc'
|
||||
return undefined
|
||||
}
|
||||
@@ -107,6 +137,33 @@ function isMaternalBloodEmpty(value: string): boolean {
|
||||
return MATERNAL_BLOOD_EMPTY_MARKERS.some((m) => v === m || v.includes(m))
|
||||
}
|
||||
|
||||
/** 运输方式文本 → 系统 transportType */
|
||||
export function resolveTransportType(text: string): 'kdys' | 'ryxd' | null {
|
||||
const v = (text || '').trim()
|
||||
if (!v) return null
|
||||
// 「顺丰车」为「顺风车」笔误,须先于「顺丰」快递匹配
|
||||
if (/顺风车|顺丰车|自行携带|人员携带|自带|自送|亲自送|自驾/.test(v)) return 'ryxd'
|
||||
if (/帮助|取样本|送到实验室|专人送|送到/.test(v)) return 'ryxd'
|
||||
if (/快递|顺丰|圆通|中通|韵达|申通|EMS|邮政|极兔|京东|德邦/.test(v)) return 'kdys'
|
||||
return null
|
||||
}
|
||||
|
||||
function addSampleHint(
|
||||
hints: DonationSampleImportHint[],
|
||||
meta: Omit<DonationSampleImportHint, 'collectTime'>,
|
||||
collectTime: string
|
||||
) {
|
||||
if (hints.some((h) => h.typeKey === meta.typeKey)) return
|
||||
hints.push({ ...meta, collectTime })
|
||||
}
|
||||
|
||||
function earliestCollectTime(hints: DonationSampleImportHint[]): string | undefined {
|
||||
if (!hints.length) return undefined
|
||||
return hints
|
||||
.map((h) => h.collectTime)
|
||||
.sort((a, b) => a.localeCompare(b))[0]
|
||||
}
|
||||
|
||||
/** 解析捐赠协议单一键导入标准文本 */
|
||||
export function parseDonationStandardImportText(text: string): DonationStandardImportParsed | null {
|
||||
const raw = (text || '').trim()
|
||||
@@ -120,13 +177,21 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
|
||||
if (!lines.length) return null
|
||||
|
||||
const result: DonationStandardImportParsed = {
|
||||
sampleTypeHeader: lines[0]
|
||||
sampleTypeHeader: lines[0],
|
||||
sampleHints: []
|
||||
}
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const kv = splitKeyValue(lines[i])
|
||||
if (!kv) continue
|
||||
|
||||
const sampleMeta = resolveSampleCollectHint(kv.key)
|
||||
if (sampleMeta) {
|
||||
const dt = parseDatetime(kv.value, true)
|
||||
if (dt) addSampleHint(result.sampleHints, sampleMeta, dt)
|
||||
continue
|
||||
}
|
||||
|
||||
const field = KEY_ALIASES[kv.key]
|
||||
if (!field) continue
|
||||
|
||||
@@ -154,11 +219,6 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
|
||||
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
|
||||
@@ -173,7 +233,7 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
|
||||
break
|
||||
}
|
||||
case 'birthOrder': {
|
||||
const n = parseScore(kv.value)
|
||||
const n = parseBirthOrder(kv.value)
|
||||
if (n !== undefined) result.birthOrder = n
|
||||
break
|
||||
}
|
||||
@@ -183,9 +243,12 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
|
||||
case 'hospitalName':
|
||||
if (!result.hospitalName) result.hospitalName = kv.value
|
||||
break
|
||||
case 'transportExpressName':
|
||||
case 'transportModeText': {
|
||||
result.transportModeText = kv.value
|
||||
result.transportExpressName = kv.value
|
||||
result.transportType = resolveTransportType(kv.value) ?? undefined
|
||||
break
|
||||
}
|
||||
case 'salesUserName':
|
||||
result.salesUserName = kv.value
|
||||
break
|
||||
@@ -194,13 +257,36 @@ export function parseDonationStandardImportText(text: string): DonationStandardI
|
||||
}
|
||||
}
|
||||
|
||||
if (result.sampleHints.length) {
|
||||
result.sampleCollectTime = earliestCollectTime(result.sampleHints)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type CellSampleTypeOption = { id: string; typeName: string }
|
||||
type CellSampleTypeOption = { id: string; typeName: string; storageYears?: number | null }
|
||||
|
||||
const SAMPLE_TYPE_SEGMENTS = ['围产期', '造血', '干细胞', '脐带', '间充质', '胎盘', '亚全能', '捐献']
|
||||
|
||||
/** 按类型名称精确或包含匹配 */
|
||||
export function matchSampleTypeByExactName(
|
||||
typeName: string,
|
||||
types: CellSampleTypeOption[]
|
||||
): CellSampleTypeOption | null {
|
||||
const target = (typeName || '').trim()
|
||||
if (!target || !types.length) return null
|
||||
|
||||
const exact = types.find((t) => t.typeName === target)
|
||||
if (exact) return exact
|
||||
|
||||
const contains = types.filter(
|
||||
(t) => t.typeName.includes(target) || target.includes(t.typeName)
|
||||
)
|
||||
if (contains.length === 1) return contains[0]
|
||||
|
||||
return matchCellSampleType(target, types)
|
||||
}
|
||||
|
||||
/** 根据首行文本与细胞样本类型列表做语义匹配 */
|
||||
export function matchCellSampleType(
|
||||
headerLine: string,
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
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'
|
||||
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]
|
||||
}
|
||||
|
||||
/** 缴费方式:仅「分期」为 fq,其余(含微信/趸交)均为 dj */
|
||||
export function parsePayModeFromText(value: string): {
|
||||
payMode: 'dj' | 'fq'
|
||||
transferLabel?: string
|
||||
} {
|
||||
const v = (value || '').trim()
|
||||
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 }
|
||||
@@ -41,7 +41,17 @@ export type AttachmentZipExportResult = {
|
||||
const PREPARING_MAX = 5
|
||||
const DOWNLOADING_MAX = 85
|
||||
|
||||
function isInboundQcWorkOrder(row: QcWorkOrder) {
|
||||
return (row.workOrderType || 'inbound') === 'inbound'
|
||||
}
|
||||
|
||||
function workOrderFolderName(row: QcWorkOrder, used: Set<string>) {
|
||||
if (isInboundQcWorkOrder(row)) {
|
||||
const labCode = sanitizeZipPathSegment(row.labStorageCode || '', '未命名')
|
||||
const workOrderNo = sanitizeZipPathSegment(row.workOrderNo || row.id, '工单')
|
||||
const base = `入库${labCode}_${workOrderNo}`
|
||||
return ensureUniqueZipEntryName(base, used)
|
||||
}
|
||||
const parts = [sanitizeZipPathSegment(row.workOrderNo || row.id, '工单')]
|
||||
if (row.sampleCode) {
|
||||
parts.push(`样本${sanitizeZipPathSegment(row.sampleCode)}`)
|
||||
@@ -49,15 +59,34 @@ function workOrderFolderName(row: QcWorkOrder, used: Set<string>) {
|
||||
return ensureUniqueZipEntryName(parts.join('_'), used)
|
||||
}
|
||||
|
||||
function batchFolderName(batch: QcInspectionBatch, used: Set<string>) {
|
||||
const no = sanitizeZipPathSegment(batch.batchNo || '', '')
|
||||
function applyInboundLabStorageCodePrefix(code: string, workOrder: QcWorkOrder) {
|
||||
if (!isInboundQcWorkOrder(workOrder)) return code
|
||||
const raw = String(code || '').trim()
|
||||
const workOrderNo = String(workOrder.workOrderNo || '').trim()
|
||||
if (!raw || !workOrderNo || !raw.startsWith(workOrderNo)) return raw
|
||||
const labCode = sanitizeZipPathSegment(workOrder.labStorageCode || '', '未命名')
|
||||
return sanitizeZipPathSegment(`${labCode}${raw.slice(workOrderNo.length)}`, '')
|
||||
}
|
||||
|
||||
function batchFolderName(batch: QcInspectionBatch, workOrder: QcWorkOrder, used: Set<string>) {
|
||||
const no = sanitizeZipPathSegment(
|
||||
applyInboundLabStorageCodePrefix(batch.batchNo || '', workOrder),
|
||||
''
|
||||
)
|
||||
const name = sanitizeZipPathSegment(batch.batchName || '未命名批次')
|
||||
const base = no ? `${no}_${name}` : name || `批次${batch.orderNum ?? 1}`
|
||||
return ensureUniqueZipEntryName(base, used)
|
||||
}
|
||||
|
||||
function taskFolderName(task: { taskNo?: string; itemName?: string }, used: Set<string>) {
|
||||
const no = sanitizeZipPathSegment(task.taskNo || '', '')
|
||||
function taskFolderName(
|
||||
task: { taskNo?: string; itemName?: string },
|
||||
workOrder: QcWorkOrder,
|
||||
used: Set<string>
|
||||
) {
|
||||
const no = sanitizeZipPathSegment(
|
||||
applyInboundLabStorageCodePrefix(task.taskNo || '', workOrder),
|
||||
''
|
||||
)
|
||||
const name = sanitizeZipPathSegment(task.itemName || '未命名任务')
|
||||
const base = no ? `${no}_${name}` : name
|
||||
return ensureUniqueZipEntryName(base, used)
|
||||
@@ -76,7 +105,7 @@ export function buildQcWorkOrderZipEntries(
|
||||
const usedBatchFolders = new Set<string>()
|
||||
|
||||
for (const batch of batches) {
|
||||
const batchFolder = batchFolderName(batch, usedBatchFolders)
|
||||
const batchFolder = batchFolderName(batch, wo, usedBatchFolders)
|
||||
const tasks = batch.tasks || []
|
||||
const usedTaskFolders = new Set<string>()
|
||||
|
||||
@@ -84,7 +113,7 @@ export function buildQcWorkOrderZipEntries(
|
||||
const files = task.attachmentFiles?.filter((f) => String(f.fileUrl || '').trim()) || []
|
||||
if (!files.length) continue
|
||||
|
||||
const taskFolder = taskFolderName(task, usedTaskFolders)
|
||||
const taskFolder = taskFolderName(task, wo, usedTaskFolders)
|
||||
const usedFileNames = new Set<string>()
|
||||
const contextHint = [wo.workOrderNo, batch.batchName, task.itemName]
|
||||
.filter(Boolean)
|
||||
|
||||
Reference in new issue
Block a user