优化成本计算问题,加入辅料备件分摊页面

This commit is contained in:
2026-01-26 19:29:11 +08:00
parent 50fa87115d
commit d5cbaab645
14 changed files with 1754 additions and 117 deletions

View File

@@ -60,3 +60,21 @@ export function exportCoilTotalMerged(query) {
responseType: 'blob'
})
}
// 辅料分摊构成(按物料汇总)
export function fetchAuxMaterialBreakdown(query) {
return request({
url: '/ems/energy/report/material/aux/breakdown',
method: 'get',
params: query
})
}
// 备件分摊构成(按备件汇总)
export function fetchSparePartBreakdown(query) {
return request({
url: '/ems/energy/report/material/spare/breakdown',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,496 @@
<template>
<div class="material-allocation-page">
<el-card class="search-card">
<el-form :model="queryParams" inline label-width="100px" size="small">
<el-form-item label="入场卷号" required>
<el-input v-model="queryParams.enterCoilNo" placeholder="必填,支持模糊"></el-input>
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="queryParams.currentCoilNo" placeholder="支持模糊"></el-input>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="queryParams.startDate" type="date" value-format="yyyy-MM-dd" placeholder="开始日期"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="queryParams.endDate" type="date" value-format="yyyy-MM-dd" placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="16" class="stats-row">
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">辅料分摊合计</div>
<div class="value">¥ {{ formatNumber(auxSummary.totalAuxCost, 2) }}</div>
<div class="desc">按日池 + 时长占比分摊</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">卷数</div>
<div class="value">{{ mergedTotal }}</div>
<div class="desc">当前查询条件下</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">种类数</div>
<div class="value">{{ breakdownSummary.kinds }}</div>
<div class="desc">按物料聚合</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">分摊数量合计</div>
<div class="value">{{ formatNumber(breakdownSummary.totalAllocatedQty, 2) }}</div>
<div class="desc">数量按分摊系数切分</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="4">
<div class="stat-card">
<div class="label">分摊金额合计</div>
<div class="value">¥ {{ formatNumber(breakdownSummary.totalAllocatedAmount, 2) }}</div>
<div class="desc">金额按分摊系数切分</div>
</div>
</el-col>
</el-row>
<div class="section">
<div class="section-header">
<div class="title">分摊数据</div>
<div class="actions">
<el-button size="mini" type="primary" icon="el-icon-download" @click="exportSummary">导出汇总</el-button>
<el-button size="mini" icon="el-icon-document" @click="exportDetailCsv" :disabled="detailRows.length === 0">导出明细CSV</el-button>
<el-button size="mini" icon="el-icon-document" @click="exportBreakdownCsv" :disabled="breakdownRows.length === 0">导出构成CSV</el-button>
</div>
</div>
<div class="sub-title">辅料消耗构成</div>
<el-table :data="breakdownRows" border stripe v-loading="breakdownLoading">
<el-table-column prop="itemCode" label="编码" width="140"></el-table-column>
<el-table-column prop="itemName" label="名称" min-width="160"></el-table-column>
<el-table-column prop="spec" label="规格" min-width="140"></el-table-column>
<el-table-column prop="unit" label="单位" width="80"></el-table-column>
<el-table-column prop="totalQty" label="全厂消耗数量" width="140" align="right">
<template slot-scope="scope">{{ formatNumber(scope.row.totalQty, 2) }}</template>
</el-table-column>
<el-table-column prop="totalAmount" label="全厂消耗金额(元)" width="160" align="right">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.totalAmount, 2) }}</template>
</el-table-column>
<el-table-column prop="allocatedQty" label="分摊数量" width="120" align="right">
<template slot-scope="scope">{{ formatNumber(scope.row.allocatedQty, 2) }}</template>
</el-table-column>
<el-table-column prop="allocatedAmount" label="分摊金额(元)" width="150" align="right">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.allocatedAmount, 2) }}</template>
</el-table-column>
</el-table>
<pagination
v-show="breakdownTotal > 0"
:total="breakdownTotal"
:page.sync="breakdownQuery.pageNum"
:limit.sync="breakdownQuery.pageSize"
@pagination="loadBreakdown"
/>
<div class="sub-title" style="margin-top: 16px;">辅料成本分摊</div>
<el-table :data="mergedRows" border stripe v-loading="loading">
<el-table-column prop="enterCoilNo" label="入场卷号"></el-table-column>
<el-table-column prop="currentCoilNo" label="当前卷号"></el-table-column>
<el-table-column prop="auxCost" label="辅料成本">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.auxCost, 2) }}</template>
</el-table-column>
<el-table-column prop="totalNetWeight" label="净重">
<template slot-scope="scope">{{ formatNumber(scope.row.totalNetWeight, 2) }}</template>
</el-table-column>
<el-table-column prop="totalGrossWeight" label="毛重">
<template slot-scope="scope">{{ formatNumber(scope.row.totalGrossWeight, 2) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openDetail(scope.row)">分摊明细</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="mergedTotal > 0"
:total="mergedTotal"
:page.sync="mergedQuery.pageNum"
:limit.sync="mergedQuery.pageSize"
@pagination="loadMerged"
/>
</div>
<el-dialog title="辅料分摊明细(按日)" :visible.sync="detailVisible" width="80%" :close-on-click-modal="false">
<div class="detail-hint">入场卷号{{ detailEnterCoilNo }}</div>
<el-table :data="detailRows" stripe border max-height="420" v-loading="detailLoading">
<el-table-column prop="day" label="日期" width="120"></el-table-column>
<el-table-column prop="auxPoolAmount" label="当日辅料池(元)" width="140">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.auxPoolAmount, 2) }}</template>
</el-table-column>
<el-table-column prop="totalMinutes" label="全厂分钟" width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.totalMinutes, 0) }}</template>
</el-table-column>
<el-table-column prop="coilMinutes" label="卷分钟" width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.coilMinutes, 0) }}</template>
</el-table-column>
<el-table-column prop="factor" label="占比" width="100">
<template slot-scope="scope">{{ toPercent(scope.row.factor) }}</template>
</el-table-column>
<el-table-column prop="allocatedAuxCost" label="分摊辅料(元)" width="140">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.allocatedAuxCost, 2) }}</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { fetchCoilTotalMerged, exportCoilTotalMerged, fetchAuxMaterialBreakdown } from '@/api/ems/energyCostReport'
export default {
name: 'AuxCostAllocation',
data() {
return {
queryParams: {
enterCoilNo: '',
currentCoilNo: '',
startDate: undefined,
endDate: undefined
},
mergedQuery: {
pageNum: 1,
pageSize: 50
},
loading: false,
mergedRows: [],
mergedTotal: 0,
auxSummary: {
totalAuxCost: 0
},
detailVisible: false,
detailLoading: false,
detailEnterCoilNo: '',
detailRows: [],
breakdownLoading: false,
breakdownRows: [],
breakdownTotal: 0,
breakdownQuery: {
pageNum: 1,
pageSize: 10
},
breakdownSummary: {
kinds: 0,
totalAllocatedQty: 0,
totalAllocatedAmount: 0
}
}
},
mounted() {
this.handleSearch()
},
methods: {
handleSearch() {
this.mergedQuery.pageNum = 1
this.breakdownQuery.pageNum = 1
this.loadMerged()
this.loadBreakdown()
},
handleReset() {
this.queryParams = {
enterCoilNo: '',
currentCoilNo: '',
startDate: undefined,
endDate: undefined
}
this.mergedQuery.pageNum = 1
this.breakdownQuery.pageNum = 1
this.mergedRows = []
this.mergedTotal = 0
this.auxSummary = { totalAuxCost: 0 }
this.detailRows = []
this.breakdownRows = []
this.breakdownTotal = 0
this.breakdownSummary = { kinds: 0, totalAllocatedQty: 0, totalAllocatedAmount: 0 }
this.handleSearch()
},
loadMerged() {
const params = {
...this.queryParams,
pageNum: this.mergedQuery.pageNum,
pageSize: this.mergedQuery.pageSize
}
this.loading = true
fetchCoilTotalMerged(params).then(res => {
this.mergedRows = res.rows || []
this.mergedTotal = res.total || 0
const rows = this.mergedRows || []
this.auxSummary = {
totalAuxCost: rows.reduce((sum, r) => sum + (Number(r.auxCost) || 0), 0)
}
}).finally(() => {
this.loading = false
})
},
loadBreakdown() {
const params = {
...this.queryParams,
pageNum: this.breakdownQuery.pageNum,
pageSize: this.breakdownQuery.pageSize
}
this.breakdownLoading = true
fetchAuxMaterialBreakdown(params).then(res => {
this.breakdownRows = res.rows || []
this.breakdownTotal = res.total || 0
const rows = this.breakdownRows || []
this.breakdownSummary = {
kinds: this.breakdownTotal || rows.length || 0,
totalAllocatedQty: rows.reduce((sum, r) => sum + (Number(r.allocatedQty) || 0), 0),
totalAllocatedAmount: rows.reduce((sum, r) => sum + (Number(r.allocatedAmount) || 0), 0)
}
}).finally(() => {
this.breakdownLoading = false
})
},
exportBreakdownCsv() {
const headers = ['编码', '名称', '规格', '单位', '全厂消耗数量', '全厂消耗金额(元)', '分摊数量', '分摊金额(元)']
const lines = [headers.join(',')]
;(this.breakdownRows || []).forEach(r => {
const line = [
this.csvCell(r.itemCode),
this.csvCell(r.itemName),
this.csvCell(r.spec),
this.csvCell(r.unit),
this.csvCell(r.totalQty),
this.csvCell(r.totalAmount),
this.csvCell(r.allocatedQty),
this.csvCell(r.allocatedAmount)
].join(',')
lines.push(line)
})
const csv = lines.join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `aux_allocation_breakdown_${this.queryParams.enterCoilNo || 'all'}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
openDetail(row) {
if (!row || !row.enterCoilNo) return
this.detailEnterCoilNo = row.enterCoilNo
this.detailVisible = true
this.buildDetailByDay(row.enterCoilNo)
},
buildDetailByDay(enterCoilNo) {
// 明细口径按天切分A 方案),当日池金额 * (卷分钟/全厂分钟)
// 当前后端 merged 接口仅返回汇总后的 auxCost/spareCost不返回按日池/分钟细节。
// 因此这里采用前端“可解释明细”的方式:
// - 先拿同查询条件下的 merged 列表(分页内),并仅对选中卷号展示其 auxCost总额按单行 auxCost 展示为 1 行。
// 若后续补充后端明细接口,可在此替换为真实的按日明细。
this.detailLoading = true
const params = {
...this.queryParams,
enterCoilNo,
pageNum: 1,
pageSize: 1
}
fetchCoilTotalMerged(params).then(res => {
const row = (res.rows || [])[0]
const total = Number(row?.auxCost) || 0
this.detailRows = [
{
day: this.queryParams.startDate && this.queryParams.endDate ? `${this.queryParams.startDate} ~ ${this.queryParams.endDate}` : '-',
auxPoolAmount: null,
totalMinutes: null,
coilMinutes: null,
factor: null,
allocatedAuxCost: total
}
]
}).finally(() => {
this.detailLoading = false
})
},
exportSummary() {
const params = { ...this.queryParams }
exportCoilTotalMerged(params).then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'aux_cost_allocation_summary.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
},
exportDetailCsv() {
// 仅导出当前弹窗可见的 detailRows
const headers = ['日期', '当日辅料池(元)', '全厂分钟', '卷分钟', '占比', '分摊辅料(元)']
const lines = [headers.join(',')]
;(this.detailRows || []).forEach(r => {
const line = [
this.csvCell(r.day),
this.csvCell(r.auxPoolAmount),
this.csvCell(r.totalMinutes),
this.csvCell(r.coilMinutes),
this.csvCell(r.factor),
this.csvCell(r.allocatedAuxCost)
].join(',')
lines.push(line)
})
const csv = lines.join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `aux_cost_allocation_detail_${this.detailEnterCoilNo || ''}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
csvCell(val) {
if (val === null || val === undefined) return ''
const s = String(val)
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"'
}
return s
},
formatNumber(val, digits = 2) {
if (val === null || val === undefined) return '-'
const num = Number(val)
if (Number.isNaN(num)) return String(val)
return num.toFixed(digits)
},
toPercent(val) {
if (val === null || val === undefined) return '-'
const num = Number(val)
if (Number.isNaN(num)) return String(val)
return (num * 100).toFixed(2) + '%'
}
}
}
</script>
<style scoped lang="scss">
.material-allocation-page {
padding: 16px 20px;
.search-card {
margin-bottom: 16px;
:deep(.el-card__body) {
padding: 16px 20px;
}
}
.stats-row {
margin-bottom: 24px;
.stat-card {
background: #fff;
border-radius: 4px;
padding: 16px;
height: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
border: 1px solid #ebeef5;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.value {
font-size: 22px;
font-weight: 600;
color: #303133;
line-height: 1.2;
margin: 8px 0 4px;
}
.desc {
font-size: 12px;
color: #909399;
margin-top: 6px;
}
}
}
.section {
background: #fff;
border-radius: 4px;
padding: 16px 20px;
margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
border: 1px solid #ebeef5;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.actions {
display: flex;
gap: 8px;
}
}
.sub-title {
font-size: 14px;
font-weight: 500;
color: #606266;
margin: 16px 0 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
.el-table {
margin-top: 8px;
th {
background-color: #f5f7fa;
color: #303133;
font-weight: 600;
}
}
.pagination-container {
margin-top: 16px;
padding: 0;
}
.detail-hint {
margin-bottom: 12px;
color: #606266;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,492 @@
<template>
<div class="material-allocation-page">
<el-card class="search-card">
<el-form :model="queryParams" inline label-width="100px" size="small">
<el-form-item label="入场卷号" required>
<el-input v-model="queryParams.enterCoilNo" placeholder="必填,支持模糊"></el-input>
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="queryParams.currentCoilNo" placeholder="支持模糊"></el-input>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="queryParams.startDate" type="date" value-format="yyyy-MM-dd" placeholder="开始日期"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="queryParams.endDate" type="date" value-format="yyyy-MM-dd" placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="16" class="stats-row">
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">备件分摊合计</div>
<div class="value">¥ {{ formatNumber(spareSummary.totalSpareCost, 2) }}</div>
<div class="desc">按日池 + 时长占比分摊</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">卷数</div>
<div class="value">{{ mergedTotal }}</div>
<div class="desc">当前查询条件下</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">种类数</div>
<div class="value">{{ breakdownSummary.kinds }}</div>
<div class="desc">按备件聚合</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="5">
<div class="stat-card">
<div class="label">分摊数量合计</div>
<div class="value">{{ formatNumber(breakdownSummary.totalAllocatedQty, 2) }}</div>
<div class="desc">数量按分摊系数切分</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="4">
<div class="stat-card">
<div class="label">分摊金额合计</div>
<div class="value">¥ {{ formatNumber(breakdownSummary.totalAllocatedAmount, 2) }}</div>
<div class="desc">金额按分摊系数切分</div>
</div>
</el-col>
</el-row>
<div class="section">
<div class="section-header">
<div class="title">分摊数据</div>
<div class="actions">
<el-button size="mini" type="primary" icon="el-icon-download" @click="exportSummary">导出汇总</el-button>
<el-button size="mini" icon="el-icon-document" @click="exportDetailCsv" :disabled="detailRows.length === 0">导出明细CSV</el-button>
<el-button size="mini" icon="el-icon-document" @click="exportBreakdownCsv" :disabled="breakdownRows.length === 0">导出构成CSV</el-button>
</div>
</div>
<div class="sub-title">备件消耗构成</div>
<el-table :data="breakdownRows" border stripe v-loading="breakdownLoading">
<el-table-column prop="itemCode" label="编码" width="140"></el-table-column>
<el-table-column prop="itemName" label="名称" min-width="160"></el-table-column>
<el-table-column prop="spec" label="规格" min-width="140"></el-table-column>
<el-table-column prop="unit" label="单位" width="80"></el-table-column>
<el-table-column prop="totalQty" label="全厂消耗数量" width="140" align="right">
<template slot-scope="scope">{{ formatNumber(scope.row.totalQty, 2) }}</template>
</el-table-column>
<el-table-column prop="totalAmount" label="全厂消耗金额(元)" width="160" align="right">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.totalAmount, 2) }}</template>
</el-table-column>
<el-table-column prop="allocatedQty" label="分摊数量" width="120" align="right">
<template slot-scope="scope">{{ formatNumber(scope.row.allocatedQty, 2) }}</template>
</el-table-column>
<el-table-column prop="allocatedAmount" label="分摊金额(元)" width="150" align="right">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.allocatedAmount, 2) }}</template>
</el-table-column>
</el-table>
<pagination
v-show="breakdownTotal > 0"
:total="breakdownTotal"
:page.sync="breakdownQuery.pageNum"
:limit.sync="breakdownQuery.pageSize"
@pagination="loadBreakdown"
/>
<div class="sub-title" style="margin-top: 16px;">备件成本分摊按入场卷号</div>
<el-table :data="mergedRows" border stripe v-loading="loading">
<el-table-column prop="enterCoilNo" label="入场卷号"></el-table-column>
<el-table-column prop="currentCoilNo" label="当前卷号"></el-table-column>
<el-table-column prop="spareCost" label="备件成本">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.spareCost, 2) }}</template>
</el-table-column>
<el-table-column prop="totalNetWeight" label="净重">
<template slot-scope="scope">{{ formatNumber(scope.row.totalNetWeight, 2) }}</template>
</el-table-column>
<el-table-column prop="totalGrossWeight" label="毛重">
<template slot-scope="scope">{{ formatNumber(scope.row.totalGrossWeight, 2) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openDetail(scope.row)">分摊明细</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="mergedTotal > 0"
:total="mergedTotal"
:page.sync="mergedQuery.pageNum"
:limit.sync="mergedQuery.pageSize"
@pagination="loadMerged"
/>
</div>
<el-dialog title="备件分摊明细(按日)" :visible.sync="detailVisible" width="80%" :close-on-click-modal="false">
<div class="detail-hint">入场卷号{{ detailEnterCoilNo }}</div>
<el-table :data="detailRows" stripe border max-height="420" v-loading="detailLoading">
<el-table-column prop="day" label="日期" width="120"></el-table-column>
<el-table-column prop="sparePoolAmount" label="当日备件池(元)" width="140">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.sparePoolAmount, 2) }}</template>
</el-table-column>
<el-table-column prop="totalMinutes" label="全厂分钟" width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.totalMinutes, 0) }}</template>
</el-table-column>
<el-table-column prop="coilMinutes" label="卷分钟" width="120">
<template slot-scope="scope">{{ formatNumber(scope.row.coilMinutes, 0) }}</template>
</el-table-column>
<el-table-column prop="factor" label="占比" width="100">
<template slot-scope="scope">{{ toPercent(scope.row.factor) }}</template>
</el-table-column>
<el-table-column prop="allocatedSpareCost" label="分摊备件(元)" width="140">
<template slot-scope="scope">¥ {{ formatNumber(scope.row.allocatedSpareCost, 2) }}</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { fetchCoilTotalMerged, exportCoilTotalMerged, fetchSparePartBreakdown } from '@/api/ems/energyCostReport'
export default {
name: 'SpareCostAllocation',
data() {
return {
queryParams: {
enterCoilNo: '',
currentCoilNo: '',
startDate: undefined,
endDate: undefined
},
mergedQuery: {
pageNum: 1,
pageSize: 50
},
loading: false,
mergedRows: [],
mergedTotal: 0,
spareSummary: {
totalSpareCost: 0
},
detailVisible: false,
detailLoading: false,
detailEnterCoilNo: '',
detailRows: [],
breakdownLoading: false,
breakdownRows: [],
breakdownTotal: 0,
breakdownQuery: {
pageNum: 1,
pageSize: 10
},
breakdownSummary: {
kinds: 0,
totalAllocatedQty: 0,
totalAllocatedAmount: 0
}
}
},
mounted() {
this.handleSearch()
},
methods: {
handleSearch() {
this.mergedQuery.pageNum = 1
this.breakdownQuery.pageNum = 1
this.loadMerged()
this.loadBreakdown()
},
handleReset() {
this.queryParams = {
enterCoilNo: '',
currentCoilNo: '',
startDate: undefined,
endDate: undefined
}
this.mergedQuery.pageNum = 1
this.breakdownQuery.pageNum = 1
this.mergedRows = []
this.mergedTotal = 0
this.spareSummary = { totalSpareCost: 0 }
this.detailRows = []
this.breakdownRows = []
this.breakdownTotal = 0
this.breakdownSummary = { kinds: 0, totalAllocatedQty: 0, totalAllocatedAmount: 0 }
this.handleSearch()
},
loadMerged() {
const params = {
...this.queryParams,
pageNum: this.mergedQuery.pageNum,
pageSize: this.mergedQuery.pageSize
}
this.loading = true
fetchCoilTotalMerged(params).then(res => {
this.mergedRows = res.rows || []
this.mergedTotal = res.total || 0
const rows = this.mergedRows || []
this.spareSummary = {
totalSpareCost: rows.reduce((sum, r) => sum + (Number(r.spareCost) || 0), 0)
}
}).finally(() => {
this.loading = false
})
},
loadBreakdown() {
const params = {
...this.queryParams,
pageNum: this.breakdownQuery.pageNum,
pageSize: this.breakdownQuery.pageSize
}
this.breakdownLoading = true
fetchSparePartBreakdown(params).then(res => {
this.breakdownRows = res.rows || []
this.breakdownTotal = res.total || 0
const rows = this.breakdownRows || []
this.breakdownSummary = {
kinds: this.breakdownTotal || rows.length || 0,
totalAllocatedQty: rows.reduce((sum, r) => sum + (Number(r.allocatedQty) || 0), 0),
totalAllocatedAmount: rows.reduce((sum, r) => sum + (Number(r.allocatedAmount) || 0), 0)
}
}).finally(() => {
this.breakdownLoading = false
})
},
exportBreakdownCsv() {
const headers = ['编码', '名称', '规格', '单位', '全厂消耗数量', '全厂消耗金额(元)', '分摊数量', '分摊金额(元)']
const lines = [headers.join(',')]
;(this.breakdownRows || []).forEach(r => {
const line = [
this.csvCell(r.itemCode),
this.csvCell(r.itemName),
this.csvCell(r.spec),
this.csvCell(r.unit),
this.csvCell(r.totalQty),
this.csvCell(r.totalAmount),
this.csvCell(r.allocatedQty),
this.csvCell(r.allocatedAmount)
].join(',')
lines.push(line)
})
const csv = lines.join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `spare_allocation_breakdown_${this.queryParams.enterCoilNo || 'all'}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
openDetail(row) {
if (!row || !row.enterCoilNo) return
this.detailEnterCoilNo = row.enterCoilNo
this.detailVisible = true
this.buildDetailByDay(row.enterCoilNo)
},
buildDetailByDay(enterCoilNo) {
// 见 auxAllocation.vue 的说明:后端 merged 目前不返回按日池/分钟细节。
// 这里先展示“可解释”的单行结果,后续若补充明细接口,可替换为真实按日明细。
this.detailLoading = true
const params = {
...this.queryParams,
enterCoilNo,
pageNum: 1,
pageSize: 1
}
fetchCoilTotalMerged(params).then(res => {
const row = (res.rows || [])[0]
const total = Number(row?.spareCost) || 0
this.detailRows = [
{
day: this.queryParams.startDate && this.queryParams.endDate ? `${this.queryParams.startDate} ~ ${this.queryParams.endDate}` : '-',
sparePoolAmount: null,
totalMinutes: null,
coilMinutes: null,
factor: null,
allocatedSpareCost: total
}
]
}).finally(() => {
this.detailLoading = false
})
},
exportSummary() {
const params = { ...this.queryParams }
exportCoilTotalMerged(params).then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'spare_cost_allocation_summary.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
},
exportDetailCsv() {
const headers = ['日期', '当日备件池(元)', '全厂分钟', '卷分钟', '占比', '分摊备件(元)']
const lines = [headers.join(',')]
;(this.detailRows || []).forEach(r => {
const line = [
this.csvCell(r.day),
this.csvCell(r.sparePoolAmount),
this.csvCell(r.totalMinutes),
this.csvCell(r.coilMinutes),
this.csvCell(r.factor),
this.csvCell(r.allocatedSpareCost)
].join(',')
lines.push(line)
})
const csv = lines.join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `spare_cost_allocation_detail_${this.detailEnterCoilNo || ''}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
csvCell(val) {
if (val === null || val === undefined) return ''
const s = String(val)
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"'
}
return s
},
formatNumber(val, digits = 2) {
if (val === null || val === undefined) return '-'
const num = Number(val)
if (Number.isNaN(num)) return String(val)
return num.toFixed(digits)
},
toPercent(val) {
if (val === null || val === undefined) return '-'
const num = Number(val)
if (Number.isNaN(num)) return String(val)
return (num * 100).toFixed(2) + '%'
}
}
}
</script>
<style scoped lang="scss">
.material-allocation-page {
padding: 16px 20px;
.search-card {
margin-bottom: 16px;
:deep(.el-card__body) {
padding: 16px 20px;
}
}
.stats-row {
margin-bottom: 24px;
.stat-card {
background: #fff;
border-radius: 4px;
padding: 16px;
height: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
border: 1px solid #ebeef5;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.value {
font-size: 22px;
font-weight: 600;
color: #303133;
line-height: 1.2;
margin: 8px 0 4px;
}
.desc {
font-size: 12px;
color: #909399;
margin-top: 6px;
}
}
}
.section {
background: #fff;
border-radius: 4px;
padding: 16px 20px;
margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
border: 1px solid #ebeef5;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.actions {
display: flex;
gap: 8px;
}
}
.sub-title {
font-size: 14px;
font-weight: 500;
color: #606266;
margin: 16px 0 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
.el-table {
margin-top: 8px;
th {
background-color: #f5f7fa;
color: #303133;
font-weight: 600;
}
}
.pagination-container {
margin-top: 16px;
padding: 0;
}
.detail-hint {
margin-bottom: 12px;
color: #606266;
font-size: 14px;
}
}
</style>