Compare commits

...

5 Commits

Author SHA1 Message Date
48d12fe056 Merge branch '0.8.X' of http://49.232.154.205:10100/DeXun/klp-oa into 0.8.X 2026-06-13 11:15:42 +08:00
6edc6e1100 feat(wms-report): 新增对比报表页面和尺寸异常统计功能
1. 新增comparison.vue对比报表页面,支持基期/当期数据对比、快速环比查询、明细表格查看和列配置
2. 在action.vue报表中添加尺寸异常统计模块,统计长度和厚度异常的卷数、总重及占比
3. 新增告警阈值配置获取逻辑,从系统配置中读取长度和厚度异常阈值
2026-06-13 11:15:38 +08:00
12ea9b0b83 refactor(wms/coil/label): 为热卷号添加动态字体大小适配
移除了硬编码的字体样式,根据热卷号长度动态调整字号,避免长文本溢出标签
2026-06-13 11:14:25 +08:00
c149216ebd refactor: 多页面UI优化、接口清理与文案调整
1. 移除质检模板页面未使用的getCheckItem接口导入
2. 给成本综合页面表格添加固定高度适配布局
3. 更新物料告警页面的表格列文案:理论值→推论值、实测值→实际值、实际偏差值→实际偏差
4. 重构重定向菜单页面的样式,优化布局与视觉效果
2026-06-13 11:14:14 +08:00
325a93fd84 refactor: 将“理论长度/厚度”统一改为“推论长度/厚度”
修改了CoilSelector组件、CoilInfo组件和报表列设置中的相关文案,将统一术语表述为“推论”,替代原有的“理论”表述。
2026-06-13 11:13:49 +08:00
13 changed files with 606 additions and 91 deletions

View File

