feat: 新增钢卷成本信息展示与能耗辅料成本报表页面
1. 在钢卷详情页新增成本信息模块,展示产线类型、投入产出量、报表信息和总成本 2. 新增成本数据服务类,支持按日期和产线获取完整成本明细与计算数据 3. 新增能耗和辅料两类成本报表页面,支持按产线筛选查看报表 4. 优化岗位管理页面,替换vis.js为ECharts实现岗位树图,新增职责弹窗查看功能 5. 优化综合成本页面,隐藏部分反填按钮和操作入口
This commit is contained in:
@@ -18,11 +18,11 @@
|
||||
<div slot="header" class="entry-header">
|
||||
<span class="entry-title">{{ activeReport.reportTitle }}</span>
|
||||
<el-tag size="mini" style="margin-left:6px">{{ lineName(activeReport) }}</el-tag>
|
||||
<span class="entry-meta">{{ parseTime(activeReport.reportDate,'{y}-{m}-{d}') }} 投入{{ activeReport.inputWeight }}t 产出{{ activeReport.outputWeight }}t</span>
|
||||
<span class="entry-meta">{{ parseTime(activeReport.reportDate,'{y}-{m}-{d}') }}</span>
|
||||
<el-button type="primary" size="mini" style="float:right;margin-left:8px" @click="saveGrid" :loading="saving">保存</el-button>
|
||||
<el-button size="mini" style="float:right;margin-left:8px" @click="openColCfg">列配置</el-button>
|
||||
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-money" @click="openPriceMgr">价格管理</el-button>
|
||||
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-upload2" @click="backfillCost" :loading="backfilling">反填</el-button>
|
||||
<!-- <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-upload2" @click="backfillCost" :loading="backfilling">反填</el-button> -->
|
||||
<span style="float:right;margin-right:12px;font-size:12px;color:#606266;display:flex;align-items:center">
|
||||
<span style="margin-right:4px">{{ inputMode ? '录入' : '查看' }}</span>
|
||||
<el-switch v-model="inputMode" size="small" />
|
||||
@@ -40,10 +40,10 @@
|
||||
</template>
|
||||
<template slot-scope="s">
|
||||
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll">
|
||||
<span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions">
|
||||
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions">
|
||||
<i title="反填" v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row)" />
|
||||
<i title="自动获取" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row)" />
|
||||
</span>
|
||||
</span> -->
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -52,8 +52,18 @@
|
||||
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
|
||||
</template>
|
||||
<template slot-scope="s">
|
||||
<div class="shift-cell"><span class="shift-tag">甲</span><el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll"><span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span></el-input></div>
|
||||
<div class="shift-cell"><span class="shift-tag">乙</span><el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll"><span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span></el-input></div>
|
||||
<div class="shift-cell">
|
||||
<span class="shift-tag">甲</span>
|
||||
<el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll">
|
||||
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="shift-cell">
|
||||
<span class="shift-tag">乙</span>
|
||||
<el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll">
|
||||
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else-if="col.$type==='metric' && !col.isShift" :key="'m'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="85" align="center">
|
||||
|
||||
344
klp-ui/src/views/cost/costDataService.js
Normal file
344
klp-ui/src/views/cost/costDataService.js
Normal file
@@ -0,0 +1,344 @@
|
||||
import { listProdReport } from '@/api/cost/prodReport'
|
||||
import { listProdDetail } from '@/api/cost/prodDetail'
|
||||
import { listProdMetric } from '@/api/cost/prodMetric'
|
||||
import { listItem } from '@/api/cost/item'
|
||||
import { listPrice } from '@/api/cost/price'
|
||||
|
||||
/**
|
||||
* 安全计算公式值
|
||||
* @param {string} f - 公式表达式
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function evalFormula(f) {
|
||||
const s = f.replace(/[^0-9+\-*/.()\s]/g, '')
|
||||
if (!s) return null
|
||||
try {
|
||||
const r = new Function('return (' + s + ')')()
|
||||
return isFinite(r) ? Math.round(r * 10000) / 10000 : null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将明细数据按日期分组,并计算指标公式,生成完整行数据
|
||||
*
|
||||
* @param {Object} report - 生产月报
|
||||
* @param {Array} details - 生产成本明细列表
|
||||
* @param {Array} metrics - 生产指标列表
|
||||
* @param {Array} items - 成本项目配置列表
|
||||
* @returns {Array<Object>} data - 完整行数据数组
|
||||
*/
|
||||
function buildComputedRows(report, details, metrics, items) {
|
||||
const itemCodeMap = {} // itemId -> itemCode
|
||||
items.forEach(item => {
|
||||
itemCodeMap[item.itemId] = item.itemCode
|
||||
})
|
||||
|
||||
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
// 1. 按 detailDate 分组明细
|
||||
const dateMap = {}
|
||||
details.forEach(d => {
|
||||
if (!d.detailDate) return
|
||||
if (!dateMap[d.detailDate]) {
|
||||
dateMap[d.detailDate] = { detailDate: d.detailDate }
|
||||
}
|
||||
const code = itemCodeMap[d.itemId]
|
||||
const sfx = d.shift && d.shift !== '0' ? '_' + d.shift : ''
|
||||
if (code) {
|
||||
dateMap[d.detailDate]['q_' + code + sfx] = parseFloat(d.quantity) || 0
|
||||
}
|
||||
})
|
||||
|
||||
const rows = Object.values(dateMap).sort((a, b) => a.detailDate.localeCompare(b.detailDate))
|
||||
|
||||
// 2. 补齐每个 itemCode 的总量(跨班次汇总)
|
||||
items.forEach(item => {
|
||||
const code = item.itemCode
|
||||
if (!code) return
|
||||
rows.forEach(row => {
|
||||
const v1 = row['q_' + code + '_1'] || 0
|
||||
const v2 = row['q_' + code + '_2'] || 0
|
||||
if (v1 || v2) {
|
||||
row['q_' + code] = v1 + v2
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 3. 逐行计算指标公式
|
||||
const rp = report || {}
|
||||
rows.forEach(row => {
|
||||
metrics.forEach((m, mi) => {
|
||||
if (!m.metricFormula) return
|
||||
|
||||
let f = m.metricFormula
|
||||
|
||||
// 替换其他指标的引用(按定义顺序,前面的指标可被后面的引用)
|
||||
for (let pmi = 0; pmi < mi; pmi++) {
|
||||
const pm = metrics[pmi]
|
||||
if (!pm.metricName) continue
|
||||
const pn = esc(pm.metricName)
|
||||
const pv = row['mv_' + pm.metricName]
|
||||
if (pv != null) {
|
||||
f = f.replace(new RegExp('@\\{' + pn + '\\}', 'g'), pv)
|
||||
}
|
||||
}
|
||||
|
||||
// 替换明细项引用 @{itemCode}
|
||||
items.forEach(item => {
|
||||
if (!item.itemCode) return
|
||||
const code = esc(item.itemCode)
|
||||
const totalVal = parseFloat(row['q_' + item.itemCode]) || 0
|
||||
const val1 = parseFloat(row['q_' + item.itemCode + '_1']) || 0
|
||||
const val2 = parseFloat(row['q_' + item.itemCode + '_2']) || 0
|
||||
|
||||
f = f.replace(new RegExp('@\\{' + code + '\\}\\.甲班', 'g'), val1)
|
||||
f = f.replace(new RegExp('@\\{' + code + '\\}\\.乙班', 'g'), val2)
|
||||
f = f.replace(new RegExp('@\\{' + code + '\\}', 'g'), totalVal)
|
||||
})
|
||||
|
||||
// 替换系统变量
|
||||
f = f.replace(/input_weight/g, rp.inputWeight || 0)
|
||||
.replace(/output_weight/g, rp.outputWeight || 0)
|
||||
.replace(/price/g, (m.usePrice === 1) ? (m.metricValue || 0) : 0)
|
||||
|
||||
const result = evalFormula(f)
|
||||
if (result != null) {
|
||||
row['mv_' + m.metricName] = result
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期和产线类型的全部成本数据
|
||||
*
|
||||
* @param {string} date - 日期,格式 yyyy-MM-dd,如 "2024-01-15"
|
||||
* @param {string} lineType - 产线类型,"1"=镀锌,"2"=酸轧
|
||||
* @returns {Promise<{
|
||||
* report: Object|null, // 生产月报
|
||||
* metrics: Array, // 生产指标明细列表(公式列)
|
||||
* details: Array, // 生产成本明细列表(原始数据)
|
||||
* items: Array, // 成本项目配置列表(原始数据列定义)
|
||||
* data: Array, // 完整行数据:按日期分组,包含原始值(q_xxx)和计算值(mv_xxx)
|
||||
* prices: Array, // 成本单价历史列表
|
||||
* detailMap: Object, // 按 itemId 分组的明细
|
||||
* itemMap: Object, // 按 itemId 索引的项目配置
|
||||
* metricMap: Object // 按 metricId 索引的指标
|
||||
* }>}
|
||||
*/
|
||||
export async function getCostDataByDate(date, lineType) {
|
||||
// 1. 查找指定日期和产线类型的生产月报
|
||||
// 假设每个日期只有一条月报,所以取第一个, 需要将日期转为第一天的日期
|
||||
const firstDay = new Date(date)
|
||||
firstDay.setDate(1)
|
||||
console.log(firstDay)
|
||||
// 使用北京时间,转换为东八区时间
|
||||
const year = firstDay.getFullYear()
|
||||
const month = (firstDay.getMonth() + 1).toString().padStart(2, '0')
|
||||
const firstDayStr = year + '-' + month + '-01 00:00:00'
|
||||
const reportRes = await listProdReport({
|
||||
reportDate: firstDayStr,
|
||||
lineType: lineType,
|
||||
pageNum: 1,
|
||||
pageSize: 1
|
||||
})
|
||||
console.log(reportRes)
|
||||
const reports = reportRes.rows || []
|
||||
const report = reports.length > 0 ? reports[0] : null
|
||||
|
||||
// 2. 获取所有成本项目配置(用于映射 itemId)
|
||||
const itemRes = await listItem({ pageNum: 1, pageSize: 9999 })
|
||||
const items = itemRes.rows || []
|
||||
const itemMap = {}
|
||||
items.forEach(item => {
|
||||
itemMap[item.itemId] = item
|
||||
})
|
||||
|
||||
// 3. 获取该日期下生效的单价历史
|
||||
const priceRes = await listPrice({
|
||||
effectiveDate: date,
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
})
|
||||
const prices = priceRes.rows || []
|
||||
|
||||
// 如果没有找到月报,返回基础数据
|
||||
if (!report) {
|
||||
return {
|
||||
report: null,
|
||||
metrics: [],
|
||||
details: [],
|
||||
items,
|
||||
data: [],
|
||||
prices,
|
||||
detailMap: {},
|
||||
itemMap,
|
||||
metricMap: {}
|
||||
}
|
||||
}
|
||||
|
||||
const reportId = report.reportId
|
||||
|
||||
// 4. 获取该月报的所有生产成本明细
|
||||
const detailRes = await listProdDetail({
|
||||
reportId,
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
})
|
||||
const details = detailRes.rows || []
|
||||
|
||||
// 5. 构建按 itemId 分组的明细
|
||||
const detailMap = {}
|
||||
details.forEach(detail => {
|
||||
const key = detail.itemId
|
||||
if (!detailMap[key]) {
|
||||
detailMap[key] = []
|
||||
}
|
||||
detailMap[key].push(detail)
|
||||
})
|
||||
|
||||
// 6. 获取该月报的所有生产指标
|
||||
const metricRes = await listProdMetric({
|
||||
reportId,
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
})
|
||||
const metrics = metricRes.rows || []
|
||||
const metricMap = {}
|
||||
metrics.forEach(metric => {
|
||||
metricMap[metric.metricId] = metric
|
||||
})
|
||||
|
||||
// 7. 构建完整行数据(原始值 + 公式计算值)
|
||||
const data = buildComputedRows(report, details, metrics, items)
|
||||
|
||||
return {
|
||||
report,
|
||||
metrics,
|
||||
details,
|
||||
items,
|
||||
data,
|
||||
prices,
|
||||
detailMap,
|
||||
itemMap,
|
||||
metricMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期范围、产线类型的成本数据,支持多天批量查询
|
||||
*
|
||||
* @param {string} startDate - 开始日期 yyyy-MM-dd
|
||||
* @param {string} endDate - 结束日期 yyyy-MM-dd
|
||||
* @param {string} lineType - 产线类型
|
||||
* @returns {Promise<Array<{ date: string, report, metrics, details, items, data, prices }>>}
|
||||
*/
|
||||
export async function getCostDataByDateRange(startDate, endDate, lineType) {
|
||||
// 查询日期范围内的所有月报
|
||||
const reportRes = await listProdReport({
|
||||
lineType,
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
})
|
||||
const allReports = (reportRes.rows || []).filter(r => {
|
||||
return r.reportDate >= startDate && r.reportDate <= endDate
|
||||
})
|
||||
|
||||
// 获取所有项目配置和单价(共享数据,只查一次)
|
||||
const itemRes = await listItem({ pageNum: 1, pageSize: 9999 })
|
||||
const items = itemRes.rows || []
|
||||
const itemMap = {}
|
||||
items.forEach(item => {
|
||||
itemMap[item.itemId] = item
|
||||
})
|
||||
|
||||
const priceRes = await listPrice({
|
||||
pageNum: 1,
|
||||
pageSize: 9999
|
||||
})
|
||||
const prices = priceRes.rows || []
|
||||
|
||||
// 逐日报表获取完整数据
|
||||
const results = []
|
||||
for (const report of allReports) {
|
||||
const reportId = report.reportId
|
||||
|
||||
const [detailRes, metricRes] = await Promise.all([
|
||||
listProdDetail({ reportId, pageNum: 1, pageSize: 9999 }),
|
||||
listProdMetric({ reportId, pageNum: 1, pageSize: 9999 })
|
||||
])
|
||||
|
||||
const details = detailRes.rows || []
|
||||
const metrics = metricRes.rows || []
|
||||
|
||||
const detailMap = {}
|
||||
details.forEach(detail => {
|
||||
const key = detail.itemId
|
||||
if (!detailMap[key]) detailMap[key] = []
|
||||
detailMap[key].push(detail)
|
||||
})
|
||||
|
||||
const metricMap = {}
|
||||
metrics.forEach(metric => {
|
||||
metricMap[metric.metricId] = metric
|
||||
})
|
||||
|
||||
const data = buildComputedRows(report, details, metrics, items)
|
||||
|
||||
results.push({
|
||||
date: report.reportDate,
|
||||
report,
|
||||
metrics,
|
||||
details,
|
||||
items,
|
||||
data,
|
||||
prices,
|
||||
detailMap,
|
||||
itemMap,
|
||||
metricMap
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 itemId 获取指定日期下该成本项目的明细数据
|
||||
*
|
||||
* @param {string} date - 日期 yyyy-MM-dd
|
||||
* @param {string} lineType - 产线类型
|
||||
* @param {string|number} itemId - 成本项目ID
|
||||
* @returns {Promise<{ quantity: number|null, unitPrice: number|null, amount: number|null }>}
|
||||
*/
|
||||
export async function getCostItemData(date, lineType, itemId) {
|
||||
const { details, items } = await getCostDataByDate(date, lineType)
|
||||
const itemDetails = details.filter(d => String(d.itemId) === String(itemId))
|
||||
const item = items.find(i => String(i.itemId) === String(itemId))
|
||||
|
||||
let totalQuantity = 0
|
||||
itemDetails.forEach(d => {
|
||||
totalQuantity += parseFloat(d.quantity) || 0
|
||||
})
|
||||
|
||||
return {
|
||||
itemId,
|
||||
itemName: item ? item.itemName : '',
|
||||
itemCode: item ? item.itemCode : '',
|
||||
category: item ? item.category : '',
|
||||
unit: item ? item.unit : '',
|
||||
quantity: totalQuantity || null,
|
||||
unitPrice: itemDetails.length > 0 ? itemDetails[0].unitPrice : null,
|
||||
amount: itemDetails.length > 0 ? itemDetails[0].amount : null,
|
||||
details: itemDetails
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getCostDataByDate,
|
||||
getCostDataByDateRange,
|
||||
getCostItemData
|
||||
}
|
||||
162
klp-ui/src/views/cost/views/assised.vue
Normal file
162
klp-ui/src/views/cost/views/assised.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 产线 tabs -->
|
||||
<div class="line-tab-bar" v-if="productionLines.length">
|
||||
<div class="line-tabs">
|
||||
<span v-for="line in productionLines" :key="line.lineId"
|
||||
:class="['line-tab', { active: currentLineId === line.lineId }]"
|
||||
@click="switchLine(line.lineId)">{{ line.lineName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-tab-bar">
|
||||
<div class="report-tabs">
|
||||
<span v-for="r in tabs" :key="r.reportId"
|
||||
:class="['report-tab', { active: activeReport && activeReport.reportId === r.reportId }]"
|
||||
@click="enter(r)">{{ r.reportTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!activeReport" class="empty-hint">请在上方选择报表</div>
|
||||
|
||||
<template v-if="activeReport">
|
||||
<el-card class="mb8">
|
||||
<div slot="header" class="entry-header">
|
||||
<span class="entry-title">{{ activeReport.reportTitle }}</span>
|
||||
<el-tag size="mini" style="margin-left:6px">{{ lineName(activeReport) }}</el-tag>
|
||||
<span class="entry-meta">{{ parseTime(activeReport.reportDate,'{y}-{m}-{d}') }}</span>
|
||||
</div>
|
||||
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="gridRows" border stripe size="mini" style="width:100%">
|
||||
<el-table-column label="日期" width="135" fixed>
|
||||
<template slot-scope="s">{{ parseTime(s.row.detailDate, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="col in displayCols" :key="'d'+col.itemId" align="center">
|
||||
<template slot="header">
|
||||
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
|
||||
</template>
|
||||
<template slot-scope="s">{{ formatNum(s.row['q'+col.itemId]) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { listProdReport, getProdReport } from "@/api/cost/prodReport"
|
||||
import { listProdDetail } from "@/api/cost/prodDetail"
|
||||
import { listItem } from "@/api/cost/item"
|
||||
|
||||
export default {
|
||||
name: "CostComprehensive",
|
||||
data() {
|
||||
return {
|
||||
tabs: [],
|
||||
activeReport: null, gridLoading: false, gridRows: [],
|
||||
allItems: [], allCols: [],
|
||||
currentLineId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['productionLines']),
|
||||
displayCols() {
|
||||
return this.allCols.filter(c => c.$type === 'detail')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('productionLine/getProductionLines').then(() => {
|
||||
if (this.productionLines.length > 0) {
|
||||
this.currentLineId = this.productionLines[0].lineId
|
||||
this.getTabList()
|
||||
this.loadItems()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
switchLine(lineId) {
|
||||
this.currentLineId = lineId
|
||||
this.activeReport = null
|
||||
this.gridRows = []
|
||||
this.allCols = []
|
||||
this.getTabList()
|
||||
},
|
||||
getTabList() {
|
||||
const params = { pageNum: 1, pageSize: 9999 }
|
||||
if (this.currentLineId) params.lineType = this.currentLineId
|
||||
listProdReport(params).then(r => { this.tabs = r.rows || [] })
|
||||
},
|
||||
|
||||
/* grid */
|
||||
async loadGrid() {
|
||||
const rid = this.activeReport.reportId; this.gridLoading = true
|
||||
try {
|
||||
const dr = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
|
||||
await this.restoreAllCols()
|
||||
this.buildGrid(dr.rows || [])
|
||||
} finally { this.gridLoading = false }
|
||||
},
|
||||
async restoreAllCols() {
|
||||
await this.loadItems()
|
||||
const cfg = JSON.parse(this.activeReport.colConfig || 'null')
|
||||
if (!cfg || !cfg.columns || !cfg.columns.length) { this.allCols = []; return }
|
||||
|
||||
const cols = []
|
||||
for (const c of cfg.columns) {
|
||||
if (c.t === 'd') {
|
||||
const id = String(c.id)
|
||||
const item = this.allItems.find(i => String(i.itemId) === id && i.category === '辅料')
|
||||
if (item) cols.push({ $type: 'detail', itemId: item.itemId, itemCode: item.itemCode, itemName: item.itemName, unit: item.unit })
|
||||
}
|
||||
}
|
||||
this.allCols = cols
|
||||
},
|
||||
buildGrid(details) {
|
||||
const map = {}; details.forEach(d => {
|
||||
if (!d.detailDate) return
|
||||
if (!map[d.detailDate]) map[d.detailDate] = { detailDate: d.detailDate }
|
||||
const key = 'q' + d.itemId
|
||||
const val = d.quantity ? Number(d.quantity) : 0
|
||||
map[d.detailDate][key] = (map[d.detailDate][key] || 0) + val
|
||||
})
|
||||
this.gridRows = Object.values(map).sort((a,b) => a.detailDate.localeCompare(b.detailDate))
|
||||
},
|
||||
|
||||
/* helpers */
|
||||
lineName(row) {
|
||||
if (row.lineType) {
|
||||
const found = this.productionLines.find(l => l.lineId == row.lineType);
|
||||
if (found) return found.lineName
|
||||
else return row.lineType || '-'
|
||||
}
|
||||
},
|
||||
formatNum(val) { if (val === null || val === undefined || val === '') return ''; const n = Number(val); if (isNaN(n)) return val; return parseFloat(n.toFixed(4)) },
|
||||
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
|
||||
async enter(row) { const r = await getProdReport(row.reportId); if (r.data) this.activeReport = r.data; else this.activeReport = row; this.loadGrid() }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
/deep/ .el-table td { padding: 6px 0; }
|
||||
/deep/ .el-table th { padding: 8px 0; font-size: 13px; }
|
||||
/deep/ .el-table .cell { padding: 0 6px; line-height: 1.5; font-size: 13px; }
|
||||
/deep/ .el-button--mini { padding: 4px 8px; font-size: 12px; }
|
||||
/deep/ .el-button--mini.el-button--text { padding: 0; }
|
||||
.line-tab-bar { display: flex; align-items: center; border-bottom: 1px solid #dcdfe6; margin-bottom: 10px; padding: 0 4px; }
|
||||
.line-tabs { flex: 1; display: flex; overflow-x: auto; margin-bottom: -1px; }
|
||||
.line-tab { display: inline-block; padding: 8px 20px; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; font-size: 14px; color: #606266; transition: all 0.2s; user-select: none; }
|
||||
.line-tab:hover { color: #1890ff; }
|
||||
.line-tab.active { color: #1890ff; border-bottom-color: #1890ff; font-weight: 500; }
|
||||
.report-tab-bar { display: flex; align-items: center; border-bottom: 1px solid #dcdfe6; margin-bottom: 14px; padding: 0 4px; }
|
||||
.report-tabs { flex: 1; display: flex; overflow-x: auto; margin-bottom: -1px; }
|
||||
.report-tab { display: inline-block; padding: 9px 20px; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; font-size: 14px; color: #606266; transition: all 0.2s; user-select: none; }
|
||||
.report-tab:hover { color: #409eff; }
|
||||
.report-tab.active { color: #409eff; border-bottom-color: #409eff; font-weight: 500; }
|
||||
.empty-hint { text-align: center; padding: 80px 0; color: #999; font-size: 15px; }
|
||||
.entry-header { line-height: 32px; }
|
||||
.entry-title { font-weight: bold; font-size: 16px; }
|
||||
.entry-meta { margin-left: 10px; color: #999; font-size: 13px; }
|
||||
.col-hd { font-size: 13px; line-height: 1.4; }
|
||||
</style>
|
||||
162
klp-ui/src/views/cost/views/enegry.vue
Normal file
162
klp-ui/src/views/cost/views/enegry.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 产线 tabs -->
|
||||
<div class="line-tab-bar" v-if="productionLines.length">
|
||||
<div class="line-tabs">
|
||||
<span v-for="line in productionLines" :key="line.lineId"
|
||||
:class="['line-tab', { active: currentLineId === line.lineId }]"
|
||||
@click="switchLine(line.lineId)">{{ line.lineName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-tab-bar">
|
||||
<div class="report-tabs">
|
||||
<span v-for="r in tabs" :key="r.reportId"
|
||||
:class="['report-tab', { active: activeReport && activeReport.reportId === r.reportId }]"
|
||||
@click="enter(r)">{{ r.reportTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!activeReport" class="empty-hint">请在上方选择报表</div>
|
||||
|
||||
<template v-if="activeReport">
|
||||
<el-card class="mb8">
|
||||
<div slot="header" class="entry-header">
|
||||
<span class="entry-title">{{ activeReport.reportTitle }}</span>
|
||||
<el-tag size="mini" style="margin-left:6px">{{ lineName(activeReport) }}</el-tag>
|
||||
<span class="entry-meta">{{ parseTime(activeReport.reportDate,'{y}-{m}-{d}') }}</span>
|
||||
</div>
|
||||
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="gridRows" border stripe size="mini" style="width:100%">
|
||||
<el-table-column label="日期" width="135" fixed>
|
||||
<template slot-scope="s">{{ parseTime(s.row.detailDate, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="col in displayCols" :key="'d'+col.itemId" align="center">
|
||||
<template slot="header">
|
||||
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
|
||||
</template>
|
||||
<template slot-scope="s">{{ formatNum(s.row['q'+col.itemId]) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { listProdReport, getProdReport } from "@/api/cost/prodReport"
|
||||
import { listProdDetail } from "@/api/cost/prodDetail"
|
||||
import { listItem } from "@/api/cost/item"
|
||||
|
||||
export default {
|
||||
name: "CostComprehensive",
|
||||
data() {
|
||||
return {
|
||||
tabs: [],
|
||||
activeReport: null, gridLoading: false, gridRows: [],
|
||||
allItems: [], allCols: [],
|
||||
currentLineId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['productionLines']),
|
||||
displayCols() {
|
||||
return this.allCols.filter(c => c.$type === 'detail')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('productionLine/getProductionLines').then(() => {
|
||||
if (this.productionLines.length > 0) {
|
||||
this.currentLineId = this.productionLines[0].lineId
|
||||
this.getTabList()
|
||||
this.loadItems()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
switchLine(lineId) {
|
||||
this.currentLineId = lineId
|
||||
this.activeReport = null
|
||||
this.gridRows = []
|
||||
this.allCols = []
|
||||
this.getTabList()
|
||||
},
|
||||
getTabList() {
|
||||
const params = { pageNum: 1, pageSize: 9999 }
|
||||
if (this.currentLineId) params.lineType = this.currentLineId
|
||||
listProdReport(params).then(r => { this.tabs = r.rows || [] })
|
||||
},
|
||||
|
||||
/* grid */
|
||||
async loadGrid() {
|
||||
const rid = this.activeReport.reportId; this.gridLoading = true
|
||||
try {
|
||||
const dr = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
|
||||
await this.restoreAllCols()
|
||||
this.buildGrid(dr.rows || [])
|
||||
} finally { this.gridLoading = false }
|
||||
},
|
||||
async restoreAllCols() {
|
||||
await this.loadItems()
|
||||
const cfg = JSON.parse(this.activeReport.colConfig || 'null')
|
||||
if (!cfg || !cfg.columns || !cfg.columns.length) { this.allCols = []; return }
|
||||
|
||||
const cols = []
|
||||
for (const c of cfg.columns) {
|
||||
if (c.t === 'd') {
|
||||
const id = String(c.id)
|
||||
const item = this.allItems.find(i => String(i.itemId) === id && i.category === '能耗')
|
||||
if (item) cols.push({ $type: 'detail', itemId: item.itemId, itemCode: item.itemCode, itemName: item.itemName, unit: item.unit })
|
||||
}
|
||||
}
|
||||
this.allCols = cols
|
||||
},
|
||||
buildGrid(details) {
|
||||
const map = {}; details.forEach(d => {
|
||||
if (!d.detailDate) return
|
||||
if (!map[d.detailDate]) map[d.detailDate] = { detailDate: d.detailDate }
|
||||
const key = 'q' + d.itemId
|
||||
const val = d.quantity ? Number(d.quantity) : 0
|
||||
map[d.detailDate][key] = (map[d.detailDate][key] || 0) + val
|
||||
})
|
||||
this.gridRows = Object.values(map).sort((a,b) => a.detailDate.localeCompare(b.detailDate))
|
||||
},
|
||||
|
||||
/* helpers */
|
||||
lineName(row) {
|
||||
if (row.lineType) {
|
||||
const found = this.productionLines.find(l => l.lineId == row.lineType);
|
||||
if (found) return found.lineName
|
||||
else return row.lineType || '-'
|
||||
}
|
||||
},
|
||||
formatNum(val) { if (val === null || val === undefined || val === '') return ''; const n = Number(val); if (isNaN(n)) return val; return parseFloat(n.toFixed(4)) },
|
||||
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
|
||||
async enter(row) { const r = await getProdReport(row.reportId); if (r.data) this.activeReport = r.data; else this.activeReport = row; this.loadGrid() }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
/deep/ .el-table td { padding: 6px 0; }
|
||||
/deep/ .el-table th { padding: 8px 0; font-size: 13px; }
|
||||
/deep/ .el-table .cell { padding: 0 6px; line-height: 1.5; font-size: 13px; }
|
||||
/deep/ .el-button--mini { padding: 4px 8px; font-size: 12px; }
|
||||
/deep/ .el-button--mini.el-button--text { padding: 0; }
|
||||
.line-tab-bar { display: flex; align-items: center; border-bottom: 1px solid #dcdfe6; margin-bottom: 10px; padding: 0 4px; }
|
||||
.line-tabs { flex: 1; display: flex; overflow-x: auto; margin-bottom: -1px; }
|
||||
.line-tab { display: inline-block; padding: 8px 20px; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; font-size: 14px; color: #606266; transition: all 0.2s; user-select: none; }
|
||||
.line-tab:hover { color: #1890ff; }
|
||||
.line-tab.active { color: #1890ff; border-bottom-color: #1890ff; font-weight: 500; }
|
||||
.report-tab-bar { display: flex; align-items: center; border-bottom: 1px solid #dcdfe6; margin-bottom: 14px; padding: 0 4px; }
|
||||
.report-tabs { flex: 1; display: flex; overflow-x: auto; margin-bottom: -1px; }
|
||||
.report-tab { display: inline-block; padding: 9px 20px; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; font-size: 14px; color: #606266; transition: all 0.2s; user-select: none; }
|
||||
.report-tab:hover { color: #409eff; }
|
||||
.report-tab.active { color: #409eff; border-bottom-color: #409eff; font-weight: 500; }
|
||||
.empty-hint { text-align: center; padding: 80px 0; color: #999; font-size: 15px; }
|
||||
.entry-header { line-height: 32px; }
|
||||
.entry-title { font-weight: bold; font-size: 16px; }
|
||||
.entry-meta { margin-left: 10px; color: #999; font-size: 13px; }
|
||||
.col-hd { font-size: 13px; line-height: 1.4; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user