refactor(payment): 统一搜索栏“展开/收起”按钮实现

- 取消 el-tooltip 包裹,改用纯 el-button 实现带图标的展开收起按钮
- 按 avue-crud 规范控制按钮显示,新增 searchIndex (默认 4)和 searchItemCount 控制逻辑
- 搜索项数量未超出 searchIndex 时隐藏展开收起按钮
- 相关组件支付模块多处应用此统一方案(bill-ledger-search、bill-payment-search、invoice-application、invoice-receipt、receipt-claim-record、receipt-flow)
- 维护展开状态对应的箭头图标和文案为“收起”与“展开”

refactor(business): 优化路线变更内容渲染与样式

- 替换旧的 formatRouteChangeContent 为更详细的 parseRouteChangeContent,支持多分组与字段显示
- 路线变更内容支持多层次分组、字段标签、改变前后值对比及未变更标识展示
- 取消全局列溢出提示以避免多行内容被截断,针对路线变更内容列显式禁用溢出提示并设置白色换行样式
- 在 business-crud-page、waybill-manage-page、loading-manage 相关页面和组件中统一应用该解析与渲染方案

refactor(option): 调整 measurement-unit 配置以提升表格列展示

- 移除 menuWidth 配置,采用 menuFixed 固定右侧模式
- 为创建人和更新人列添加 minWidth 110,时间列添加 width 180,避免内容被截断
- 表格列数组顺序调整,确保搜索与展示保持一致

refactor(payment & receipt-flow): 统一支付及回执模块工具栏和表格行为

- 按照统一规范和用户反馈,调整多处页面的搜索栏操作区和表格列配置
- 引入 TableHeaderTools 组件和相关状态传递,实现搜索显隐与表格尺寸切换统一
- 优化 avue-crud 表格组件的交互一致性和视觉表现

refactor(utils): 重构路线变更内容解析函数

- 新增 parseRouteChangeContent 以支持复杂路线变更文本的解析与分组
- 识别运单号、字段标签、变更前后值,支持多种箭头符号及调整描述
- 用法推广至相关业务页面,支持更加直观的路线变更展示界面
This commit is contained in:
2026-10-14 16:21:50 +08:00
parent 749b314ceb
commit b39501291c
13 changed files with 670 additions and 77 deletions
+123 -19
View File
@@ -1,29 +1,133 @@
/**
* 路线变更记录内容格式化
* 路线变更记录内容解析
*
* 存储格式(单行):
* YD-20260904-0022:发货地 A → B;到货地 C → D;YD-20260904-0025:发货地 E → F;到货地 G → H
* 路线顺序:A → B → C 调整为 B → A → C
*
* 展示格式(变更前 / 变更后分行):
* YD-20260904-0022:发货地 A →
* B
* 到货地 C →
* D
* 解析结果(分组,供变更记录列表按「分组标题 - 字段 - 变更前/后」渲染):
* [
* {
* title: 'YD-20260904-0022',
* text: '',
* fields: [{ label: '发货地', before: 'A', after: 'B', single: 'B', changed: true, unchanged: false }],
* },
* { title: '', text: '无法识别的原文', fields: [] },
* ]
*
* 说明:仅用于展示层,不改变已存储的 content 文本(历史数据同样适用)。
*/
export const formatRouteChangeContent = (content, emptyText = '-') => {
if (content === undefined || content === null) return emptyText;
const text = String(content).trim();
if (!text) return emptyText;
// 长标签必须排在短标签前(「发货地址」优先于「发货地」),否则会被前缀吃掉
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;
return text
.replace(/\r\n?/g, '\n')
// 多条记录之间换行
.replace(/[ \t]*[;;][ \t]*/g, '\n')
// 变更前地址 / 变更后地址分行(箭头保留在上一行行尾)
.replace(/[ \t]*[-—]+>[ \t]*|[ \t]*→[ \t]*/g, ' →\n')
.replace(/\n{2,}/g, '\n')
.replace(/[ \t]+\n/g, '\n')
.trim();
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;
};