feat(wms/report): 新增自定义导出列顺序功能并优化导出弹窗布局

1. 在ExcelUtil工具类中新增exportExcelOrdered方法,支持按指定顺序的列动态生成表头和数据行进行导出
2. 重构接收报表页面的自定义导出弹窗:将布局拆分为左侧可选列面板和右侧导出顺序面板,支持拖拽排序
3. 新增后端/exportCustomOrdered接口,接收有序字段列表并调用新的导出方法
4. 优化弹窗样式:调整宽度、间距、滚动区域,新增顺序序号和移除按钮
5. 移除原有的/exportCustom接口,统一使用新的有序导出逻辑
This commit is contained in:
2026-06-01 15:14:34 +08:00
parent 285775c733
commit 8b1d7ed280
3 changed files with 277 additions and 53 deletions

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;