Merge branch '0.8.X' of http://49.232.154.205:10100/DeXun/klp-oa into 0.8.X
This commit is contained in:
@@ -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() {
|
||||
|
||||
652
klp-ui/src/views/wms/coil/stock/index.vue
Normal file
652
klp-ui/src/views/wms/coil/stock/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user