refactor(wms-report): 统一报表页面模板,提取公共逻辑为action-template

1.  将merge、repair下的多个报表页面重构为使用action-template组件
2.  提取自定义导出功能为公共组件CustomExport并复用至receive.vue和action-template
3.  统一报表页面的查询、导出、列配置等公共逻辑
This commit is contained in:
2026-06-03 16:31:51 +08:00
parent 88149561c5
commit 12a887e074
17 changed files with 574 additions and 963 deletions

View File

@@ -86,66 +86,18 @@
<columns-setting :reportType="activeColumnConfig"></columns-setting>
</el-dialog>
<!-- 自定义导出列选择弹窗 -->
<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: 200px" />
<div class="custom-export-actions">
<el-button size="small" @click="selectAllColumns">全选</el-button>
<el-button size="small" @click="invertColumns">反选</el-button>
<el-button size="small" @click="selectedColumns = []">清空</el-button>
</div>
</div>
<div class="custom-export-body">
<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>
</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>{{ orderedColumns.length }}</b> / {{ flatColumns.length }} </span>
<el-button @click="customExportVisible = false">取消</el-button>
<el-button type="primary" @click="doCustomExport" :disabled="orderedColumns.length === 0">
导出
</el-button>
</div>
</el-dialog>
<custom-export
ref="customExport"
:visible.sync="customExportVisible"
storage-key="coil-report-receive"
:column-groups="columnGroups"
@export="handleCustomExport"
/>
</div>
</template>
<script>
import { listCoilWithIds, getExportColumns } from "@/api/wms/coil";
import { listCoilWithIds } from "@/api/wms/coil";
import {
listPendingAction,
} from '@/api/wms/pendingAction';
@@ -162,7 +114,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';
import CustomExport from "@/views/wms/report/components/CustomExport.vue";
export default {
@@ -178,7 +130,7 @@ export default {
TimeRangePicker,
HierarchicalPivot,
CrossTable,
draggable,
CustomExport,
},
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
data() {
@@ -206,10 +158,6 @@ export default {
activeColumnConfig: 'coil-report-receive',
settingVisible: false,
customExportVisible: false,
exportColumns: {},
selectedColumns: [],
orderedColumns: [],
columnSearch: '',
columnGroups: {
'基本信息': ['itemTypeDesc', 'warehouseName', 'actualWarehouseName', 'dataTypeText'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo'],
@@ -295,38 +243,22 @@ export default {
coilIds() {
return this.list.map(item => item.coilId).join(',')
},
groupedColumns() {
const result = {}
Object.entries(this.columnGroups).forEach(([groupName, fieldKeys]) => {
const items = fieldKeys
.filter(key => this.exportColumns[key])
.map(key => ({ key, label: this.exportColumns[key] }))
if (items.length) {
result[groupName] = items
}
})
return result
actionIds() {
const ids = this.list.map(item => item.actionId).filter(id => id)
return ids.length ? ids.join(',') : ''
},
flatColumns() {
return Object.values(this.groupedColumns).flat()
exportIdParams() {
return this.actionIds ? { actionIds: this.actionIds } : { coilIds: this.coilIds }
},
hasExportData() {
return !!(this.coilIds || this.actionIds)
},
exportQueryParams() {
const { pageNum, pageSize, ...filters } = this.queryParams
return filters
},
},
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: {
// 加载列设置
@@ -384,55 +316,25 @@ export default {
// 导出
exportData() {
this.download('wms/materialCoil/export', {
coilIds: this.coilIds,
...this.exportQueryParams,
...this.exportIdParams,
}, `materialCoil_${new Date().getTime()}.xlsx`)
},
// 打开自定义导出弹窗
openCustomExport() {
getExportColumns().then(res => {
this.exportColumns = res.data
// 读缓存:上次保存的列顺序
const cached = localStorage.getItem('custom-export-columns-coil-report-receive')
if (cached) {
try {
const arr = JSON.parse(cached)
if (Array.isArray(arr) && arr.length) {
this.selectedColumns = [...arr]
this.orderedColumns = [...arr]
this.customExportVisible = true
return
}
} catch (e) { /* ignore */ }
}
this.selectedColumns = []
this.orderedColumns = []
this.customExportVisible = true
})
if (!this.hasExportData) {
this.$message({ message: '暂无数据可导出', type: 'warning' })
return
}
this.$refs.customExport.open()
},
// 执行自定义导出(按 orderedColumns 顺序)
doCustomExport() {
// 缓存当前配置
localStorage.setItem('custom-export-columns-coil-report-receive', JSON.stringify(this.orderedColumns))
this.customExportVisible = false
handleCustomExport(orderedColumns) {
this.download('wms/materialCoil/exportCustomOrdered', {
coilIds: this.coilIds,
columnsOrdered: this.orderedColumns.join(','),
...this.exportQueryParams,
...this.exportIdParams,
columnsOrdered: 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)
},
selectAllColumns() {
this.selectedColumns = this.flatColumns.map(f => f.key)
},
invertColumns() {
const allKeys = this.flatColumns.map(f => f.key)
this.selectedColumns = allKeys.filter(k => !this.selectedColumns.includes(k))
},
saveReport() {
this.loading = true
saveReportFile(this.coilIds, {
@@ -462,157 +364,4 @@ export default {
</script>
<style scoped>
.custom-export-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.custom-export-actions {
display: flex;
gap: 8px;
}
.custom-export-body {
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: 12px;
}
.column-group-title {
font-size: 12px;
font-weight: 600;
color: #909399;
margin-bottom: 6px;
padding-left: 8px;
border-left: 2px solid #dcdfe6;
}
.column-group-items {
display: grid;
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;
justify-content: space-between;
}
.selected-tip {
font-size: 13px;
color: #909399;
}
.selected-tip b {
color: #409eff;
}
</style>