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