Files
klp-oa/klp-ui/src/views/wms/report/receive.vue
Joshi 8b1d7ed280 feat(wms/report): 新增自定义导出列顺序功能并优化导出弹窗布局
1. 在ExcelUtil工具类中新增exportExcelOrdered方法,支持按指定顺序的列动态生成表头和数据行进行导出
2. 重构接收报表页面的自定义导出弹窗:将布局拆分为左侧可选列面板和右侧导出顺序面板,支持拖拽排序
3. 新增后端/exportCustomOrdered接口,接收有序字段列表并调用新的导出方法
4. 优化弹窗样式:调整宽度、间距、滚动区域,新增顺序序号和移除按钮
5. 移除原有的/exportCustom接口,统一使用新的有序导出逻辑
2026-06-01 15:14:34 +08:00

603 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container" v-loading="loading">
<el-row>
<el-form label-width="80px" inline>
<el-form-item label="时间范围" prop="timeRange">
<time-range-picker
v-model="queryParams"
start-key="byCreateTimeStart"
end-key="byCreateTimeEnd"
:default-start-time="defaultStartTime"
:default-end-time="defaultEndTime"
@quick-select="handleQuery"
/>
</el-form-item>
<el-form-item label="入场钢卷号" prop="endTime">
<el-input style="width: 200px; display: inline-block;" v-model="queryParams.enterCoilNo"
placeholder="请输入入场钢卷号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="当前钢卷号" prop="endTime">
<el-input style="width: 200px;" v-model="queryParams.currentCoilNo" placeholder="请输入当前钢卷号" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="逻辑库位" prop="warehouseId">
<warehouse-select v-model="queryParams.warehouseId" placeholder="请选择仓库/库区/库位"
style="width: 100%; display: inline-block; width: 200px;" clearable />
</el-form-item>
<el-form-item label="产品名称" prop="itemName">
<el-input style="width: 200px;" v-model="queryParams.itemName" placeholder="请输入产品名称" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="规格" prop="itemSpecification">
<memo-input style="width: 200px;" v-model="queryParams.itemSpecification" storageKey="coilSpec"
placeholder="请选择规格" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材质" prop="itemMaterial">
<muti-select style="width: 200px;" v-model="queryParams.itemMaterial" :options="dict.type.coil_material"
placeholder="请选择材质" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="厂家" prop="itemManufacturer">
<muti-select style="width: 200px;" v-model="queryParams.itemManufacturer"
:options="dict.type.coil_manufacturer" placeholder="请选择厂家" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="品质" prop="qualityStatusCsv">
<muti-select v-model="queryParams.qualityStatusCsv" :options="dict.type.coil_quality_status"
placeholder="请选择品质" clearable />
</el-form-item>
<el-form-item label="收货计划" prop="planId">
<el-select style="width: 200px;" v-model="queryParams.planId" placeholder="请输入计划名称搜索收货计划" filterable remote
:remote-method="remoteMethod">
<el-option v-for="item in planList" :key="item.planId" :label="item.planName" :value="item.planId" />
</el-select>
<!-- 默认选中今天的收货计划如果今天还没有收货计划提醒创建收货计划 -->
</el-form-item>
<el-form-item prop="endTime">
<el-button type="primary" @click="getList">查询</el-button>
<el-button type="primary" @click="exportData">导出</el-button>
<el-button type="primary" @click="openCustomExport">自定义导出</el-button>
<el-button type="primary" @click="settingVisible = true">列设置</el-button>
<el-button type="primary" @click="saveReport">保存报表</el-button>
</el-form-item>
</el-form>
</el-row>
<el-descriptions title="统计信息" :column="3" border>
<el-descriptions-item label="总钢卷数量">{{ summary.totalCount }}</el-descriptions-item>
<el-descriptions-item label="总重">{{ summary.totalWeight }}t</el-descriptions-item>
<el-descriptions-item label="均重">{{ summary.avgWeight }}t</el-descriptions-item>
</el-descriptions>
<!-- 厂家材质透视表 -->
<HierarchicalPivot :data="list" :config="hierarchicalPivotConfig" />
<!-- 宽度厚度统计表 -->
<CrossTable :data="list" :config="crossTableConfig" />
<el-descriptions title="明细信息" :column="3" border>
</el-descriptions>
<coil-table :columns="receiveColumns" :data="list"></coil-table>
<el-dialog title="列设置" :visible.sync="settingVisible" width="50%">
<el-radio-group v-model="activeColumnConfig">
<el-radio-button label="coil-report-receive">收货明细配置</el-radio-button>
</el-radio-group>
<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>
</div>
</template>
<script>
import { listCoilWithIds, getExportColumns } from "@/api/wms/coil";
import {
listPendingAction,
} from '@/api/wms/pendingAction';
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
import MemoInput from "@/components/MemoInput";
import MutiSelect from "@/components/MutiSelect";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import { listDeliveryPlan } from '@/api/wms/deliveryPlan'
import ColumnsSetting from "@/views/wms/report/components/setting/columns.vue";
import CoilTable from "@/views/wms/report/components/coilTable/index.vue";
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 {
components: {
ProductInfo,
RawMaterialInfo,
CoilNo,
MemoInput,
MutiSelect,
WarehouseSelect,
ColumnsSetting,
CoilTable,
TimeRangePicker,
HierarchicalPivot,
CrossTable,
draggable,
},
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
data() {
// 工具函数:个位数补零,保证格式统一(比如 9 → 095 → 05
const addZero = (num) => num.toString().padStart(2, '0')
const now = new Date() // 当前本地北京时间
// 核心:获取【昨天】的日期对象(自动处理跨月/跨年,无边界问题)
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
// 昨天的年、月、日(补零格式化)
const yesYear = yesterday.getFullYear()
const yesMonth = addZero(yesterday.getMonth() + 1)
const yesDay = addZero(yesterday.getDate())
// 今天的年、月、日(补零格式化)
const nowYear = now.getFullYear()
const nowMonth = addZero(now.getMonth() + 1)
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
activeColumnConfig: 'coil-report-receive',
settingVisible: false,
customExportVisible: false,
exportColumns: {},
selectedColumns: [],
orderedColumns: [],
columnSearch: '',
columnGroups: {
'基本信息': ['itemTypeDesc', 'warehouseName', 'actualWarehouseName', 'dataTypeText'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo'],
'时间': ['createTime', 'exportTime', 'exportBy'],
'物理属性': ['netWeight', 'length', 'specification', 'actualThickness'],
'材质属性': ['material', 'manufacturer', 'surfaceTreatmentDesc', 'zincLayer', 'packingStatus', 'temperGrade', 'coatingType'],
'用途': ['purpose', 'businessPurpose'],
'状态': ['qualityStatus', 'statusDesc', 'isRelatedToOrderText'],
'其他': ['itemName', 'itemId', 'packagingRequirement', 'trimmingRequirement', 'transferType', 'saleName', 'remark', 'team'],
},
list: [],
defaultStartTime: startTime,
defaultEndTime: endTime,
queryParams: {
pageNum: 1,
pageSize: 9999,
selectType: 'raw_material',
enterCoilNo: '',
currentCoilNo: '',
warehouseId: '',
productName: '',
itemSpecification: '',
itemMaterial: '',
itemManufacturer: '',
planId: '',
},
planList: [],
loading: false,
receiveColumns: [],
// 厂家材质透视表配置
hierarchicalPivotConfig: {
groupFields: ['manufacturer', 'material', 'specification'],
groupLabels: ['厂家', '材质', '规格'],
summaryFields: [
{ prop: 'count', label: '求和项:件数', field: '', align: 'center' },
{ prop: 'weight', label: '求和项:重量', field: 'netWeight', align: 'center' }
],
formatValue: (value, summaryField) => {
if (summaryField.field === '') {
return value
}
return value.toFixed(3)
}
},
// 宽度厚度统计表配置
crossTableConfig: {
rowField: 'computedWidth',
rowLabel: '宽度',
rowWidth: 100,
columnField: 'computedThickness',
summaryColumns: [
{ prop: 'count', label: '件数', width: 80, field: '' },
{ prop: 'weight', label: '重量', width: 100, field: 'netWeight' }
],
formatValue: (value, summaryField) => {
if (summaryField.field === '') {
return value
}
return value.toFixed(2)
},
formatKey: (num) => {
if (num === null || num === undefined || isNaN(num)) return null
return parseFloat(num).toFixed(2)
}
}
}
},
computed: {
summary() {
// 总钢卷数量、总重、均重
const totalCount = this.list.length
const totalWeight = this.list.reduce((acc, cur) => acc + parseFloat(cur.netWeight), 0)
const avgWeight = totalCount > 0 ? (totalWeight / totalCount).toFixed(2) : 0
return {
totalCount,
totalWeight: totalWeight.toFixed(2),
avgWeight,
}
},
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
},
flatColumns() {
return Object.values(this.groupedColumns).flat()
},
},
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() {
this.receiveColumns = JSON.parse(localStorage.getItem('preference-tableColumns-coil-report-receive') || '[]') || []
},
// 统一查询入口
handleQuery() {
this.getList()
},
remoteMethod(query) {
listDeliveryPlan({ planName: query, pageNum: 1, pageSize: 5, planType: 1 }).then(res => {
this.planList = res.rows
})
},
getList() {
this.loading = true
listPendingAction({
// actionStatus: 2,
warehouseId: this.queryParams.planId,
actionType: 401,
pageSize: 99999,
pageNum: 1,
startTime: this.queryParams.byCreateTimeStart,
endTime: this.queryParams.byCreateTimeEnd,
}).then(res => {
const actions = res.rows
const coilIds = actions.map(item => item.coilId).join(',')
if (!coilIds) {
this.$message({
message: '暂无数据',
type: 'warning',
})
this.list = []
this.loading = false
return
}
listCoilWithIds({
...this.queryParams,
coilIds: coilIds,
}).then(res => {
this.list = res.rows.map(item => {
// 计算宽度和厚度,将规格按照*分割,*前的是厚度,*后的是宽度
const [thickness, width] = item.specification?.split('*') || []
return {
...item,
computedThickness: parseFloat(thickness),
computedWidth: parseFloat(width),
}
})
this.loading = false
})
})
},
// 导出
exportData() {
this.download('wms/materialCoil/export', {
coilIds: this.coilIds,
}, `materialCoil_${new Date().getTime()}.xlsx`)
},
// 打开自定义导出弹窗
openCustomExport() {
getExportColumns().then(res => {
this.exportColumns = res.data
this.selectedColumns = []
this.customExportVisible = true
})
},
// 执行自定义导出(按 orderedColumns 顺序)
doCustomExport() {
this.customExportVisible = false
this.download('wms/materialCoil/exportCustomOrdered', {
coilIds: this.coilIds,
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)
},
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, {
reportParams: this.queryParams,
reportType: '收货报表',
}).then(res => {
this.$message({
message: '保存成功',
type: 'success',
})
}).catch(err => {
this.$message({
message: '保存失败',
type: 'error',
})
}).finally(() => {
this.loading = false
})
}
},
mounted() {
this.getList()
this.remoteMethod('')
this.loadColumns()
}
}
</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>