This commit is contained in:
2026-06-01 15:56:51 +08:00
3 changed files with 277 additions and 53 deletions

View File

@@ -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 <T> void exportExcelOrdered(List<T> list, String sheetName,
List<String> orderedFields,
Map<String, String> fieldLabelMap,
HttpServletResponse response) {
if (orderedFields == null || orderedFields.isEmpty()) {
throw new IllegalArgumentException("导出列不能为空");
}
// 构建动态表头
List<List<String>> heads = orderedFields.stream()
.map(f -> Collections.singletonList(fieldLabelMap.getOrDefault(f, f)))
.collect(Collectors.toList());
// 构建数据行
List<List<Object>> data = new ArrayList<>(list.size());
if (!list.isEmpty()) {
Map<String, Field> fieldCache = new HashMap<>();
Class<?> clazz = list.get(0).getClass();
for (T vo : list) {
List<Object> 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);
}
}
}

View File

@@ -87,9 +87,9 @@
</el-dialog>
<!-- 自定义导出列选择弹窗 -->
<el-dialog title="自定义导出 - 选择导出列" :visible.sync="customExportVisible" width="750px">
<el-dialog title="自定义导出 - 选择导出列" :visible.sync="customExportVisible" width="850px" top="5vh">
<div class="custom-export-toolbar">
<el-input v-model="columnSearch" placeholder="搜索列名" prefix-icon="el-icon-search" clearable size="small" style="width: 220px" />
<el-input v-model="columnSearch" placeholder="搜索列名" prefix-icon="el-icon-search" clearable size="small" style="width: 200px" />
<div class="custom-export-actions">
<el-button size="small" @click="selectAllColumns">全选</el-button>
<el-button size="small" @click="invertColumns">反选</el-button>
@@ -97,25 +97,47 @@
</div>
</div>
<div class="custom-export-body">
<el-checkbox-group v-model="selectedColumns">
<div v-for="(group, gName) in groupedColumns" :key="gName" class="column-group">
<div class="column-group-title">{{ gName }}</div>
<div class="column-group-items">
<el-checkbox
v-for="field in group"
:key="field.key"
:label="field.key"
:style="{ display: columnSearch && !filterMatch(field) ? 'none' : '' }"
>{{ field.label }}</el-checkbox>
</div>
<div class="export-left">
<div class="export-panel-title">可选列</div>
<div class="export-left-scroll">
<el-checkbox-group v-model="selectedColumns">
<div v-for="(group, gName) in groupedColumns" :key="gName" class="column-group">
<div class="column-group-title">{{ gName }}</div>
<div class="column-group-items">
<el-checkbox
v-for="field in group"
:key="field.key"
:label="field.key"
:style="{ display: columnSearch && !filterMatch(field) ? 'none' : '' }"
>{{ field.label }}</el-checkbox>
</div>
</div>
</el-checkbox-group>
</div>
</el-checkbox-group>
</div>
<div class="export-right">
<div class="export-panel-title">
导出顺序
<span class="order-count">{{ orderedColumns.length }}</span>
</div>
<div class="export-right-scroll">
<draggable v-model="orderedColumns" class="ordered-list" ghost-class="ghost" handle=".drag-handle">
<div v-for="field in orderedColumns" :key="field" class="ordered-item">
<i class="el-icon-rank drag-handle"></i>
<span class="order-index">{{ orderedColumns.indexOf(field) + 1 }}</span>
<span class="order-label">{{ exportColumns[field] || field }}</span>
<i class="el-icon-close order-remove" @click.stop="removeOrderedField(field)"></i>
</div>
</draggable>
<div v-if="orderedColumns.length === 0" class="empty-tip">勾选左侧列后出现在此处可拖拽排序</div>
</div>
</div>
</div>
<div slot="footer" class="custom-export-footer">
<span class="selected-tip">已选 <b>{{ selectedColumns.length }}</b> / {{ flatColumns.length }} </span>
<span class="selected-tip">已选 <b>{{ orderedColumns.length }}</b> / {{ flatColumns.length }} </span>
<el-button @click="customExportVisible = false">取消</el-button>
<el-button type="primary" @click="doCustomExport" :disabled="selectedColumns.length === 0">
导出选中列
<el-button type="primary" @click="doCustomExport" :disabled="orderedColumns.length === 0">
导出
</el-button>
</div>
</el-dialog>
@@ -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;

View File

@@ -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<WmsMaterialCoilAllExportVo> list = iWmsMaterialCoilService.queryExportListAll(bo);
if (StringUtils.isNotBlank(columns)) {
Set<String> 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<WmsMaterialCoilAllExportVo> list = iWmsMaterialCoilService.queryExportListAll(bo);
List<String> orderedFields = Arrays.asList(columnsOrdered.split(","));
ExcelUtil.exportExcelOrdered(list, "钢卷物料表", orderedFields,
getAllExportFieldLabelMap(), response);
}
/**
* 从 WmsMaterialCoilAllExportVo 注解中提取字段名->中文列名映射
*/
private Map<String, String> getAllExportFieldLabelMap() {
Map<String, String> 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;
}
/**
* 导出钢卷物料表列表(完整字段版本)
* 导出全部字段