/** * 三方车辆/人员客户文本粘贴解析 * 标准格式见 `PL_PASTE_SAMPLE_TEXT`。 */ /** * 粘贴识别输入框预填示例(格式与解析规则一致,敏感字段已脱敏,供用户对照改写) * 姓名保留姓、手机保留前 3 后 4、身份证保留前 6 后 4、车牌保留省份简称后打码 */ export const PL_PASTE_SAMPLE_TEXT = [ '车牌:皖******皖******挂', '核载:34 吨', '驾驶员:张**', '身份证号:340000********0000', '手机号:138****0000', '押运员:李**', '身份证号:340000********0000', '电话:139****0000' ].join('\n') export interface PlPastePersonnelDraft { personnelName: string phoneNo?: string idCardNo?: string personnelType: 'driver' | 'escort' } export interface PlPasteVehicleDraft { licensePlate: string trailerNo?: string loadCapacity?: number } export interface PlPasteRecognizeResult { vehicle?: PlPasteVehicleDraft driver?: PlPastePersonnelDraft escort?: PlPastePersonnelDraft warnings: string[] } /** 普通蓝牌 7 位、挂车带「挂」、新能源约 8 位;每次新建避免 lastIndex 残留 */ function plateTokenRe() { return /[\u4e00-\u9fa5][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,6}挂?/gi } const PLATE_LINE_LABEL = /^(?:车牌号|车牌|主车号|主车|号牌)[::]\s*/i const TRAILER_LINE_LABEL = /^(?:挂车号|挂车)[::]\s*/i const ID_CARD_LINE = /^(?:身份证号码|身份证号|身份证|证件号)[::]?\s*([0-9Xx]{15,18})$/ function extractPlates(text: string): string[] { const compact = text.replace(/\s+/g, '').toUpperCase() const found = compact.match(plateTokenRe()) || [] return found } function remainderAfterPlates(text: string, plates: string[]): string { let rest = text.replace(/\s+/g, '').toUpperCase() for (const plate of plates) { rest = rest.replace(plate.toUpperCase(), '') } return rest.replace(/[,,、;;\-_//]/g, '') } function applyPlatesToVehicle( vehicle: PlPasteVehicleDraft | undefined, plates: string[] ): PlPasteVehicleDraft { const next = vehicle || { licensePlate: '' } if (plates[0] && !next.licensePlate) { next.licensePlate = plates[0] } if (plates[1] && !next.trailerNo) { next.trailerNo = plates[1] } else if (plates.length === 1 && plates[0].endsWith('挂') && next.licensePlate && !next.trailerNo) { next.trailerNo = plates[0] if (next.licensePlate === plates[0]) { next.licensePlate = '' } } return next } export function parsePlCustomerVehicleText(raw: string): PlPasteRecognizeResult { const warnings: string[] = [] const lines = raw .split(/\r?\n/) .map(l => l.trim()) .filter(Boolean) let vehicle: PlPasteVehicleDraft | undefined let driver: PlPastePersonnelDraft | undefined let escort: PlPastePersonnelDraft | undefined let section: 'none' | 'driver' | 'escort' = 'none' for (const line of lines) { if (TRAILER_LINE_LABEL.test(line)) { const plates = extractPlates(line.replace(TRAILER_LINE_LABEL, '')) if (plates.length) { if (!vehicle) vehicle = { licensePlate: '' } vehicle.trailerNo = plates[0] continue } } const plateLabeled = PLATE_LINE_LABEL.test(line) const plateSource = plateLabeled ? line.replace(PLATE_LINE_LABEL, '') : line const plates = extractPlates(plateSource) if (plates.length && (plateLabeled || !remainderAfterPlates(plateSource, plates))) { vehicle = applyPlatesToVehicle(vehicle, plates) continue } const load = line.match(/核载[::]?\s*([\d.]+)\s*吨?/) if (load) { if (!vehicle) { vehicle = { licensePlate: '' } } vehicle.loadCapacity = Number(load[1]) continue } if (/^驾驶员[::]/.test(line)) { section = 'driver' driver = { personnelName: line.replace(/^驾驶员[::]\s*/, '').trim(), personnelType: 'driver' } continue } if (/^押运员[::]/.test(line)) { section = 'escort' escort = { personnelName: line.replace(/^押运员[::]\s*/, '').trim(), personnelType: 'escort' } continue } const phone = line.match(/^(?:手机号|电话)[::]?\s*(\d{11})$/) if (phone) { if (section === 'driver' && driver) { driver.phoneNo = phone[1] continue } if (section === 'escort' && escort) { escort.phoneNo = phone[1] continue } } const idCard = line.match(ID_CARD_LINE) if (idCard) { const no = idCard[1].toUpperCase() if (section === 'driver' && driver) { driver.idCardNo = no continue } if (section === 'escort' && escort) { escort.idCardNo = no continue } } warnings.push(`未识别行:${line}`) } if (vehicle && !vehicle.licensePlate.trim()) { warnings.push('未识别到主车车牌') } return { vehicle, driver, escort, warnings } }