From 8b1d7ed2803ba9a074bef2817b0a4e2c9651636e Mon Sep 17 00:00:00 2001 From: Joshi <3040996759@qq.com> Date: Mon, 1 Jun 2026 15:14:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(wms/report):=20=E6=96=B0=E5=A2=9E=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=AF=BC=E5=87=BA=E5=88=97=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在ExcelUtil工具类中新增exportExcelOrdered方法,支持按指定顺序的列动态生成表头和数据行进行导出 2. 重构接收报表页面的自定义导出弹窗:将布局拆分为左侧可选列面板和右侧导出顺序面板,支持拖拽排序 3. 新增后端/exportCustomOrdered接口,接收有序字段列表并调用新的导出方法 4. 优化弹窗样式:调整宽度、间距、滚动区域,新增顺序序号和移除按钮 5. 移除原有的/exportCustom接口,统一使用新的有序导出逻辑 --- .../com/klp/common/utils/poi/ExcelUtil.java | 68 ++++++ klp-ui/src/views/wms/report/receive.vue | 211 +++++++++++++++--- .../controller/WmsMaterialCoilController.java | 51 +++-- 3 files changed, 277 insertions(+), 53 deletions(-) diff --git a/klp-common/src/main/java/com/klp/common/utils/poi/ExcelUtil.java b/klp-common/src/main/java/com/klp/common/utils/poi/ExcelUtil.java index 773abe33..b23d37a5 100644 --- a/klp-common/src/main/java/com/klp/common/utils/poi/ExcelUtil.java +++ b/klp-common/src/main/java/com/klp/common/utils/poi/ExcelUtil.java @@ -26,10 +26,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * Excel相关处理 @@ -353,4 +358,67 @@ public class ExcelUtil { return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx"; } + /** + * 导出excel(按指定顺序的列导出,使用动态表头) + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param orderedFields 按导出顺序排列的Java字段名列表 + * @param fieldLabelMap Java字段名 -> Excel列头中文名 映射 + * @param response 响应体 + */ + public static void exportExcelOrdered(List list, String sheetName, + List orderedFields, + Map fieldLabelMap, + HttpServletResponse response) { + if (orderedFields == null || orderedFields.isEmpty()) { + throw new IllegalArgumentException("导出列不能为空"); + } + // 构建动态表头 + List> heads = orderedFields.stream() + .map(f -> Collections.singletonList(fieldLabelMap.getOrDefault(f, f))) + .collect(Collectors.toList()); + + // 构建数据行 + List> data = new ArrayList<>(list.size()); + if (!list.isEmpty()) { + Map fieldCache = new HashMap<>(); + Class clazz = list.get(0).getClass(); + for (T vo : list) { + List row = new ArrayList<>(orderedFields.size()); + for (String fieldName : orderedFields) { + Field field = fieldCache.computeIfAbsent(fieldName, k -> { + try { + Field f = clazz.getDeclaredField(k); + f.setAccessible(true); + return f; + } catch (NoSuchFieldException e) { + return null; + } + }); + try { + row.add(field != null ? field.get(vo) : null); + } catch (IllegalAccessException e) { + row.add(null); + } + } + data.add(row); + } + } + + try { + resetResponse(sheetName, response); + ServletOutputStream os = response.getOutputStream(); + EasyExcel.write(os) + .head(heads) + .autoCloseStream(false) + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerConverter(new ExcelBigNumberConvert()) + .sheet(sheetName) + .doWrite(data); + } catch (IOException e) { + throw new RuntimeException("导出Excel异常", e); + } + } + } diff --git a/klp-ui/src/views/wms/report/receive.vue b/klp-ui/src/views/wms/report/receive.vue index 21bd8ca3..2f585050 100644 --- a/klp-ui/src/views/wms/report/receive.vue +++ b/klp-ui/src/views/wms/report/receive.vue @@ -87,9 +87,9 @@ - +
- +
全选 反选 @@ -97,25 +97,47 @@
- -
-
{{ gName }}
-
- {{ field.label }} -
+
+
可选列
+
+ +
+
{{ gName }}
+
+ {{ field.label }} +
+
+
- +
+
+
+ 导出顺序 + {{ orderedColumns.length }} +
+
+ +
+ + {{ orderedColumns.indexOf(field) + 1 }} + {{ exportColumns[field] || field }} + +
+
+
勾选左侧列后出现在此处,可拖拽排序
+
+
@@ -140,6 +162,7 @@ import TimeRangePicker from "@/views/wms/report/components/timeRangePicker.vue"; import HierarchicalPivot from "@/views/wms/report/components/hierarchicalPivot/index.vue"; import CrossTable from "@/views/wms/report/components/crossTable/index.vue"; import { saveReportFile } from "@/views/wms/report/js/reportFile"; +import draggable from 'vuedraggable'; export default { @@ -155,6 +178,7 @@ export default { TimeRangePicker, HierarchicalPivot, CrossTable, + draggable, }, dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'], data() { @@ -184,6 +208,7 @@ export default { customExportVisible: false, exportColumns: {}, selectedColumns: [], + orderedColumns: [], columnSearch: '', columnGroups: { '基本信息': ['itemTypeDesc', 'warehouseName', 'actualWarehouseName', 'dataTypeText'], @@ -287,6 +312,22 @@ export default { }, }, + watch: { + selectedColumns: { + immediate: false, + handler(nv, ov) { + const newSet = new Set(nv) + const oldSet = ov ? new Set(ov) : new Set() + // 移除取消勾选的列 + this.orderedColumns = this.orderedColumns.filter(f => newSet.has(f)) + // 新勾选的追加到末尾 + const added = nv.filter(f => !oldSet.has(f)) + if (added.length) { + this.orderedColumns.push(...added) + } + } + } + }, methods: { // 加载列设置 loadColumns() { @@ -354,14 +395,17 @@ export default { this.customExportVisible = true }) }, - // 执行自定义导出 + // 执行自定义导出(按 orderedColumns 顺序) doCustomExport() { this.customExportVisible = false - this.download('wms/materialCoil/exportCustom', { + this.download('wms/materialCoil/exportCustomOrdered', { coilIds: this.coilIds, - columns: this.selectedColumns.join(','), + columnsOrdered: this.orderedColumns.join(','), }, `materialCoil_${new Date().getTime()}.xlsx`) }, + removeOrderedField(field) { + this.selectedColumns = this.selectedColumns.filter(f => f !== field) + }, filterMatch(field) { const keyword = this.columnSearch.toLowerCase() return !keyword || field.label.toLowerCase().includes(keyword) || field.key.toLowerCase().includes(keyword) @@ -406,8 +450,8 @@ export default { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 16px; - padding-bottom: 12px; + margin-bottom: 12px; + padding-bottom: 10px; border-bottom: 1px solid #ebeef5; } .custom-export-actions { @@ -415,29 +459,134 @@ export default { gap: 8px; } .custom-export-body { - max-height: 420px; + display: flex; + gap: 16px; + height: 420px; +} +.export-left { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} +.export-right { + width: 260px; + display: flex; + flex-direction: column; + border-left: 1px solid #ebeef5; + padding-left: 16px; +} +.export-panel-title { + font-size: 13px; + font-weight: 600; + color: #303133; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 6px; +} +.order-count { + background: #409eff; + color: #fff; + font-size: 11px; + padding: 1px 7px; + border-radius: 10px; + font-weight: normal; +} +.export-left-scroll { + flex: 1; + overflow-y: auto; + padding-right: 8px; +} +.export-right-scroll { + flex: 1; overflow-y: auto; } .column-group { - margin-bottom: 14px; + margin-bottom: 12px; } .column-group-title { - font-size: 13px; + font-size: 12px; font-weight: 600; - color: #606266; - margin-bottom: 8px; - padding-left: 2px; - border-left: 3px solid #409eff; + color: #909399; + margin-bottom: 6px; padding-left: 8px; + border-left: 2px solid #dcdfe6; } .column-group-items { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px 12px; + grid-template-columns: repeat(2, 1fr); + gap: 2px 8px; } .column-group-items .el-checkbox { margin-right: 0; } +.ordered-list { + min-height: 60px; +} +.ordered-item { + display: flex; + align-items: center; + padding: 6px 8px; + margin-bottom: 4px; + background: #f5f7fa; + border: 1px solid #e4e7ed; + border-radius: 4px; + cursor: default; + transition: background .2s; +} +.ordered-item:hover { + background: #ecf5ff; + border-color: #c6e2ff; +} +.ordered-item.ghost { + opacity: 0.4; + background: #409eff; +} +.drag-handle { + color: #c0c4cc; + cursor: grab; + margin-right: 6px; +} +.drag-handle:active { + cursor: grabbing; +} +.order-index { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + font-size: 11px; + color: #909399; + background: #e4e7ed; + border-radius: 50%; + margin-right: 6px; + flex-shrink: 0; +} +.order-label { + flex: 1; + font-size: 13px; + color: #303133; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.order-remove { + color: #c0c4cc; + cursor: pointer; + font-size: 12px; + flex-shrink: 0; +} +.order-remove:hover { + color: #f56c6c; +} +.empty-tip { + color: #c0c4cc; + font-size: 12px; + text-align: center; + padding-top: 30px; +} .custom-export-footer { display: flex; align-items: center; diff --git a/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java b/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java index d6054043..fb5a391b 100644 --- a/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java +++ b/klp-wms/src/main/java/com/klp/controller/WmsMaterialCoilController.java @@ -5,8 +5,6 @@ import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Arrays; -import java.util.Set; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.stream.Collectors; import java.util.HashMap; @@ -33,7 +31,6 @@ import com.klp.common.core.validate.AddGroup; import com.klp.common.core.validate.EditGroup; import com.klp.common.enums.BusinessType; import com.klp.common.utils.poi.ExcelUtil; -import com.klp.common.utils.StringUtils; import com.klp.domain.bo.WmsMaterialCoilBo; import com.klp.domain.bo.WmsMaterialCoilReportSummaryBo; import com.klp.domain.vo.dashboard.CoilTrimStatisticsVo; @@ -135,25 +132,6 @@ public class WmsMaterialCoilController extends BaseController { ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilExportVo.class, response); } - /** - * 个性化导出:前端传入要导出的字段名,仅导出选中列 - * columns 参数为逗号分隔的 Java 字段名,如 "itemTypeDesc,enterCoilNo,netWeight" - * 不传 columns 时等同于 /exportAll 导出全部字段 - */ - @Log(title = "钢卷物料表", businessType = BusinessType.EXPORT) - @PostMapping("/exportCustom") - public void exportCustom(WmsMaterialCoilBo bo, - @RequestParam(required = false) String columns, - HttpServletResponse response) { - List list = iWmsMaterialCoilService.queryExportListAll(bo); - if (StringUtils.isNotBlank(columns)) { - Set includeFields = new HashSet<>(Arrays.asList(columns.split(","))); - ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilAllExportVo.class, includeFields, response); - } else { - ExcelUtil.exportExcel(list, "钢卷物料表", WmsMaterialCoilAllExportVo.class, response); - } - } - /** * 获取可导出的列元数据(供前端列选择器使用) * 返回 { "fieldName": "中文列名" } 的映射,基于完整导出字段 @@ -198,6 +176,35 @@ public class WmsMaterialCoilController extends BaseController { return R.ok(columns); } + /** + * 自定义导出(指定列顺序):前端传入按导出顺序排列的字段名 + * columnsOrdered 参数为逗号分隔的有序字段名,如 "team,enterCoilNo,netWeight,remark" + */ + @Log(title = "钢卷物料表", businessType = BusinessType.EXPORT) + @PostMapping("/exportCustomOrdered") + public void exportCustomOrdered(WmsMaterialCoilBo bo, + @RequestParam String columnsOrdered, + HttpServletResponse response) { + List list = iWmsMaterialCoilService.queryExportListAll(bo); + List orderedFields = Arrays.asList(columnsOrdered.split(",")); + ExcelUtil.exportExcelOrdered(list, "钢卷物料表", orderedFields, + getAllExportFieldLabelMap(), response); + } + + /** + * 从 WmsMaterialCoilAllExportVo 注解中提取字段名->中文列名映射 + */ + private Map getAllExportFieldLabelMap() { + Map map = new LinkedHashMap<>(); + for (java.lang.reflect.Field field : WmsMaterialCoilAllExportVo.class.getDeclaredFields()) { + com.alibaba.excel.annotation.ExcelProperty ep = field.getAnnotation(com.alibaba.excel.annotation.ExcelProperty.class); + if (ep != null && ep.value().length > 0) { + map.put(field.getName(), ep.value()[0]); + } + } + return map; + } + /** * 导出钢卷物料表列表(完整字段版本) * 导出全部字段