Files
klp-oa/klp-ui/src/views/wms/report/comparison.vue
砂糖 948e62daae fix(wms/report/comparison): 修复库存对比报表的查询参数错误
将原有的actionId、actionIds查询参数修正为coilId、coilIds,匹配接口实际需要的入参字段
2026-06-13 11:23:36 +08:00

386 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container 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.coilId).map(i => i.coilId).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, coilIds: 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>