Files
klp-oa/klp-ui/src/views/wms/coil/stock/index.vue
砂糖 6e28ba0342 fix(wms/coil/stock, wms/report): 修复两处功能问题
1. 补充卷库存查询的质检状态参数
2. 修复自定义导出列重复添加和缓存解析重复数据的问题
2026-07-08 13:07:50 +08:00

669 lines
29 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 inline>
<el-form-item label="逻辑库位">
<warehouse-select
v-model="queryParams.warehouseIds"
multiple
collapse-tags
placeholder="请选择仓库/库区/库位"
style="width: 280px"
clearable
/>
</el-form-item>
<el-form-item label="品质">
<muti-select
v-model="queryParams.qualityStatusCsv"
:options="dict.type.coil_quality_status"
placeholder="请选择品质"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<!-- 操作提示 -->
<el-alert
v-if="!hintDismissed"
title="💡 操作提示"
type="info"
:closable="true"
@close="hintDismissed = true"
show-icon
style="margin-bottom: 16px"
>
<ul class="hint-list">
<li>点击<strong>柱状图</strong><strong>饼图</strong>的库区可钻取该库区的详情弹窗</li>
<li>点击下方<strong>库区汇总表格</strong>的任意行打开详情弹窗</li>
<li>弹窗内可切换<strong>卷数/净重</strong>指标配置<strong>品质合并分组</strong> A类 / B类透视<strong>班组 × 品质</strong>交叉分布</li>
<li>品质合并配置会自动保存下次打开无需重新设置</li>
</ul>
</el-alert>
<!-- 汇总统计 -->
<el-descriptions title="汇总统计" :column="4" border v-if="summary.totalCount > 0">
<el-descriptions-item label="总钢卷数">{{ summary.totalCount }}</el-descriptions-item>
<el-descriptions-item label="总净重(t)">{{ summary.totalNetWeight }}</el-descriptions-item>
<el-descriptions-item label="均重(t)">{{ summary.avgWeight }}</el-descriptions-item>
<el-descriptions-item label="非空库区数">{{ summary.warehouseCount }}</el-descriptions-item>
</el-descriptions>
<!-- ECharts 图表区域 -->
<el-row :gutter="20" v-if="aggregatedList.length > 0" style="margin-top: 20px">
<el-col :span="14">
<div class="chart-card">
<h3 class="chart-title">各库区总净重 / 均重</h3>
<div ref="barChartRef" class="chart-box"></div>
</div>
</el-col>
<el-col :span="10">
<div class="chart-card">
<h3 class="chart-title">各库区钢卷数量分布</h3>
<div ref="pieChartRef" class="chart-box"></div>
</div>
</el-col>
</el-row>
<!-- 汇总明细表格按库区汇总点击行可钻取详情 -->
<div v-if="aggregatedList.length > 0" style="margin-top: 20px">
<h3>库区汇总明细点击行查看详情</h3>
<el-table
:data="aggregatedList"
border
stripe
max-height="700"
style="width: 100%"
@row-click="handleRowClick"
row-class-name="clickable-row"
>
<el-table-column prop="warehouseName" label="逻辑库位" align="center" min-width="200" show-overflow-tooltip />
<el-table-column prop="coilCount" label="卷数" align="center" width="100" sortable />
<el-table-column prop="totalNetWeight" label="总净重(t)" align="center" width="140" sortable />
<el-table-column prop="avgNetWeight" label="均重(t)" align="center" width="120" />
</el-table>
</div>
<el-empty v-if="!loading && aggregatedList.length === 0" description="暂无数据" />
<!-- 库区详情弹窗 -->
<el-dialog
:title="`${dialogTitle}`"
:visible.sync="dialogVisible"
width="90%"
top="5vh"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<div v-loading="dialogLoading">
<!-- 数据透视图班组 × 品质 -->
<div v-if="dialogPivotTeams.length > 0 && dialogPivotCols.length > 0">
<div class="pivot-toolbar">
<h4>班组 × 品质 数据透视图</h4>
<div class="pivot-actions">
<el-radio-group v-model="dialogPivotMeasure" size="small" @change="buildDialogPivot">
<el-radio-button label="count">卷数</el-radio-button>
<el-radio-button label="weight">净重(t)</el-radio-button>
</el-radio-group>
<el-button size="small" type="primary" plain @click="showMergeConfig">品质合并配置</el-button>
<el-button size="small" @click="resetMergeConfig">重置合并</el-button>
</div>
</div>
<el-table
:data="dialogPivotRows"
border
stripe
class="pivot-table"
:header-cell-style="pivotHeaderStyle"
>
<el-table-column prop="team" label="班组" align="center" width="100" fixed="left" />
<el-table-column
v-for="col in dialogPivotCols"
:key="col.key"
:label="col.label"
align="center"
:min-width="100"
>
<template slot-scope="{ row }">
<span :style="{ color: getDialogPivotCellColor(row[col.key]) }">
{{ formatDialogPivotValue(row[col.key]) }}
</span>
</template>
</el-table-column>
<el-table-column prop="_total" label="合计" align="center" width="110">
<template slot-scope="{ row }">
<strong>{{ formatDialogPivotValue(row._total) }}</strong>
</template>
</el-table-column>
</el-table>
<!-- 列合计行 -->
<div class="pivot-footer" v-if="dialogPivotColTotals">
<span class="footer-label">合计</span>
<span v-for="col in dialogPivotCols" :key="col.key" class="footer-cell">
{{ formatDialogPivotValue(dialogPivotColTotals[col.key]) }}
</span>
<span class="footer-cell footer-total">
<strong>{{ formatDialogPivotValue(dialogPivotColTotals._total) }}</strong>
</span>
</div>
</div>
<!-- 弹窗汇总 -->
<el-descriptions :column="4" border v-if="dialogSummary.totalCount > 0" style="margin-top: 16px">
<el-descriptions-item label="钢卷数">{{ dialogSummary.totalCount }}</el-descriptions-item>
<el-descriptions-item label="总净重(t)">{{ dialogSummary.totalNetWeight }}</el-descriptions-item>
<el-descriptions-item label="均重(t)">{{ dialogSummary.avgWeight }}</el-descriptions-item>
<el-descriptions-item label="品质种类">{{ dialogSummary.qualityKinds }}</el-descriptions-item>
</el-descriptions>
<!-- 钢卷明细表格 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 20px">
<h4 style="margin: 0">钢卷明细</h4>
<el-button type="primary" size="small" icon="el-icon-download" @click="handleExportDetail">导出</el-button>
</div>
<el-table :data="dialogList" border stripe max-height="500" style="width: 100%">
<el-table-column prop="enterCoilNo" label="入场钢卷号" align="center" min-width="150" show-overflow-tooltip />
<el-table-column prop="currentCoilNo" label="当前钢卷号" align="center" min-width="150" show-overflow-tooltip />
<el-table-column prop="itemName" label="物料名称" align="center" min-width="140" show-overflow-tooltip />
<el-table-column prop="specification" label="规格" align="center" width="120" />
<el-table-column prop="netWeight" label="净重(t)" align="center" width="100" />
<el-table-column prop="qualityStatus" label="品质" align="center" width="80">
<template slot-scope="scope">
<dict-tag v-if="scope.row.qualityStatus" :options="dict.type.coil_quality_status" :value="scope.row.qualityStatus" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="team" label="班组" align="center" width="80" />
<el-table-column label="距今" align="center" width="90">
<template slot-scope="scope">
{{ timeAgo(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<pagination
:total="dialogTotal"
:page.sync="dialogPageNum"
:limit.sync="dialogPageSize"
@pagination="fetchDialogList"
/>
</div>
</el-dialog>
<!-- 品质合并配置弹窗 -->
<el-dialog
title="品质合并配置"
:visible.sync="mergeConfigVisible"
width="700px"
:close-on-click-modal="false"
>
<div class="merge-config-body">
<div v-for="(group, gi) in mergeGroups" :key="gi" class="merge-group">
<div class="merge-group-header">
<el-input v-model="group.name" placeholder="分组名称" size="small" style="width: 120px" />
<el-button type="text" icon="el-icon-delete" style="color: #F56C6C" @click="removeMergeGroup(gi)" :disabled="mergeGroups.length <= 1">删除</el-button>
</div>
<div class="merge-group-tags">
<el-tag v-for="(qs, qi) in group.qualities" :key="qi" closable size="small" @close="removeQualityFromGroup(gi, qi)">{{ qs }}</el-tag>
<el-button v-if="unassignedQualities.length > 0" size="mini" icon="el-icon-plus" @click="showAddQuality(gi)">添加品质</el-button>
</div>
</div>
<el-button type="primary" size="small" icon="el-icon-plus" @click="addMergeGroup">新增分组</el-button>
<div class="unassigned-section" v-if="unassignedQualities.length > 0">
<h4>未分配品质将单独成列</h4>
<el-tag v-for="qs in unassignedQualities" :key="qs" size="small" type="info">{{ qs }}</el-tag>
</div>
</div>
<span slot="footer">
<el-button @click="mergeConfigVisible = false">取消</el-button>
<el-button type="primary" @click="applyMergeConfig">应用</el-button>
</span>
</el-dialog>
<!-- 添加品质到分组的弹窗 -->
<el-dialog title="选择品质" :visible.sync="addQualityVisible" width="400px" append-to-body>
<el-checkbox-group v-model="selectedQualities">
<el-checkbox v-for="qs in unassignedQualities" :key="qs" :label="qs">{{ qs }}</el-checkbox>
</el-checkbox-group>
<span slot="footer">
<el-button @click="addQualityVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAddQuality">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listLightCoil, listMaterialCoil } from "@/api/wms/coil";
import { listWarehouse } from "@/api/wms/warehouse";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import MutiSelect from "@/components/MutiSelect";
import * as echarts from "echarts";
const DEFAULT_MERGE_GROUPS = [
{ name: 'A类', qualities: ['A+', 'A', 'A-'] },
{ name: 'B类', qualities: ['B+', 'B', 'B-'] },
{ name: 'C类', qualities: ['C+', 'C', 'C-'] },
{ name: 'D类', qualities: ['D+', 'D', 'D-'] },
{ name: 'O类', qualities: ['O'] },
];
const PIVOT_STORAGE_KEY = 'coil-stock-pivot-config';
export default {
components: { WarehouseSelect, MutiSelect },
dicts: ['coil_quality_status'],
data() {
return {
loading: false,
hintDismissed: false,
queryParams: {
warehouseIds: [],
qualityStatusCsv: 'A+,A,A-,B+,B,B-',
},
allCoils: [],
warehouseMap: {},
aggregatedList: [],
barChartInst: null,
pieChartInst: null,
// 品质合并配置(全局共享)
mergeGroups: [],
mergeConfigVisible: false,
addQualityVisible: false,
addQualityGroupIndex: -1,
selectedQualities: [],
// 弹窗
dialogVisible: false,
dialogLoading: false,
dialogTitle: '',
currentWarehouse: null,
dialogList: [],
dialogPageNum: 1,
dialogPageSize: 20,
dialogTotal: 0,
dialogAllCoils: [],
// 弹窗内数据透视图
dialogPivotMeasure: 'count',
dialogPivotCols: [],
dialogPivotRows: [],
dialogPivotTeams: [],
dialogPivotColTotals: null,
};
},
computed: {
summary() {
if (this.allCoils.length === 0) return { totalCount: 0, totalNetWeight: 0, avgWeight: 0, warehouseCount: 0 };
const totalCount = this.allCoils.length;
const totalNetWeight = this.allCoils.reduce((sum, c) => sum + (parseFloat(c.netWeight) || 0), 0);
const warehouseIds = new Set(this.allCoils.map(c => c.warehouseId).filter(Boolean));
return {
totalCount,
totalNetWeight: totalNetWeight.toFixed(2),
avgWeight: totalCount > 0 ? (totalNetWeight / totalCount).toFixed(2) : '0.00',
warehouseCount: warehouseIds.size,
};
},
dialogSummary() {
const list = this.dialogAllCoils;
if (list.length === 0) return { totalCount: 0, totalNetWeight: 0, avgWeight: 0, qualityKinds: 0 };
const totalCount = list.length;
const totalNetWeight = list.reduce((s, c) => s + (parseFloat(c.netWeight) || 0), 0);
const qualitySet = new Set(list.map(c => c.qualityStatus).filter(Boolean));
return {
totalCount,
totalNetWeight: totalNetWeight.toFixed(2),
avgWeight: totalCount > 0 ? (totalNetWeight / totalCount).toFixed(2) : '0.00',
qualityKinds: qualitySet.size,
};
},
unassignedQualities() {
const assigned = new Set();
this.mergeGroups.forEach(g => g.qualities.forEach(q => assigned.add(q)));
const all = new Set();
this.allCoils.forEach(c => { if (c.qualityStatus) all.add(c.qualityStatus); });
this.dialogAllCoils.forEach(c => { if (c.qualityStatus) all.add(c.qualityStatus); });
return [...all].filter(q => !assigned.has(q)).sort();
},
},
mounted() {
this.loadMergeConfig();
this.fetchWarehouseMap().then(() => { this.getList(); });
},
beforeDestroy() {
this.disposeChart(this.barChartInst);
this.disposeChart(this.pieChartInst);
},
methods: {
disposeChart(inst) { if (inst) inst.dispose(); },
// ============ 品质合并配置持久化 ============
loadMergeConfig() {
try {
const saved = localStorage.getItem(PIVOT_STORAGE_KEY);
if (saved) { this.mergeGroups = JSON.parse(saved); return; }
} catch (e) { /* ignore */ }
this.mergeGroups = JSON.parse(JSON.stringify(DEFAULT_MERGE_GROUPS));
},
saveMergeConfig() {
try { localStorage.setItem(PIVOT_STORAGE_KEY, JSON.stringify(this.mergeGroups)); } catch (e) { /* ignore */ }
},
showMergeConfig() { this.mergeConfigVisible = true; },
addMergeGroup() { this.mergeGroups.push({ name: '新分组', qualities: [] }); },
removeMergeGroup(gi) { if (this.mergeGroups.length <= 1) return; this.mergeGroups.splice(gi, 1); },
showAddQuality(gi) { this.addQualityGroupIndex = gi; this.selectedQualities = []; this.addQualityVisible = true; },
confirmAddQuality() {
if (this.addQualityGroupIndex >= 0 && this.selectedQualities.length > 0) {
const group = this.mergeGroups[this.addQualityGroupIndex];
this.selectedQualities.forEach(q => { if (!group.qualities.includes(q)) group.qualities.push(q); });
}
this.addQualityVisible = false;
this.addQualityGroupIndex = -1;
},
removeQualityFromGroup(gi, qi) { this.mergeGroups[gi].qualities.splice(qi, 1); },
applyMergeConfig() {
this.saveMergeConfig();
this.mergeConfigVisible = false;
this.buildDialogPivot();
},
resetMergeConfig() {
this.mergeGroups = JSON.parse(JSON.stringify(DEFAULT_MERGE_GROUPS));
this.saveMergeConfig();
this.buildDialogPivot();
},
async fetchWarehouseMap() {
try {
const res = await listWarehouse({ pageNum: 1, pageSize: 9999 });
const list = res.data || [];
const map = {};
const allIds = [];
list.forEach(item => {
map[item.warehouseId] = item.warehouseName;
allIds.push(item.warehouseId);
});
this.warehouseMap = map;
// 默认全选
if (this.queryParams.warehouseIds.length === 0) {
this.queryParams.warehouseIds = allIds;
}
} catch (e) { console.error('获取仓库列表失败:', e); }
},
getList() {
this.loading = true;
const params = { pageSize: 99999, pageNum: 1, dataType: 1, status: 0 };
if (this.queryParams.warehouseIds && this.queryParams.warehouseIds.length > 0) {
params.warehouseIds = this.queryParams.warehouseIds.join(',');
}
if (this.queryParams.qualityStatusCsv) params.qualityStatusCsv = this.queryParams.qualityStatusCsv;
listLightCoil(params)
.then(res => {
this.allCoils = res || [];
this.buildAggregation();
})
.catch(err => { console.error('查询钢卷列表失败:', err); this.$message.error('查询钢卷数据失败'); })
.finally(() => { this.loading = false; });
},
buildAggregation() {
const map = {};
this.allCoils.forEach(coil => {
const whId = coil.warehouseId || '__unknown__';
if (!map[whId]) map[whId] = { warehouseId: coil.warehouseId, coilCount: 0, totalNetWeight: 0 };
map[whId].coilCount += 1;
map[whId].totalNetWeight += parseFloat(coil.netWeight) || 0;
});
this.aggregatedList = Object.values(map).map(item => ({
...item,
warehouseName: item.warehouseId ? (this.warehouseMap[item.warehouseId] || `未知库区(${item.warehouseId})`) : '未知库区',
totalNetWeight: parseFloat(item.totalNetWeight.toFixed(2)),
avgNetWeight: item.coilCount > 0 ? parseFloat((item.totalNetWeight / item.coilCount).toFixed(2)) : 0,
}));
this.aggregatedList.sort((a, b) => a.warehouseName.localeCompare(b.warehouseName, 'zh'));
this.$nextTick(() => { this.renderCharts(); });
},
// ============ 主页面图表 ============
renderCharts() { this.renderBarChart(); this.renderPieChart(); },
renderBarChart() {
const dom = this.$refs.barChartRef;
if (!dom) return;
if (!this.barChartInst) this.barChartInst = echarts.init(dom);
this.barChartInst.off('click');
this.barChartInst.on('click', params => { this.handleChartClick(params.name); });
this.barChartInst.setOption({
tooltip: {
trigger: 'axis', axisPointer: { type: 'cross', crossStyle: { color: '#999' } },
formatter: params => `${params[0].name}<br/>${params[0].marker} 总净重: ${params[0].value} t<br/>${params[1].marker} 均重: ${params[1].value} t`
},
legend: { data: ['总净重', '均重'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true },
xAxis: { type: 'category', data: this.aggregatedList.map(i => i.warehouseName), axisLabel: { rotate: 30, color: '#666' }, axisLine: { lineStyle: { color: '#ddd' } } },
yAxis: [
{ type: 'value', name: '总净重(t)', axisLabel: { color: '#666' }, splitLine: { lineStyle: { color: '#f0f0f0' } } },
{ type: 'value', name: '均重(t)', axisLabel: { color: '#666' }, splitLine: { show: false } },
],
series: [
{ name: '总净重', type: 'bar', data: this.aggregatedList.map(i => i.totalNetWeight), itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#165DFF' }, { offset: 1, color: '#6AA1FF' }]) }, emphasis: { itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#0E42D2' }, { offset: 1, color: '#4080FF' }]) } } },
{ name: '均重', type: 'line', yAxisIndex: 1, data: this.aggregatedList.map(i => i.avgNetWeight), lineStyle: { color: '#F7BA1E', width: 2 }, itemStyle: { color: '#F7BA1E' }, symbol: 'circle', symbolSize: 6 },
]
});
},
renderPieChart() {
const dom = this.$refs.pieChartRef;
if (!dom) return;
if (!this.pieChartInst) this.pieChartInst = echarts.init(dom);
this.pieChartInst.off('click');
this.pieChartInst.on('click', params => { this.handleChartClick(params.name); });
const colors = ['#165DFF','#14C9C9','#F7BA1E','#F77234','#722ED1','#00B42A','#F5319D','#0FC6C2','#FF7D00','#3491FA'];
this.pieChartInst.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} 卷 ({d}%)' },
series: [{ name: '钢卷数量', type: 'pie', radius: ['45%','72%'], center: ['50%','48%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 }, label: { show: true, formatter: '{b}\n{d}%' }, emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, data: this.aggregatedList.map((item, i) => ({ name: item.warehouseName, value: item.coilCount, itemStyle: { color: colors[i % colors.length] } })) }]
});
},
// ============ 弹窗 ============
handleChartClick(warehouseName) {
const row = this.aggregatedList.find(item => item.warehouseName === warehouseName);
if (row) this.handleRowClick(row);
},
handleRowClick(row) {
this.currentWarehouse = { warehouseId: row.warehouseId, warehouseName: row.warehouseName };
this.dialogTitle = `库区详情 - ${row.warehouseName}`;
this.dialogVisible = true;
this.dialogPageNum = 1;
this.dialogList = [];
this.dialogAllCoils = [];
this.dialogPivotCols = [];
this.dialogPivotRows = [];
this.dialogPivotTeams = [];
this.dialogPivotColTotals = null;
this.$nextTick(() => { this.loadDialogData(); });
},
loadDialogData() {
if (!this.currentWarehouse) return;
this.dialogLoading = true;
const baseParams = {
warehouseId: this.currentWarehouse.warehouseId,
qualityStatusCsv: this.queryParams.qualityStatusCsv || '',
dataType: 1, status: 0,
};
Promise.all([
listLightCoil({ ...baseParams, pageSize: 99999, pageNum: 1 }),
listMaterialCoil({ ...baseParams, pageNum: this.dialogPageNum, pageSize: this.dialogPageSize }),
]).then(([lightRes, detailRes]) => {
this.dialogAllCoils = lightRes || [];
this.dialogList = (detailRes.rows || []).map(item => ({ ...item, netWeight: item.netWeight != null ? parseFloat(item.netWeight).toFixed(2) : '0.00' }));
this.dialogTotal = detailRes.total || 0;
this.$nextTick(() => { this.buildDialogPivot(); });
}).catch(err => { console.error('加载库区详情失败:', err); this.$message.error('加载详情失败'); })
.finally(() => { this.dialogLoading = false; });
},
fetchDialogList() {
if (!this.currentWarehouse) return;
this.dialogLoading = true;
listMaterialCoil({ warehouseId: this.currentWarehouse.warehouseId, dataType: 1, status: 0, pageNum: this.dialogPageNum, pageSize: this.dialogPageSize })
.then(res => {
this.dialogList = (res.rows || []).map(item => ({ ...item, netWeight: item.netWeight != null ? parseFloat(item.netWeight).toFixed(2) : '0.00' }));
this.dialogTotal = res.total || 0;
}).catch(err => { console.error('加载钢卷明细失败:', err); })
.finally(() => { this.dialogLoading = false; });
},
handleExportDetail() {
if (!this.currentWarehouse) return;
this.download('wms/materialCoil/export', {
warehouseId: this.currentWarehouse.warehouseId,
dataType: 1,
status: 0,
}, `钢卷明细_${this.currentWarehouse.warehouseName}_${new Date().getTime()}.xlsx`);
},
// ============ 弹窗数据透视图(班组 × 品质) ============
getQualityGroup(qs) {
if (!qs) return '未知';
for (const g of this.mergeGroups) { if (g.qualities.includes(qs)) return g.name; }
return qs;
},
buildDialogPivot() {
if (this.dialogAllCoils.length === 0) {
this.dialogPivotCols = []; this.dialogPivotRows = []; this.dialogPivotTeams = []; this.dialogPivotColTotals = null;
return;
}
const colSet = new Set();
this.dialogAllCoils.forEach(c => colSet.add(this.getQualityGroup(c.qualityStatus)));
this.dialogPivotCols = [...colSet].sort().map(key => ({ key, label: key }));
const teamSet = new Set();
this.dialogAllCoils.forEach(c => teamSet.add(c.team || '未知'));
this.dialogPivotTeams = [...teamSet].sort((a, b) => a.localeCompare(b, 'zh'));
const cross = {};
const useWeight = this.dialogPivotMeasure === 'weight';
this.dialogAllCoils.forEach(c => {
const team = c.team || '未知';
const colKey = this.getQualityGroup(c.qualityStatus);
if (!cross[team]) cross[team] = {};
cross[team][colKey] = (cross[team][colKey] || 0) + (useWeight ? (parseFloat(c.netWeight) || 0) : 1);
});
this.dialogPivotColTotals = {};
this.dialogPivotCols.forEach(col => { this.dialogPivotColTotals[col.key] = 0; });
this.dialogPivotColTotals._total = 0;
this.dialogPivotRows = this.dialogPivotTeams.map(team => {
const row = { team };
let rowTotal = 0;
this.dialogPivotCols.forEach(col => {
const v = cross[team]?.[col.key] || 0;
row[col.key] = v;
rowTotal += v;
this.dialogPivotColTotals[col.key] += v;
});
row._total = rowTotal;
this.dialogPivotColTotals._total += rowTotal;
return row;
});
if (useWeight) {
Object.keys(this.dialogPivotColTotals).forEach(k => { this.dialogPivotColTotals[k] = parseFloat(this.dialogPivotColTotals[k].toFixed(2)); });
}
},
getDialogPivotCellColor(val) {
if (!val || val === 0) return '#c0c4cc';
const maxVal = this.dialogPivotColTotals?._total || 1;
const ratio = Math.min(val / Math.max(maxVal / this.dialogPivotCols.length / 2, 1), 1);
return `rgb(${Math.round(22 + (1 - ratio) * 200)},${Math.round(93 + (1 - ratio) * 100)},${Math.round(255 * (1 - ratio * 0.6))})`;
},
formatDialogPivotValue(val) {
if (val == null || val === 0) return this.dialogPivotMeasure === 'weight' ? '0' : '0';
return this.dialogPivotMeasure === 'weight' ? val.toFixed(2) : String(val);
},
pivotHeaderStyle() {
return { background: '#f5f7fa', color: '#303133', fontWeight: '600' };
},
handleDialogClosed() {
this.currentWarehouse = null;
},
// 计算距今时间
timeAgo(createTime) {
if (!createTime) return '-';
const then = new Date(createTime).getTime();
if (isNaN(then)) return '-';
const diff = Date.now() - then;
if (diff < 0) return '-';
const seconds = Math.floor(diff / 1000);
if (seconds < 60) return `${seconds}秒前`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
return `${days}天前`;
},
resetQuery() {
this.queryParams = { warehouseIds: Object.keys(this.warehouseMap), qualityStatusCsv: 'A+,A,A-,B+,B,B-' };
this.getList();
},
},
};
</script>
<style scoped>
.app-container { padding: 20px; }
h3 { margin: 16px 0 12px; font-size: 16px; color: #303133; }
h4 { margin: 8px 0; font-size: 15px; color: #303133; }
.chart-card { background: #fff; border-radius: 6px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.chart-title { margin: 0 0 8px; font-size: 15px; font-weight: 500; color: #303133; }
.chart-box { width: 100%; height: 380px; }
/* 数据透视图 */
.pivot-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.pivot-toolbar h4 { margin: 0; }
.pivot-actions { display: flex; gap: 10px; align-items: center; }
.pivot-table { width: 100%; }
.pivot-footer { display: flex; align-items: center; background: #f5f7fa; border: 1px solid #ebeef5; border-top: 0; padding: 8px 0; font-size: 14px; }
.pivot-footer .footer-label { width: 100px; text-align: center; font-weight: 600; color: #303133; }
.pivot-footer .footer-cell { flex: 1; min-width: 100px; text-align: center; color: #606266; }
.pivot-footer .footer-total { width: 110px; flex: none; }
/* 合并配置 */
.merge-group { background: #f5f7fa; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
.merge-group-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.merge-group-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.unassigned-section { margin-top: 16px; padding-top: 12px; border-top: 1px dashed #dcdfe6; }
.unassigned-section h4 { margin: 0 0 8px; font-size: 13px; color: #909399; }
.unassigned-section .el-tag { margin-right: 6px; margin-bottom: 6px; }
.clickable-row { cursor: pointer; }
.hint-list {
margin: 4px 0 0;
padding-left: 20px;
line-height: 1.8;
}
</style>