feat(vehicle): 归一化保险记录导入日期格式,修复设备台账批量删除按钮
- 保险记录批量导入时,前端读取 Excel 并规范日期格式为 YYYY-MM-DD,兼容多种日期写法 - 重写 `handleImport` 支持自定义上传逻辑,调用归一化处理后再上传文件 - 添加日期归一化相关方法,确保不合法或已标准格式保持原值 - 设备台账界面新增批量删除按钮及相关逻辑,支持多选数据批量删除并提示确认 - 补充导出含创建时间和更新时间的后端修复说明,避免导出时间字段为空问题 - 说明后端导出分配逻辑,强调导出字段由后端控制,前端修改导出列无效
This commit is contained in:
@@ -10,3 +10,27 @@
|
|||||||
3. 模板 `el-timeline` 的 `v-for` 改用 `orderedWaybillPunchRecords`;`key` 由 `record.nodeCode || record.id || index` 改为 `${nodeCode||id||'punch'}-${index}`,避免同节点多次打卡(在途)时 key 重复。
|
3. 模板 `el-timeline` 的 `v-for` 改用 `orderedWaybillPunchRecords`;`key` 由 `record.nodeCode || record.id || index` 改为 `${nodeCode||id||'punch'}-${index}`,避免同节点多次打卡(在途)时 key 重复。
|
||||||
- 校验:项目根目录临时脚本 `_verify-sfc.mjs`(@vue/compiler-sfc parse + compileScript + compileTemplate)通过,已删除脚本。
|
- 校验:项目根目录临时脚本 `_verify-sfc.mjs`(@vue/compiler-sfc parse + compileScript + compileTemplate)通过,已删除脚本。
|
||||||
- 注意:`getPunchRecords`(`src/api/business/waybill-manage.js` → `/punch-records`)后端未提供节点排序字段,排序只能在前端做。
|
- 注意:`getPunchRecords`(`src/api/business/waybill-manage.js` → `/punch-records`)后端未提供节点排序字段,排序只能在前端做。
|
||||||
|
|
||||||
|
## 保险记录批量导入:前端归一化日期格式
|
||||||
|
|
||||||
|
- 文件:`src/views/vehicle/insurance-record.vue`(仅此一个文件)
|
||||||
|
- 背景:保险记录批量导入是**后端驱动**——前端 `handleImport` 经 `importBlob` 把原始 Excel 直接 POST 到 `/blade-transport/insurance-record/import-insurance-record`,前端不解析行。后端只认 `2026-09-01`,`2026-9-1` 导入失败。
|
||||||
|
- 决策:用户选择**前端归一化**(不动后端)。把 `openImportDialog` 改为自写 `httpRequest`:用 `xlsx` 读取 Excel → 把日期列(开始日期/结束日期/开票日期)`2026-9-1` 归一为 `2026-09-01` → 重写成新 File 再交给 `handleImportExcel` 上传;复用其失败明细下载与成功刷新逻辑。无日期列时回退为原文件(行为不变)。
|
||||||
|
- 关键方法:`normalizeExcelDates` / `readExcelRows` / `writeExcelFile` / `normalizeDateString`(正则 `^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$`,用 `new Date` 校验合法性后补零;非法/已标准格式返回 null 保持原值)。
|
||||||
|
- 校验:本机 dev(localhost:2889)待用户部署自测。注意所有单元格经 `sheet_to_json(raw:false)` 后均为字符串,与 transport-plan-import 的客户端解析模式一致。
|
||||||
|
|
||||||
|
## 车/船务模块导出 xlsx「创建时间/更新时间」为空(后端 tms-api 修复)
|
||||||
|
|
||||||
|
- 现象:`/vehicle/insurance-record` 批量导出的 xlsx 里「创建时间」「更新时间」两列空白,「更新人」有值。用户要求车/船务模块所有页面统一修。
|
||||||
|
- 定位:导出由**后端**生成(前端只传查询参数,`exportColumns` 参数后端未使用)。实测 `~/Downloads/保险记录2026-09-17 20_46_08.xlsx`:K/M 列单元格无 `<v>` 节点(值 null),E/F/J 日期列是数值 46266.0(带 numFmt `yyyy-MM-dd`,WPS 显示为日期)。
|
||||||
|
- 根因:Service 用 BladeX `org.springblade.core.tool.utils.BeanUtil`(**继承 Spring `BeanUtils`**)做 `copyProperties`。实体 `TenantEntity.createTime/updateTime` 是 `java.util.Date`,导出类字段是 `java.time.LocalDateTime` → Spring BeanUtils **类型不兼容静默跳过**,值为 null。而「更新人」是手动 `UserCache.getUserRealName(...)` 赋值的,所以有值。(Hutool 的 BeanUtil 能转 Date→LocalDateTime,但本项目用的是 BladeX 版本,不能。)
|
||||||
|
- 佐证:项目里正常的导出(Waybill / LoadingManage / CustomerArchive / CommonCargo)时间字段一律用 `java.util.Date`,并在 Service 里手动 `excel.setCreateTime(record.getCreateTime())`。
|
||||||
|
- 修复(后端仓库 `/Users/gxwebsoft/JAVA/tms-api`,blade-transport 模块):在 13 个导出方法里补显式赋值(Date→LocalDateTime 用 BladeX `DateUtil.fromDate`,`Func.isEmpty` 判空),不改 Excel 类字段类型(避免影响自定义 converter 与导入模板)。
|
||||||
|
```java
|
||||||
|
excel.setCreateTime(Func.isEmpty(x.getCreateTime()) ? null : DateUtil.fromDate(x.getCreateTime()));
|
||||||
|
excel.setUpdateTime(Func.isEmpty(x.getUpdateTime()) ? null : DateUtil.fromDate(x.getUpdateTime()));
|
||||||
|
```
|
||||||
|
- 涉及文件(13 个 Service + 1 新建 Excel 类 + 设备台账 controller/接口):InsuranceRecord / ViolationRecord / MaintenanceRecord / MaintenancePlan / TireReplacementRecord / AccidentRecord / AnnualInspectionRecord / MileageRecord / TransportChangeRecord / EtcRecord / OilElectricRecord / OtherExpenseRecord / EquipmentLedger。
|
||||||
|
- 设备台账特殊:原 `EquipmentLedgerExcel` 根本没有审计列(前端列表有),新建 `EquipmentLedgerExportExcel extends EquipmentLedgerExcel`(创建时间/更新人/更新时间),导出链路改用新类,**导入模板仍用旧类不受影响**。
|
||||||
|
- 未验证:本机无 mvn(仅 IDEA 内置),且离线模式缺 `blade-bom` 无法编译;用 javap 确认 `DateUtil.fromDate(Date)` 存在 + 逐行 grep 复核代替。需用户在 IDEA 里编译并重新部署后自测。
|
||||||
|
- 同类隐患(本次未改,其他模块):TemporaryCreditLimit、ProcessConfig、CommonRoute、CommonAddress、ContractManage、ProjectApply、ShippingTemplate 的导出 Excel 也是 LocalDateTime 且未手动 set 时间。
|
||||||
|
|||||||
@@ -111,3 +111,9 @@
|
|||||||
- option 由 prop `crudOption` 经 `cloneOption` 克隆。
|
- option 由 prop `crudOption` 经 `cloneOption` 克隆。
|
||||||
- 独立表单页走 `PageAvueForm`,不包 el-dialog。
|
- 独立表单页走 `PageAvueForm`,不包 el-dialog。
|
||||||
- 自定义详情弹窗:`config.detailButton=true` + `detailSections:[{title, fields}]`。
|
- 自定义详情弹窗:`config.detailButton=true` + `detailSections:[{title, fields}]`。
|
||||||
|
|
||||||
|
## 配套后端工程与导出字段约定
|
||||||
|
- **本前端工程 tms-erp-web-ws 对应的后端是 `/Users/gxwebsoft/JAVA/tms-api`**(不是 `tms-erp-api-ws`,后者功能滞后,排查时不要看错仓库)。
|
||||||
|
- 所有列表导出(批量导出/下载模板)由后端 FastExcel 生成,前端 `exportColumns` 参数后端未使用 —— 导出列以 `XxxExportExcel.java` 为准,前端改 option 不影响导出。
|
||||||
|
- BladeX `org.springblade.core.tool.utils.BeanUtil` **继承 Spring `BeanUtils`**:字段类型不兼容时**静默跳过**(实体 `Date createTime` → Excel `LocalDateTime createTime` 会丢值)。所以 Excel 导出的审计时间必须在 Service 里手动赋值,并用 `DateUtil.fromDate(...)` 做 Date→LocalDateTime 转换(`Func.isEmpty` 判空)。参考写法见 Waybill/InsuranceRecord 的 `exportXxx()`。
|
||||||
|
- 排查导出文件问题:用 Python 标准库 `zipfile` + 解析 `xl/worksheets/sheet1.xml`、`xl/styles.xml` 直接看单元格有无 `<v>` 与 numFmt,无需装 openpyxl。
|
||||||
|
|||||||
@@ -21,6 +21,15 @@
|
|||||||
@on-load="onLoad"
|
@on-load="onLoad"
|
||||||
>
|
>
|
||||||
<template #menu-left>
|
<template #menu-left>
|
||||||
|
<el-button
|
||||||
|
v-if="hasPermission('equipment_ledger_delete')"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
plain
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="hasPermission('equipment_ledger_import')"
|
v-if="hasPermission('equipment_ledger_import')"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -310,6 +319,23 @@ export default {
|
|||||||
this.$message.success('操作成功');
|
this.$message.success('操作成功');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleDelete() {
|
||||||
|
if (this.selectionList.length === 0) {
|
||||||
|
this.$message.warning('请选择至少一条数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm('确认删除选中的数据?删除后将不可恢复!', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => remove(this.ids))
|
||||||
|
.then(() => {
|
||||||
|
this.onLoad(this.page);
|
||||||
|
this.$message.success('操作成功');
|
||||||
|
this.$refs.crud.toggleSelection();
|
||||||
|
});
|
||||||
|
},
|
||||||
beforeOpen(done, type) {
|
beforeOpen(done, type) {
|
||||||
this.boxType = type;
|
this.boxType = type;
|
||||||
if (type === 'add') {
|
if (type === 'add') {
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ import { getList as getOcrTemplateList } from '@/api/base/insurance-ocr-template
|
|||||||
import { getDeptTree } from '@/api/system/dept';
|
import { getDeptTree } from '@/api/system/dept';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { handleImportExcel } from '@/utils/import-excel';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
@@ -540,7 +540,96 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleImport() {
|
handleImport() {
|
||||||
openImportDialog(this, '保险记录');
|
const column = this.findColumn(this.excelOption.column, 'excelFile');
|
||||||
|
if (!column) {
|
||||||
|
this.$message.error('导入配置异常,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 上传前先归一化日期列(2026-9-1 → 2026-09-01),兼容两种格式
|
||||||
|
column.httpRequest = async (uploadOption, uploadColumn) => {
|
||||||
|
try {
|
||||||
|
const normalizedFile = await this.normalizeExcelDates(uploadOption.file);
|
||||||
|
await handleImportExcel(
|
||||||
|
this,
|
||||||
|
{ ...uploadOption, file: normalizedFile },
|
||||||
|
uploadColumn,
|
||||||
|
'保险记录',
|
||||||
|
null,
|
||||||
|
{ timeout: 60000 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error(error.message || '导入失败');
|
||||||
|
if (typeof uploadOption.onError === 'function') {
|
||||||
|
uploadOption.onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.excelBox = true;
|
||||||
|
},
|
||||||
|
// 读取 Excel,把指定日期列统一归一化为 YYYY-MM-DD,再写回新文件
|
||||||
|
async normalizeExcelDates(file) {
|
||||||
|
const rows = await this.readExcelRows(file);
|
||||||
|
const dateHeaders = ['开始日期', '结束日期', '开票日期'];
|
||||||
|
const headerIndex = (rows[0] || []).reduce((map, label, index) => {
|
||||||
|
const key = String(label || '').trim();
|
||||||
|
if (key) map[key] = index;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
const targetIndexes = dateHeaders
|
||||||
|
.map(label => headerIndex[label])
|
||||||
|
.filter(index => index !== undefined);
|
||||||
|
if (!targetIndexes.length) return file;
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
if (rowIndex === 0) return;
|
||||||
|
targetIndexes.forEach(index => {
|
||||||
|
if (row[index] !== undefined && row[index] !== '') {
|
||||||
|
const normalized = this.normalizeDateString(String(row[index]));
|
||||||
|
if (normalized) row[index] = normalized;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this.writeExcelFile(rows, file.name);
|
||||||
|
},
|
||||||
|
async readExcelRows(file) {
|
||||||
|
const XLSX = await import('xlsx');
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const workbook = XLSX.read(buffer, { type: 'array', cellDates: false });
|
||||||
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
return XLSX.utils.sheet_to_json(worksheet, {
|
||||||
|
header: 1,
|
||||||
|
defval: '',
|
||||||
|
raw: false,
|
||||||
|
blankrows: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async writeExcelFile(rows, fileName) {
|
||||||
|
const XLSX = await import('xlsx');
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), 'Sheet1');
|
||||||
|
const binary = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||||
|
return new File([binary], fileName || 'insurance-record.xlsx', {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 兼容 YYYY-M-D / YYYY-MM-DD / YYYY/M/D / YYYY.年.月.日,非法或已标准格式原样返回 null
|
||||||
|
normalizeDateString(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) return null;
|
||||||
|
const match = text.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
|
||||||
|
if (!match) return null;
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
if (
|
||||||
|
date.getFullYear() !== year ||
|
||||||
|
date.getMonth() !== month - 1 ||
|
||||||
|
date.getDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
return `${year}-${pad(month)}-${pad(day)}`;
|
||||||
},
|
},
|
||||||
handleExport() {
|
handleExport() {
|
||||||
this.$confirm('是否导出保险记录数据?', '提示', {
|
this.$confirm('是否导出保险记录数据?', '提示', {
|
||||||
|
|||||||
Reference in New Issue
Block a user