优化成本计算问题,加入辅料备件分摊页面
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
496
klp-ui/src/views/ems/cost/auxAllocation.vue
Normal file
496
klp-ui/src/views/ems/cost/auxAllocation.vue
Normal 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>
|
||||
492
klp-ui/src/views/ems/cost/spareAllocation.vue
Normal file
492
klp-ui/src/views/ems/cost/spareAllocation.vue
Normal 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>
|
||||
Reference in New Issue
Block a user