@@ -201,9 +201,9 @@ export const optionalColumns = [
{ label: '表面处理', value: 'surfaceTreatmentDesc' },
{ label: '镀层质量', value: 'zincLayer' },
{ label: '实测长度', value: 'actualLength' },
{ label: '论长度', value: 'theoreticalLength' },
{ label: '论长度', value: 'theoreticalLength' },
{ label: '实测厚度', value: 'actualThickness' },
{ label: '论厚度', value: 'theoreticalThickness' },
{ label: '论厚度', value: 'theoreticalThickness' },
{ label: '实测宽度', value: 'actualWidth' },
{ label: '毛重', value: 'grossWeight' },
{ label: '净重', value: 'netWeight' },

View File

@@ -27,7 +27,7 @@
</span>
</div>
<el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" />
<el-table v-loading="gridLoading" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table v-loading="gridLoading" height="calc(100vh - 140px)" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table-column label="日期" width="135" fixed>
<template slot-scope="s"><el-date-picker v-model="s.row.detailDate" type="date" value-format="yyyy-MM-dd" size="mini" style="width:124px" @change="sortGrid" /></template>
</el-table-column>

View File

@@ -191,7 +191,7 @@
<script>
import { listInspectionItemTemplate, getInspectionItemTemplate, delInspectionItemTemplate, addInspectionItemTemplate, updateInspectionItemTemplate, getInfoByInspectionItem } from "@/api/mes/qc/inspectionItemTemplate";
import { listCheckItem, getCheckItem, addCheckItem, updateCheckItem, delCheckItem } from "@/api/mes/qc/checkItem";
import { listCheckItem, addCheckItem, updateCheckItem, delCheckItem } from "@/api/mes/qc/checkItem";
import { addInspectionTaskWithItems } from "@/api/mes/qc/inspectionTask";
import CheckItemTransfer from '@/components/KLPService/CheckItemSelect/index'
import CoilSelector from "@/components/CoilSelector/index.vue";

View File

@@ -103,77 +103,87 @@ export default {
</script>
<style lang="scss" scoped>
$c: #5F7BA0;
.submenu-page {
padding: 24px;
max-width: 960px;
padding: 20px 24px;
max-width: 1200px;
margin: 0 auto;
}
// ===== HEADER =====
.submenu-page__header {
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #e4e7ed;
padding: 0 0 12px 0;
border-bottom: 1px solid #f0f2f5;
}
.submenu-page__parent-title {
font-size: 16px;
font-size: 15px;
font-weight: 600;
color: #303133;
}
// ===== SECTIONS =====
.submenu-page__list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
display: flex;
flex-direction: column;
gap: 28px;
}
.submenu-page__empty {
text-align: center;
padding: 60px 0;
padding: 40px 0;
color: #909399;
font-size: 14px;
font-size: 13px;
}
.submenu-group {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
transition: border-color 0.2s, box-shadow 0.2s;
// ===== GROUP =====
&:hover {
border-color: #5F7BA0;
box-shadow: 0 2px 8px rgba(95, 123, 160, 0.12);
}
.submenu-group {
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
overflow: visible;
}
.submenu-group__title {
display: flex;
align-items: center;
padding: 14px 16px;
padding: 0 0 10px 0;
margin-bottom: 12px;
cursor: pointer;
background: linear-gradient(135deg, #fafbfc, #f5f7fa);
border-bottom: 1px solid #f0f2f5;
transition: background 0.15s, border-color 0.15s;
border-bottom: 1px solid #f5f6f8;
background: transparent;
transition: border-color 0.15s;
user-select: none;
&:hover {
background: linear-gradient(135deg, #f0f2f5, #eceff3);
border-bottom-color: #e4e7ed;
border-bottom-color: $c;
.submenu-group__text { color: $c; }
.submenu-group__arrow { color: $c; transform: translateX(3px); }
}
}
.submenu-group__icon {
font-size: 18px;
color: #5F7BA0;
margin-right: 10px;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 10px;
background: rgba($c, 0.08);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
flex-shrink: 0;
font-size: 16px;
color: $c;
}
.submenu-group__text {
font-size: 14px;
font-size: 15px;
font-weight: 600;
color: #303133;
flex: 1;
@@ -181,89 +191,104 @@ export default {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.15s;
}
.submenu-group__count {
font-size: 12px;
color: #5F7BA0;
background: rgba(95, 123, 160, 0.08);
padding: 2px 8px;
color: #909399;
background: #f5f6f8;
padding: 2px 10px;
border-radius: 10px;
margin-right: 8px;
flex-shrink: 0;
}
.submenu-group__arrow {
font-size: 12px;
font-size: 13px;
color: #c0c4cc;
flex-shrink: 0;
transition: color 0.15s;
.submenu-group__title:hover & {
color: #5F7BA0;
}
transition: transform 0.2s, color 0.2s;
}
// ===== TILE GRID =====
.submenu-group__items {
padding: 6px 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
padding: 0;
background: transparent;
}
// ===== TILE =====
.submenu-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 16px 10px 24px;
padding: 16px 10px 14px;
cursor: pointer;
transition: background 0.15s, padding-left 0.2s;
background: #fff;
border: 1px solid #f0f2f5;
border-radius: 10px;
text-align: center;
transition: border-color 0.15s, box-shadow 0.15s;
&:hover {
background: rgba(95, 123, 160, 0.05);
padding-left: 28px;
border-color: $c;
box-shadow: 0 2px 10px rgba($c, 0.1);
.submenu-item__arrow { opacity: 1; }
}
&:not(:last-child) {
border-bottom: 1px solid #f7f7f7;
&:active {
background: #fafbfd;
}
}
.submenu-item__folder,
.submenu-item__doc {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
flex-shrink: 0;
font-size: 18px;
color: $c;
background: rgba($c, 0.07);
}
.submenu-item__folder {
font-size: 14px;
color: #e6a23c;
margin-right: 10px;
flex-shrink: 0;
}
.submenu-item__doc {
font-size: 14px;
color: #5F7BA0;
margin-right: 10px;
flex-shrink: 0;
background: rgba(#e6a23c, 0.08);
}
.submenu-item__text {
font-size: 13px;
color: #606266;
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 500;
color: #555;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.submenu-item__arrow {
font-size: 12px;
font-size: 11px;
color: #c0c4cc;
flex-shrink: 0;
margin-left: 4px;
transition: color 0.15s;
.submenu-item:hover & {
color: #5F7BA0;
}
margin-top: 6px;
opacity: 0;
transition: opacity 0.15s;
}
.submenu-item--leaf {
.submenu-item__doc {
color: #a8abb2;
background: #f5f6f8;
}
}
</style>

View File

@@ -67,14 +67,14 @@ export default {
label: '长度[m]',
children: [
{ label: '实测', key: 'actualLength' },
{ label: '论', key: 'theoreticalLength' }
{ label: '论', key: 'theoreticalLength' }
]
},
{
label: '厚度[mm]',
children: [
{ label: '实测', key: 'actualThickness' },
{ label: '论', key: 'theoreticalThickness' }
{ label: '论', key: 'theoreticalThickness' }
]
},
{ label: '排产厚度[mm]', key: 'scheduleThickness' },

View File

@@ -64,15 +64,15 @@
<span v-else>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="论值" align="center" prop="theoreticalVal" />
<el-table-column label="实值" align="center" prop="actualVal" />
<el-table-column label="论值" align="center" prop="theoreticalVal" />
<el-table-column label="实值" align="center" prop="actualVal" />
<el-table-column label="允许偏差" align="center" prop="allowDeviation">
<template slot-scope="scope">
<span v-if="scope.row.warningType === 'LENGTH'">{{ parseFloat((scope.row.allowDeviation * 100).toFixed(2)) }}%</span>
<span v-else>{{ scope.row.allowDeviation }}</span>
</template>
</el-table-column>
<el-table-column label="实际偏差" align="center" prop="deviationValue" />
<el-table-column label="实际偏差" align="center" prop="deviationValue" />
<el-table-column label="告警说明" align="center" prop="warningMsg" show-overflow-tooltip />
<el-table-column label="告警状态" align="center" prop="warningStatus">

View File

@@ -20,9 +20,10 @@
料卷号
</div>
<div
style="flex: 1; height: 100%; display: flex; align-items: center; justify-content: center; border: 1px solid #333; font-size: 1em !important; box-sizing: border-box; padding: 3px; word-break: break-all; overflow-wrap: break-word;"
class="value-cell enter-coil-no">
{{ content.enterCoilNo || '' }}
style="flex: 1; height: 100%; display: flex; align-items: center; justify-content: center; border: 1px solid #333; box-sizing: border-box; padding: 3px; word-break: break-all; overflow-wrap: break-word;"
:style="enterCoilNoStyle"
class="value-cell enter-coil-no">
{{ content.enterCoilNo || '' }}
</div>
</div>
<div style="display: flex; flex: 1; align-items: center;">
@@ -181,6 +182,16 @@ export default {
printScale: 1,
}
},
computed: {
enterCoilNoStyle() {
const len = (this.content.enterCoilNo || '').length;
if (len > 25) return { fontSize: '0.55em' };
if (len > 20) return { fontSize: '0.65em' };
if (len > 15) return { fontSize: '0.75em' };
if (len > 10) return { fontSize: '0.9em' };
return {};
}
},
mounted() {
// 使用 matchMedia 监听打印状态(更可靠)
this.printMediaQuery = window.matchMedia('print');

View File

@@ -11,7 +11,7 @@
</div>
<div class="grid-cell label-cell">原料号</div>
<div class="grid-cell value-cell enter-coil-no">
<div class="nob">{{ content.enterCoilNo || '' }}</div>
<div class="nob" :style="enterCoilNoStyle">{{ content.enterCoilNo || '' }}</div>
</div>
@@ -91,6 +91,16 @@ export default {
printMediaQuery: null
}
},
computed: {
enterCoilNoStyle() {
const len = (this.content.enterCoilNo || '').length;
if (len > 25) return { fontSize: '0.55em' };
if (len > 20) return { fontSize: '0.65em' };
if (len > 15) return { fontSize: '0.75em' };
if (len > 10) return { fontSize: '0.9em' };
return {};
}
},
mounted() {
this.printMediaQuery = window.matchMedia('print');
this.printMediaQuery.addListener(this.handlePrintMediaChange);
@@ -241,7 +251,7 @@ export default {
}
.current-coil-no, .enter-coil-no {
font-size: 1em !important;
font-size: 1em;
}
.qrcode-container {

View File

@@ -11,7 +11,7 @@
</div>
<div class="grid-cell label-cell">热卷号</div>
<div class="grid-cell value-cell">
<div class="nob enter-coil-no">{{ content.enterCoilNo || '' }}</div>
<div class="nob enter-coil-no" :style="enterCoilNoStyle">{{ content.enterCoilNo || '' }}</div>
</div>
<!-- 第二行规格钢种 -->
@@ -104,6 +104,16 @@ export default {
printMediaQuery: null
}
},
computed: {
enterCoilNoStyle() {
const len = (this.content.enterCoilNo || '').length;
if (len > 25) return { fontSize: '0.55em' };
if (len > 20) return { fontSize: '0.65em' };
if (len > 15) return { fontSize: '0.75em' };
if (len > 10) return { fontSize: '0.9em' };
return {};
}
},
mounted() {
this.printMediaQuery = window.matchMedia('print');
this.printMediaQuery.addListener(this.handlePrintMediaChange);
@@ -262,7 +272,7 @@ export default {
}
.current-coil-no, .enter-coil-no {
font-size: 1em !important;
font-size: 1em;
}

View File

@@ -11,7 +11,7 @@
</div>
<div class="grid-cell label-cell">热卷号</div>
<div class="grid-cell value-cell">
<div class="nob enter-coil-no">{{ content.enterCoilNo || '' }}</div>
<div class="nob enter-coil-no" :style="enterCoilNoStyle">{{ content.enterCoilNo || '' }}</div>
</div>
<!-- 第二行规格钢种 -->
@@ -104,6 +104,16 @@ export default {
printMediaQuery: null
}
},
computed: {
enterCoilNoStyle() {
const len = (this.content.enterCoilNo || '').length;
if (len > 25) return { fontSize: '0.55em' };
if (len > 20) return { fontSize: '0.65em' };
if (len > 15) return { fontSize: '0.75em' };
if (len > 10) return { fontSize: '0.9em' };
return {};
}
},
mounted() {
this.printMediaQuery = window.matchMedia('print');
this.printMediaQuery.addListener(this.handlePrintMediaChange);
@@ -262,7 +272,7 @@ export default {
}
.current-coil-no, .enter-coil-no {
font-size: 1em !important;
font-size: 1em;
}

View File

@@ -0,0 +1,385 @@
<template>
<div class="app-container comparison-report" v-loading="loading">
<!-- 查询条件 -->
<el-card shadow="never" class="filter-card">
<el-form label-width="70px" inline class="filter-form">
<el-form-item label="产线">
<el-select style="width: 150px;" v-model="actionTypes" placeholder="产线" clearable @change="handleQuery" size="small">
<el-option label="全部" value="" />
<el-option v-for="line in lineOptions" :key="line.value" :label="line.label" :value="line.value" />
</el-select>
</el-form-item>
<el-form-item label="品质">
<muti-select v-model="queryParams.qualityStatusCsv" :options="dict.type.coil_quality_status"
placeholder="品质" clearable style="width: 150px;" size="small" />
</el-form-item>
<el-form-item label="规格">
<memo-input style="width: 130px;" v-model="queryParams.itemSpecification" storageKey="coilSpec"
placeholder="规格" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材质">
<muti-select style="width: 150px;" v-model="queryParams.itemMaterial" :options="dict.type.coil_material"
placeholder="材质" clearable size="small" />
</el-form-item>
<el-form-item label="厂家">
<muti-select style="width: 150px;" v-model="queryParams.itemManufacturer"
:options="dict.type.coil_manufacturer" placeholder="厂家" clearable size="small" />
</el-form-item>
<el-form-item label="入场号">
<el-input style="width: 150px;" v-model="queryParams.enterCoilNo" placeholder="入场钢卷号" clearable size="small"
@keyup.enter.native="handleQuery" />
</el-form-item>
</el-form>
<div class="time-row">
<span class="time-label">基期</span>
<el-date-picker style="width: 170px;" v-model="baseStartTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始" size="small" />
<span class="time-sep"></span>
<el-date-picker style="width: 170px;" v-model="baseEndTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束" size="small" />
<span class="time-label current-label">当期</span>
<el-date-picker style="width: 170px;" v-model="currentStartTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始" size="small" />
<span class="time-sep"></span>
<el-date-picker style="width: 170px;" v-model="currentEndTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束" size="small" />
<el-button-group size="small" class="quick-btns">
<el-button @click="setQuickCompare('prevDay')">日环比</el-button>
<el-button @click="setQuickCompare('prevWeek')">周环比</el-button>
<el-button @click="setQuickCompare('prevMonth')">月环比</el-button>
<el-button @click="setQuickCompare('prevYear')">年环比</el-button>
</el-button-group>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button size="small" icon="el-icon-setting" @click="settingVisible = true">列配置</el-button>
</div>
</el-card>
<!-- 口径说明 -->
<el-alert type="info" :closable="false" class="explain-alert">
<span slot="title">
<strong>口径</strong>数值环比 = (当期基期)/|基期|×100%<span class="color-up">红增</span>/<span class="color-down">绿降</span>百分比环比 = 当期百分点基期百分点 M卷currentCoilNo含M且不在前五位不计入产出
</span>
</el-alert>
<!-- 指标卡片 -->
<div class="card-group">
<div class="group-title">环比对比统计</div>
<el-row :gutter="8">
<el-col :span="6" v-for="item in comparisonItems" :key="item.key">
<div class="kpi-card">
<div class="kpi-label">{{ item.label }}</div>
<div class="kpi-body">
<span class="kpi-base">{{ item.base }}</span>
<span class="kpi-arrow"></span>
<span class="kpi-current">{{ item.current }}</span>
</div>
<div class="kpi-rate" :style="{ color: item.rateColor }">{{ item.rate }}</div>
</div>
</el-col>
</el-row>
</div>
<div class="card-group">
<div class="group-title">已处理M统计信息对比</div>
<el-row :gutter="8">
<el-col :span="6" v-for="item in mComparisonItems" :key="item.key">
<div class="kpi-card">
<div class="kpi-label">{{ item.label }}</div>
<div class="kpi-body">
<span class="kpi-base">{{ item.base }}</span>
<span class="kpi-arrow"></span>
<span class="kpi-current">{{ item.current }}</span>
</div>
<div class="kpi-rate" :style="{ color: item.rateColor }">{{ item.rate }}</div>
</div>
</el-col>
</el-row>
</div>
<div class="card-group">
<div class="group-title">异常库位统计对比</div>
<el-row :gutter="8">
<el-col :span="6" v-for="item in abComparisonItems" :key="item.key">
<div class="kpi-card">
<div class="kpi-label">{{ item.label }}</div>
<div class="kpi-body">
<span class="kpi-base">{{ item.base }}</span>
<span class="kpi-arrow"></span>
<span class="kpi-current">{{ item.current }}</span>
</div>
<div class="kpi-rate" :style="{ color: item.rateColor }">{{ item.rate }}</div>
</div>
</el-col>
</el-row>
</div>
<!-- 明细信息 -->
<div class="detail-section">
<div class="group-title">明细信息</div>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="产出(当期)" name="currentOutput" />
<el-tab-pane label="投入(当期)" name="currentLoss" />
<el-tab-pane label="产出(基期)" name="baseOutput" />
<el-tab-pane label="投入(基期)" name="baseLoss" />
</el-tabs>
<coil-table :data="activeDetailList" :total="activeDetailTotal" :page-num="activeDetailPageNum"
:page-size="detailPageSize" :columns="activeDetailColumns" :loading="detailLoading"
@size-change="handleDetailSizeChange"
@current-change="handleDetailPageChange"
height="calc(100vh - 500px)" />
</div>
<el-dialog title="列设置" :visible.sync="settingVisible" width="50%">
<el-radio-group v-model="activeColumnConfig">
<el-radio-button label="coil-report-loss">投入明细</el-radio-button>
<el-radio-button label="coil-report-output">产出明细</el-radio-button>
</el-radio-group>
<columns-setting :reportType="activeColumnConfig" />
</el-dialog>
</div>
</template>
<script>
import { listLightCoil, listCoilWithIds } from "@/api/wms/coil";
import { listLightPendingAction } from '@/api/wms/pendingAction';
import MemoInput from "@/components/MemoInput";
import MutiSelect from "@/components/MutiSelect";
import { calcSummary, calcMSummary, calcAbSummary } from "@/views/wms/report/js/calc";
import CoilTable from "@/views/wms/report/components/coilTable";
import ColumnsSetting from "@/views/wms/report/components/setting/columns";
export default {
name: 'ComparisonReport',
components: { MemoInput, MutiSelect, CoilTable, ColumnsSetting },
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
data() {
const addZero = (num) => num.toString().padStart(2, '0')
const now = new Date()
const currentMonthStart = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-01 00:00:00`
const currentMonthEnd = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-${addZero(now.getDate())} 23:59:59`
const prevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
const prevMonthLastDay = new Date(now.getFullYear(), now.getMonth(), 0)
const baseMonthStart = `${prevMonth.getFullYear()}-${addZero(prevMonth.getMonth() + 1)}-01 00:00:00`
const baseMonthEnd = `${prevMonth.getFullYear()}-${addZero(prevMonth.getMonth() + 1)}-${addZero(Math.min(now.getDate(), prevMonthLastDay.getDate()))} 23:59:59`
return {
currentOutList: [], currentLossList: [], baseOutList: [], baseLossList: [],
currentOutDetailList: [], currentOutDetailTotal: 0, currentOutPageNum: 1,
currentLossDetailList: [], currentLossDetailTotal: 0, currentLossPageNum: 1,
baseOutDetailList: [], baseOutDetailTotal: 0, baseOutPageNum: 1,
baseLossDetailList: [], baseLossDetailTotal: 0, baseLossPageNum: 1,
detailPageSize: 20,
currentOutIds: '', currentLossActionIds: '', baseOutIds: '', baseLossActionIds: '',
activeTab: 'currentOutput', activeColumnConfig: 'coil-report-output',
settingVisible: false, loading: false, detailLoading: false,
currentStartTime: currentMonthStart, currentEndTime: currentMonthEnd,
baseStartTime: baseMonthStart, baseEndTime: baseMonthEnd,
queryParams: { enterCoilNo: '', currentCoilNo: '', itemName: '', itemSpecification: '', itemMaterial: '', itemManufacturer: '', qualityStatusCsv: '' },
lossColumns: [], outputColumns: [],
actionTypes: '',
lineOptions: [
{ label: '酸轧线', value: '11,120,201,520' }, { label: '镀锌线', value: '202,501,521' },
{ label: '双机架', value: '205,504,524' }, { label: '镀铬线', value: '206,505,525' },
{ label: '拉矫线', value: '204,503,523' }, { label: '脱脂线', value: '203,502,522' },
]
}
},
computed: {
summary() { return calcSummary(this.currentOutList, this.currentLossList) },
baseSummary() { return calcSummary(this.baseOutList, this.baseLossList) },
currentMSummary() { return calcMSummary(this.currentOutList, this.currentLossList) },
baseMSummary() { return calcMSummary(this.baseOutList, this.baseLossList) },
currentAbSummary() { return calcAbSummary(this.currentOutList) },
baseAbSummary() { return calcAbSummary(this.baseOutList) },
comparisonItems() {
return this.buildComparisonItems(this.summary, this.baseSummary, [
{ key: 'outCount', label: '产出数量' }, { key: 'outTotalWeight', label: '产出总重(t)' }, { key: 'outAvgWeight', label: '产出均重(t)' },
{ key: 'lossCount', label: '消耗数量' }, { key: 'lossTotalWeight', label: '消耗总重(t)' }, { key: 'lossAvgWeight', label: '消耗均重(t)' },
{ key: 'totalCount', label: '合计数量' }, { key: 'totalWeight', label: '合计总重(t)' }, { key: 'totalAvgWeight', label: '合计均重(t)' },
{ key: 'countDiff', label: '数量差值' }, { key: 'weightDiff', label: '总重差值' }, { key: 'avgWeightDiff', label: '均重差值(t)' },
{ key: 'passRate', label: '成品率', isPercent: true }, { key: 'lossRate', label: '损耗率', isPercent: true },
{ key: 'abRate', label: '异常率', isPercent: true }, { key: 'passRate2', label: '正品率', isPercent: true },
])
},
mComparisonItems() {
return this.buildComparisonItems(this.currentMSummary, this.baseMSummary, [
{ key: 'outCount', label: 'M-产出数量' }, { key: 'outTotalWeight', label: 'M-产出总重(t)' }, { key: 'outAvgWeight', label: 'M-产出均重(t)' },
{ key: 'lossCount', label: 'M-消耗数量' }, { key: 'lossTotalWeight', label: 'M-消耗总重(t)' }, { key: 'lossAvgWeight', label: 'M-消耗均重(t)' },
{ key: 'countDiff', label: 'M-数量差值' }, { key: 'weightDiff', label: 'M-总重差值' },
{ key: 'passRate', label: 'M-成品率', isPercent: true }, { key: 'lossRate', label: 'M-损耗率', isPercent: true },
{ key: 'abRate', label: 'M-异常率', isPercent: true }, { key: 'passRate2', label: 'M-正品率', isPercent: true },
])
},
abComparisonItems() {
const cur = this.currentAbSummary, base = this.baseAbSummary
const cm = {}, bm = {}
cur.forEach(i => { cm[i.label] = i.value })
base.forEach(i => { bm[i.label] = i.value })
return ['技术部钢卷数','小钢卷库钢卷数','废品库钢卷数','退货库钢卷数',
'技术部钢卷重量','小钢卷库钢卷重量','废品库钢卷重量','退货库钢卷重量',
'技术部占比','小钢卷库占比','废品库占比','退货库占比'].map(label => {
const { rate, rateColor } = this.calcSingleRate(cm[label] || 0, bm[label] || 0, label.includes('占比'))
return { key: label, label, base: bm[label] || 0, current: cm[label] || 0, rate, rateColor }
})
},
activeDetailList() {
const m = { currentOutput: 'currentOutDetailList', currentLoss: 'currentLossDetailList', baseOutput: 'baseOutDetailList', baseLoss: 'baseLossDetailList' }
return this[m[this.activeTab]] || []
},
activeDetailTotal() {
const m = { currentOutput: 'currentOutDetailTotal', currentLoss: 'currentLossDetailTotal', baseOutput: 'baseOutDetailTotal', baseLoss: 'baseLossDetailTotal' }
return this[m[this.activeTab]] || 0
},
activeDetailPageNum() {
const m = { currentOutput: 'currentOutPageNum', currentLoss: 'currentLossPageNum', baseOutput: 'baseOutPageNum', baseLoss: 'baseLossPageNum' }
return this[m[this.activeTab]] || 1
},
activeDetailColumns() {
return this.activeTab.endsWith('Output') ? this.outputColumns : this.lossColumns
},
},
created() { this.handleQuery(); this.loadColumns() },
watch: {
activeTab() { this.loadDetailData() }
},
methods: {
calcSingleRate(curVal, baseVal, isPercent) {
if (isPercent) {
const cp = parseFloat(curVal) || 0, bp = parseFloat(baseVal) || 0, diff = (cp - bp).toFixed(2)
return { rate: (diff >= 0 ? '+' : '') + diff + '%', rateColor: diff > 0 ? '#f56c6c' : diff < 0 ? '#67c23a' : '' }
}
const cv = parseFloat(curVal) || 0, bv = parseFloat(baseVal) || 0, diff = cv - bv
let label = '0.00%'
if (bv !== 0) { const r = ((cv - bv) / Math.abs(bv) * 100).toFixed(2); label = (r >= 0 ? '+' : '') + r + '%' }
else if (cv !== 0) label = '+∞'
return { rate: label, rateColor: diff > 0 ? '#f56c6c' : diff < 0 ? '#67c23a' : '' }
},
buildComparisonItems(current, base, itemDefs) {
return itemDefs.map(item => {
const { rate, rateColor } = this.calcSingleRate(current[item.key], base[item.key], item.isPercent)
return { key: item.key, label: item.label, base: base[item.key], current: current[item.key], rate, rateColor }
})
},
setQuickCompare(type) {
const now = new Date(), addZero = n => n.toString().padStart(2, '0'), fd = d => `${d.getFullYear()}-${addZero(d.getMonth()+1)}-${addZero(d.getDate())}`
switch (type) {
case 'prevDay': {
const y = new Date(now); y.setDate(y.getDate()-1)
this.currentStartTime = `${fd(y)} 00:00:00`; this.currentEndTime = `${fd(y)} 23:59:59`
const by = new Date(y); by.setDate(by.getDate()-1)
this.baseStartTime = `${fd(by)} 00:00:00`; this.baseEndTime = `${fd(by)} 23:59:59`; break
}
case 'prevWeek': {
const wa = new Date(now); wa.setDate(wa.getDate()-7)
this.currentStartTime = `${fd(wa)} 00:00:00`; this.currentEndTime = `${fd(now)} 23:59:59`
const twa = new Date(wa); twa.setDate(twa.getDate()-7)
this.baseStartTime = `${fd(twa)} 00:00:00`; this.baseEndTime = `${fd(wa)} 23:59:59`; break
}
case 'prevMonth': {
this.currentStartTime = `${fd(new Date(now.getFullYear(),now.getMonth(),1))} 00:00:00`; this.currentEndTime = `${fd(now)} 23:59:59`
this.baseStartTime = `${fd(new Date(now.getFullYear(),now.getMonth()-1,1))} 00:00:00`; this.baseEndTime = `${fd(new Date(now.getFullYear(),now.getMonth(),0))} 23:59:59`; break
}
case 'prevYear': {
this.currentStartTime = `${fd(new Date(now.getFullYear(),0,1))} 00:00:00`; this.currentEndTime = `${fd(now)} 23:59:59`
this.baseStartTime = `${fd(new Date(now.getFullYear()-1,0,1))} 00:00:00`; this.baseEndTime = `${fd(new Date(now.getFullYear()-1,now.getMonth(),now.getDate()))} 23:59:59`; break
}
}
this.handleQuery()
},
handleQuery() { this.currentOutPageNum = this.currentLossPageNum = this.baseOutPageNum = this.baseLossPageNum = 1; this.fetchData() },
async fetchData() {
this.loading = true
const baseQuery = { enterCoilNo: this.queryParams.enterCoilNo, currentCoilNo: this.queryParams.currentCoilNo,
itemName: this.queryParams.itemName, itemSpecification: this.queryParams.itemSpecification,
itemMaterial: this.queryParams.itemMaterial, itemManufacturer: this.queryParams.itemManufacturer,
qualityStatusCsv: this.queryParams.qualityStatusCsv }
const mapItems = list => (list || []).map(item => {
const [th, w] = item.specification?.split('*') || []
return { ...item, computedThickness: parseFloat(th), computedWidth: parseFloat(w) }
})
const fetchPeriod = async (st, et) => {
const p = { ...baseQuery, startTime: st, endTime: et, actionTypes: this.actionTypes, actionStatus: 2 }
const acts = await listLightPendingAction(p)
const oIds = acts.data.map(i => i.processedCoilIds).filter(Boolean).join(',')
const lIds = acts.data.filter(i => i.actionId).map(i => i.actionId).join(',')
if (!oIds || !lIds) return { outList: [], lossList: [], outIds: '', lossActionIds: '' }
const [oRes, lRes] = await Promise.all([
listLightCoil({ ...p, coilIds: oIds, startTime: '', endTime: '', byCreateTimeStart: st, byCreateTimeEnd: et, selectType: 'product', pageSize: 99999, pageNum: 1 }),
listLightCoil({ ...p, actionIds: lIds, startTime: '', endTime: '', selectType: 'raw_material', pageSize: 99999, pageNum: 1 }),
])
return { outList: mapItems(oRes), lossList: mapItems(lRes), outIds: oIds, lossActionIds: lIds }
}
const [cr, br] = await Promise.all([
fetchPeriod(this.currentStartTime, this.currentEndTime),
fetchPeriod(this.baseStartTime, this.baseEndTime),
])
this.currentOutList = cr.outList; this.currentLossList = cr.lossList; this.baseOutList = br.outList; this.baseLossList = br.lossList
this.currentOutIds = cr.outIds; this.currentLossActionIds = cr.lossActionIds; this.baseOutIds = br.outIds; this.baseLossActionIds = br.lossActionIds
this.loading = false
this.loadDetailData()
},
async loadDetailData() {
const cfgMap = {
currentOutput: { ids: 'currentOutIds', list: 'currentOutDetailList', total: 'currentOutDetailTotal', pn: 'currentOutPageNum', type: 'product' },
currentLoss: { ids: 'currentLossActionIds', list: 'currentLossDetailList', total: 'currentLossDetailTotal', pn: 'currentLossPageNum', type: 'raw_material' },
baseOutput: { ids: 'baseOutIds', list: 'baseOutDetailList', total: 'baseOutDetailTotal', pn: 'baseOutPageNum', type: 'product' },
baseLoss: { ids: 'baseLossActionIds', list: 'baseLossDetailList', total: 'baseLossDetailTotal', pn: 'baseLossPageNum', type: 'raw_material' },
}
const c = cfgMap[this.activeTab]
if (!c) return
this.detailLoading = true
if (!this[c.ids]) {
this[c.list] = []; this[c.total] = 0; this.detailLoading = false; return
}
const isOut = c.type === 'product'
const res = await listCoilWithIds(isOut
? { ...this.queryParams, coilIds: this[c.ids], selectType: 'product', pageSize: this.detailPageSize, pageNum: this[c.pn] }
: { ...this.queryParams, actionIds: this[c.ids], selectType: 'raw_material', pageSize: this.detailPageSize, pageNum: this[c.pn] })
this[c.list] = (res.rows || []).map(item => {
const [th, w] = item.specification?.split('*') || []
return { ...item, computedThickness: parseFloat(th), computedWidth: parseFloat(w) }
})
this[c.total] = res.total || 0; this.detailLoading = false
},
handleDetailPageChange(page) { this[this.activeTab + 'PageNum'] = page; this.loadDetailData() },
handleDetailSizeChange(size) { this.detailPageSize = size; this[this.activeTab + 'PageNum'] = 1; this.loadDetailData() },
loadColumns() {
this.lossColumns = JSON.parse(localStorage.getItem('preference-tableColumns-coil-report-loss') || '[]') || []
this.outputColumns = JSON.parse(localStorage.getItem('preference-tableColumns-coil-report-output') || '[]') || []
}
}
}
</script>
<style scoped>
.comparison-report { min-width: 900px; }
.filter-card { margin-bottom: 8px; }
.filter-card >>> .el-card__body { padding: 8px 12px 8px; }
.filter-form { margin-bottom: 0; }
.filter-form .el-form-item { margin-bottom: 6px; margin-right: 4px; }
.filter-form >>> .el-form-item__label { font-size: 12px; }
.time-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding-top: 6px; border-top: 1px solid #eee; }
.time-label { font-size: 12px; color: #909399; font-weight: 600; }
.current-label { color: #409eff; }
.time-sep { color: #c0c4cc; font-size: 12px; }
.quick-btns { margin-left: auto; }
.explain-alert { margin-bottom: 8px; }
.explain-alert >>> .el-alert__title { font-size: 12px; line-height: 1.5; }
.color-up { color: #f56c6c; }
.color-down { color: #67c23a; }
.card-group { margin-bottom: 10px; }
.group-title { font-size: 13px; font-weight: 600; color: #303133; padding: 5px 0; margin-bottom: 6px; border-bottom: 2px solid #409eff; }
.kpi-card { background: #fafbfc; border: 1px solid #e8eaed; border-radius: 4px; padding: 10px 12px; text-align: center; transition: box-shadow .15s; }
.kpi-card:hover { box-shadow: 0 2px 6px rgba(0,0,0,.06); }
.kpi-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
.kpi-body { font-size: 15px; font-weight: 600; margin-bottom: 2px; display: flex; align-items: center; justify-content: center; gap: 6px; }
.kpi-arrow { color: #c0c4cc; font-size: 12px; }
.kpi-base { color: #909399; }
.kpi-current { color: #409eff; }
.kpi-rate { font-size: 13px; font-weight: 600; }
.detail-section { margin-top: 8px; }
.detail-section >>> .el-tabs__header { margin-bottom: 4px; }
.detail-section >>> .el-tabs__item { font-size: 12px; height: 32px; line-height: 32px; }
</style>

View File

@@ -155,12 +155,12 @@ export default {
{ label: '镀层质量', value: 'zincLayer' },
{ label: '实测长度', value: 'actualLength' },
{ label: '论长度', value: 'theoreticalLength' },
{ label: '论长度', value: 'theoreticalLength' },
{ label: '长度差值', value: 'lengthDiff' },
{ label: '厚度', value: 'computedThickness' },
{ label: '实测厚度', value: 'actualThickness' },
{ label: '论厚度', value: 'theoreticalThickness' },
{ label: '论厚度', value: 'theoreticalThickness' },
{ label: '排产厚度', value: 'scheduleThickness' },
{ label: '厚度差值', value: 'thicknessDiff' },

View File

@@ -265,6 +265,17 @@
<!-- 分条信息统计 -->
<split-summary v-if="productionLine == '分条线'" :origin-outputlist="outList" :origin-loss-list="lossList"
:common-coil-ids="commonCoilIds"></split-summary>
<el-descriptions title="尺寸异常统计" :column="4" border>
<el-descriptions-item label="长度异常卷数">{{ sizeAbnormalSummary.length.count }}</el-descriptions-item>
<el-descriptions-item label="长度异常总重">{{ sizeAbnormalSummary.length.weight }}t</el-descriptions-item>
<el-descriptions-item label="长度异常数量占比">{{ sizeAbnormalSummary.length.countRate }}</el-descriptions-item>
<el-descriptions-item label="长度异常重量占比">{{ sizeAbnormalSummary.length.weightRate }}</el-descriptions-item>
<el-descriptions-item label="厚度异常卷数">{{ sizeAbnormalSummary.thickness.count }}</el-descriptions-item>
<el-descriptions-item label="厚度异常总重">{{ sizeAbnormalSummary.thickness.weight }}t</el-descriptions-item>
<el-descriptions-item label="厚度异常数量占比">{{ sizeAbnormalSummary.thickness.countRate }}</el-descriptions-item>
<el-descriptions-item label="厚度异常重量占比">{{ sizeAbnormalSummary.thickness.weightRate }}</el-descriptions-item>
</el-descriptions>
</template>
<!-- team, day, month, year 类型的统计信息 -->
@@ -344,6 +355,17 @@
<div ref="monthChart" style="width: 100%; height: 350px;"></div>
</el-card>
</div>
<el-descriptions title="尺寸异常统计" :column="4" border>
<el-descriptions-item label="长度异常卷数">{{ sizeAbnormalSummary.length.count }}</el-descriptions-item>
<el-descriptions-item label="长度异常总重">{{ sizeAbnormalSummary.length.weight }}t</el-descriptions-item>
<el-descriptions-item label="长度异常数量占比">{{ sizeAbnormalSummary.length.countRate }}</el-descriptions-item>
<el-descriptions-item label="长度异常重量占比">{{ sizeAbnormalSummary.length.weightRate }}</el-descriptions-item>
<el-descriptions-item label="厚度异常卷数">{{ sizeAbnormalSummary.thickness.count }}</el-descriptions-item>
<el-descriptions-item label="厚度异常总重">{{ sizeAbnormalSummary.thickness.weight }}t</el-descriptions-item>
<el-descriptions-item label="厚度异常数量占比">{{ sizeAbnormalSummary.thickness.countRate }}</el-descriptions-item>
<el-descriptions-item label="厚度异常重量占比">{{ sizeAbnormalSummary.thickness.weightRate }}</el-descriptions-item>
</el-descriptions>
</template>
<!-- 明细信息和标签页 -->
@@ -547,7 +569,9 @@ export default {
],
lossColumns: [],
outputColumns: [],
actionIds: ''
actionIds: '',
lengthThreshold: 0,
thicknessThreshold: 0
}
},
computed: {
@@ -610,6 +634,41 @@ export default {
customExportStorageKey() {
return `coil-report-action-${this.actionType}`
},
sizeAbnormalSummary() {
const totalCount = this.outList.length
const totalWeight = this.outList.reduce((acc, cur) => acc + (parseFloat(cur.netWeight) || 0), 0)
const lengthAbnormal = this.outList.filter(row => {
const lengthDiff = (row.actualLength || 0) - (row.theoreticalLength || 0)
const theoreticalLength = row.theoreticalLength || 1
return Math.abs(lengthDiff) / theoreticalLength > this.lengthThreshold
})
const thicknessAbnormal = this.outList.filter(row => {
const thicknessDiff = (row.theoreticalThickness || 0) - (row.computedThickness || 0)
return thicknessDiff > this.thicknessThreshold
})
const laCount = lengthAbnormal.length
const laWeight = lengthAbnormal.reduce((acc, cur) => acc + (parseFloat(cur.netWeight) || 0), 0)
const taCount = thicknessAbnormal.length
const taWeight = thicknessAbnormal.reduce((acc, cur) => acc + (parseFloat(cur.netWeight) || 0), 0)
return {
length: {
count: laCount,
weight: laWeight.toFixed(2),
countRate: totalCount > 0 ? (laCount / totalCount * 100).toFixed(2) + '%' : '0.00%',
weightRate: totalWeight > 0 ? (laWeight / totalWeight * 100).toFixed(2) + '%' : '0.00%',
},
thickness: {
count: taCount,
weight: taWeight.toFixed(2),
countRate: totalCount > 0 ? (taCount / totalCount * 100).toFixed(2) + '%' : '0.00%',
weightRate: totalWeight > 0 ? (taWeight / totalWeight * 100).toFixed(2) + '%' : '0.00%',
}
}
},
},
watch: {
warehouseOptions: {
@@ -625,6 +684,7 @@ export default {
this.initDateByReportType()
this.handleQuery()
this.loadColumns()
this.getAlarmThreshold()
},
methods: {
initDateByReportType() {
@@ -1102,6 +1162,10 @@ export default {
this.loading = false
})
},
getAlarmThreshold() {
this.getConfigKey('material.warning.length').then(response => { this.lengthThreshold = parseFloat(response.msg) || 0 })
this.getConfigKey('material.warning.thickness').then(response => { this.thicknessThreshold = parseFloat(response.msg) || 0 })
},
loadColumns() {
this.lossColumns = JSON.parse(localStorage.getItem('preference-tableColumns-coil-report-loss') || '[]') || []
this.outputColumns = JSON.parse(localStorage.getItem('preference-tableColumns-coil-report-output') || '[]') || []