This commit is contained in:
jhd
2026-07-01 11:10:11 +08:00
2 changed files with 655 additions and 2 deletions

View File

@@ -6,6 +6,7 @@
:disabled="disabled"
:size="size"
:multiple="multiple"
:collapse-tags="collapseTags"
filterable
@change="onChange"
style="width: 100%"
@@ -54,9 +55,9 @@ export default {
type: String,
default: 'mini'
},
showTop: {
collapseTags: {
type: Boolean,
default: false // 是否显示顶级节点
default: false
}
},
data() {

View File

@@ -0,0 +1,652 @@
<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>
<!-- 钢卷明细表格 -->
<h4 style="margin-top: 20px">钢卷明细</h4>
<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, 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; });
},
// ============ 弹窗数据透视图(班组 × 品质) ============
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>