/** * 路线变更记录内容解析 * * 存储格式(单行): * YD-20260904-0022:发货地 A → B;到货地 C → D;YD-20260904-0025:发货地 E → F;到货地 G → H * 路线顺序:A → B → C 调整为 B → A → C * * 解析结果(分组,供变更记录列表按「分组标题 - 字段 - 变更前/后」渲染): * [ * { * title: 'YD-20260904-0022', * text: '', * fields: [{ label: '发货地', before: 'A', after: 'B', single: 'B', changed: true, unchanged: false }], * }, * { title: '', text: '无法识别的原文', fields: [] }, * ] * * 说明:仅用于展示层,不改变已存储的 content 文本(历史数据同样适用)。 */ // 长标签必须排在短标签前(「发货地址」优先于「发货地」),否则会被前缀吃掉 const FIELD_LABELS = ['发货地址', '到货地址', '发货地', '到货地', '装货地', '卸货地', '收货地']; const FIELD_LABEL_PATTERN = new RegExp(`^(${FIELD_LABELS.join('|')})\\s*([\\s\\S]*)$`); // 变更前 / 变更后分隔符:→、➔、➜、->、-->、–>、—> const ARROW_PATTERN = /\s*(?:→|➔|➜|[\-–—]{1,2}\s*>)\s*/; // 运单地址之外的变更描述,例如「A → B → C 调整为 B → A → C」 const ADJUST_PATTERN = /^([\s\S]*?)\s调整为\s*([\s\S]*)$/; const EMPTY_TEXT = ['', '-', '--', '—', 'null', 'undefined']; // 冒号前内容超过该长度时不视为分组标题(运单号通常长于该值) const TITLE_MAX_LENGTH = 12; const normalizeValue = value => { const text = String(value === undefined || value === null ? '' : value).trim(); return EMPTY_TEXT.includes(text.toLowerCase()) ? '-' : text; }; const buildField = (label, rawValue) => { const value = String(rawValue === undefined || rawValue === null ? '' : rawValue).trim(); const adjustMatched = value.match(ADJUST_PATTERN); let before = value; let after = ''; let hasPair = false; if (adjustMatched) { before = adjustMatched[1]; after = adjustMatched[2]; hasPair = true; } else { const parts = value.split(ARROW_PATTERN); if (parts.length > 1) { before = parts[0]; after = parts[1]; hasPair = true; } } const beforeText = normalizeValue(before); const afterText = normalizeValue(after); return { label, before: beforeText, after: afterText, // 单值展示(未拆分出前后值时使用) single: hasPair ? afterText : beforeText, changed: hasPair && beforeText !== afterText, unchanged: hasPair && beforeText === afterText, }; }; const parseSegment = segment => { // 形如「发货地 A → B」(前面只有「;」分隔,无分组标题)→ 归入上一条运单 const leadingMatched = segment.match(FIELD_LABEL_PATTERN); if (leadingMatched && leadingMatched[2].trim()) { return { type: 'field', title: '', text: '', fields: [buildField(leadingMatched[1], leadingMatched[2])], }; } const colonIndex = segment.search(/[::]/); if (colonIndex === -1) { return { type: 'text', title: '', text: segment, fields: [] }; } const head = segment.slice(0, colonIndex).trim(); const tail = segment.slice(colonIndex + 1).trim(); const fieldMatched = tail.match(FIELD_LABEL_PATTERN); // 形如「YD-20260904-0022:发货地 A → B」 if (fieldMatched && fieldMatched[2].trim()) { return { type: 'waybill', title: head, text: '', fields: [buildField(fieldMatched[1], fieldMatched[2])], }; } // 形如「路线顺序:A → B → C 调整为 B → A → C」 if (head && head.length <= TITLE_MAX_LENGTH) { return { type: 'group', title: head, text: '', fields: [buildField('', tail)] }; } return { type: 'text', title: '', text: segment, fields: [] }; }; export const parseRouteChangeContent = content => { if (content === undefined || content === null) return []; const text = String(content).replace(/\r\n?/g, '\n').trim(); if (!text) return []; const segments = text .split(/[;;\n]+/) .map(item => item.trim()) .filter(Boolean); const groups = []; segments.forEach(segment => { const group = parseSegment(segment); const last = groups[groups.length - 1]; // 同一运单下的「到货地…」等无标题字段,合并进上一个运单分组 if (group.type === 'field' && last && (last.type === 'waybill' || last.type === 'field')) { last.fields.push(...group.fields); return; } groups.push(group); }); return groups; };