This commit is contained in:
zlwu committed 2026-07-02 12:25:00 +08:00
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)