该提交重构了磨辊报表的操作人员统计模块,将原有的总统计拆分为中间辊(CR)、支撑辊(BR)、工作辊(WR)的分项统计,并增加了合计栏,同时完善了辊型识别逻辑,新增了对应的数据计算和表格展示。
506 lines
17 KiB
Vue
506 lines
17 KiB
Vue
<template>
|
|
<div class="app-container roll-report" v-loading="loading">
|
|
|
|
<!-- 筛选栏 -->
|
|
<div class="filter-panel mb16">
|
|
<el-form :inline="true" size="small">
|
|
<el-form-item label="磨削时间">
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
value-format="yyyy-MM-dd"
|
|
:default-time="['00:00:00', '23:59:59']"
|
|
style="width:260px"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
|
<el-button icon="el-icon-refresh" @click="resetFilter">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<!-- 统计汇总 -->
|
|
<el-row :gutter="16" class="mb16">
|
|
<el-col :span="6">
|
|
<div class="summary-box">
|
|
<div class="summary-label">磨削总次数</div>
|
|
<div class="summary-value">{{ summary.grindCount }}</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="summary-box">
|
|
<div class="summary-label">磨削总量(mm)</div>
|
|
<div class="summary-value accent">{{ summary.totalGrindAmount }}</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="summary-box">
|
|
<div class="summary-label">涉及轧辊数</div>
|
|
<div class="summary-value">{{ summary.rollCount }}</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="summary-box">
|
|
<div class="summary-label">磨辊人数</div>
|
|
<div class="summary-value">{{ summary.operatorCount }}</div>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<!-- 图表区域 -->
|
|
<el-row :gutter="16" class="mb16">
|
|
<el-col :span="14">
|
|
<div class="chart-card">
|
|
<div class="chart-header">
|
|
<span class="chart-title"><i class="el-icon-data-line" /> 每日磨削趋势</span>
|
|
</div>
|
|
<div ref="trendChartRef" class="chart-container" style="height:340px"></div>
|
|
<div v-if="!dailyTrend.length" class="chart-empty">暂无数据</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="10">
|
|
<div class="chart-card">
|
|
<div class="chart-header">
|
|
<span class="chart-title"><i class="el-icon-pie-chart" /> 辊型磨削分布</span>
|
|
</div>
|
|
<div ref="pieChartRef" class="chart-container" style="height:340px"></div>
|
|
<div v-if="!rollTypeDist.length" class="chart-empty">暂无数据</div>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<!-- 磨辊人统计表 -->
|
|
<div class="table-card">
|
|
<div class="card-header">
|
|
<span class="card-title"><i class="el-icon-user" /> 磨辊人统计</span>
|
|
<span class="card-subtitle" v-if="operatorStats.length">共 {{ operatorStats.length }} 人</span>
|
|
</div>
|
|
<el-table :data="operatorStats" size="small" border stripe style="width:100%"
|
|
v-if="operatorStats.length" :default-sort="{ prop: 'totalGrindAmount', order: 'descending' }">
|
|
<el-table-column label="序号" type="index" align="center" />
|
|
<el-table-column label="磨辊人" prop="operator" sortable />
|
|
<el-table-column label="CR(中间辊)" align="center">
|
|
<el-table-column label="磨削次数" prop="crGrindCount" align="center" sortable />
|
|
<el-table-column label="磨削总量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.crTotalGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="平均磨削量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.crAvgGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
<el-table-column label="BR(支撑辊)" align="center">
|
|
<el-table-column label="磨削次数" prop="brGrindCount" align="center" sortable />
|
|
<el-table-column label="磨削总量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.brTotalGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="平均磨削量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.brAvgGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
<el-table-column label="WR(工作辊)" align="center">
|
|
<el-table-column label="磨削次数" prop="wrGrindCount" align="center" sortable />
|
|
<el-table-column label="磨削总量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.wrTotalGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="平均磨削量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.wrAvgGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
<el-table-column label="合计" align="center">
|
|
<el-table-column label="磨削次数" prop="grindCount" align="center" sortable />
|
|
<el-table-column label="磨削总量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.totalGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="平均磨削量(mm)" align="right" sortable>
|
|
<template slot-scope="{row}">{{ row.avgGrindAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
</el-table>
|
|
<el-empty v-else description="暂无磨削记录" />
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as echarts from 'echarts'
|
|
import { getRollStats, listRollInfo } from '@/api/mes/roll/rollInfo'
|
|
import { listRollGrindAll } from '@/api/mes/roll/rollGrind'
|
|
import rollLineMixin from '../rollLineMixin'
|
|
|
|
export default {
|
|
name: 'RollReport',
|
|
mixins: [rollLineMixin],
|
|
data() {
|
|
const now = new Date()
|
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
const pad = n => String(n).padStart(2, '0')
|
|
const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
return {
|
|
loading: false,
|
|
dateRange: [fmt(sevenDaysAgo), fmt(now)],
|
|
stats: {},
|
|
summary: { grindCount: 0, totalGrindAmount: 0, rollCount: 0, operatorCount: 0 },
|
|
dailyTrend: [],
|
|
rollTypeDist: [],
|
|
operatorStats: [],
|
|
trendChart: null,
|
|
pieChart: null,
|
|
_beginTime: fmt(sevenDaysAgo),
|
|
_endTime: fmt(now)
|
|
}
|
|
},
|
|
watch: {
|
|
lineId() {
|
|
this.loadData()
|
|
}
|
|
},
|
|
mounted() {
|
|
this.$nextTick(() => {
|
|
this.initCharts()
|
|
})
|
|
window.addEventListener('resize', this.resizeCharts)
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener('resize', this.resizeCharts)
|
|
this.trendChart && this.trendChart.dispose()
|
|
this.pieChart && this.pieChart.dispose()
|
|
},
|
|
methods: {
|
|
onLineResolved() {
|
|
this.loadData()
|
|
},
|
|
async loadData() {
|
|
this.loading = true
|
|
try {
|
|
const [b, e] = this.dateRange
|
|
this._beginTime = b || ''
|
|
this._endTime = e || ''
|
|
|
|
// 1. 状态统计
|
|
if (this.lineId) {
|
|
const statsRes = await getRollStats(this.lineId)
|
|
this.stats = statsRes.data || {}
|
|
}
|
|
|
|
// 2. 获取全部轧辊(用于辊型映射)
|
|
const rollRes = await listRollInfo({ pageNum: 1, pageSize: 99999, lineId: this.lineId || undefined })
|
|
const rolls = rollRes.rows || []
|
|
const rollMap = {}
|
|
rolls.forEach(r => { rollMap[r.rollId] = r })
|
|
|
|
// 3. 一次性获取全部磨削记录(后端支持可选参数)
|
|
const params = { lineId: this.lineId || undefined }
|
|
if (this._beginTime) params.beginTime = this._beginTime
|
|
if (this._endTime) params.endTime = this._endTime + ' 23:59:59'
|
|
const grindRes = await listRollGrindAll(params)
|
|
const allRecords = grindRes.data || []
|
|
|
|
// 4. 汇总计算
|
|
this.computeSummary(allRecords, rollMap)
|
|
this.computeDailyTrend(allRecords)
|
|
this.computeRollTypeDist(allRecords, rollMap)
|
|
this.computeOperatorStats(allRecords, rollMap)
|
|
this.updateCharts()
|
|
} catch (e) {
|
|
console.error('加载报表数据失败', e)
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
computeSummary(records, rollMap) {
|
|
const rollSet = new Set()
|
|
const operatorSet = new Set()
|
|
let totalAmount = 0
|
|
records.forEach(r => {
|
|
if (r.rollId) rollSet.add(r.rollId)
|
|
if (r.operator) operatorSet.add(r.operator)
|
|
totalAmount += Number(r.grindAmount || 0)
|
|
})
|
|
this.summary = {
|
|
grindCount: records.length,
|
|
totalGrindAmount: totalAmount.toFixed(2),
|
|
rollCount: rollSet.size,
|
|
operatorCount: operatorSet.size
|
|
}
|
|
},
|
|
computeDailyTrend(records) {
|
|
const dateMap = {}
|
|
records.forEach(r => {
|
|
if (!r.grindTime) return
|
|
const d = r.grindTime.substring(0, 10)
|
|
if (!dateMap[d]) dateMap[d] = { grindDate: d, grindCount: 0, totalGrindAmount: 0 }
|
|
dateMap[d].grindCount++
|
|
dateMap[d].totalGrindAmount += Number(r.grindAmount || 0)
|
|
})
|
|
this.dailyTrend = Object.values(dateMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
|
|
},
|
|
computeRollTypeDist(records, rollMap) {
|
|
const typeMap = {}
|
|
records.forEach(r => {
|
|
const roll = rollMap[r.rollId]
|
|
const type = roll ? (roll.rollType === 'WR' ? '工作辊' : roll.rollType === 'BR' ? '支撑辊' : roll.rollType === 'CR' ? '中间辊' : roll.rollType || '未知') : '未知'
|
|
if (!typeMap[type]) typeMap[type] = { name: type, value: 0, grindCount: 0 }
|
|
typeMap[type].value += Number(r.grindAmount || 0)
|
|
typeMap[type].grindCount++
|
|
})
|
|
this.rollTypeDist = Object.values(typeMap)
|
|
},
|
|
computeOperatorStats(records, rollMap) {
|
|
const opMap = {}
|
|
records.forEach(r => {
|
|
const name = r.operator || '未知'
|
|
if (!opMap[name]) opMap[name] = {
|
|
operator: name,
|
|
grindCount: 0, totalGrindAmount: 0,
|
|
crGrindCount: 0, crTotalGrindAmount: 0,
|
|
brGrindCount: 0, brTotalGrindAmount: 0,
|
|
wrGrindCount: 0, wrTotalGrindAmount: 0
|
|
}
|
|
opMap[name].grindCount++
|
|
opMap[name].totalGrindAmount += Number(r.grindAmount || 0)
|
|
const roll = rollMap[r.rollId]
|
|
const rollType = roll ? (roll.rollType || '') : ''
|
|
const amount = Number(r.grindAmount || 0)
|
|
if (rollType === 'CR') {
|
|
opMap[name].crGrindCount++
|
|
opMap[name].crTotalGrindAmount += amount
|
|
} else if (rollType === 'BR') {
|
|
opMap[name].brGrindCount++
|
|
opMap[name].brTotalGrindAmount += amount
|
|
} else if (rollType === 'WR') {
|
|
opMap[name].wrGrindCount++
|
|
opMap[name].wrTotalGrindAmount += amount
|
|
}
|
|
})
|
|
this.operatorStats = Object.values(opMap).map(item => ({
|
|
...item,
|
|
totalGrindAmount: Number(item.totalGrindAmount.toFixed(2)),
|
|
avgGrindAmount: item.grindCount > 0 ? Number((item.totalGrindAmount / item.grindCount).toFixed(2)) : 0,
|
|
crTotalGrindAmount: Number(item.crTotalGrindAmount.toFixed(2)),
|
|
crAvgGrindAmount: item.crGrindCount > 0 ? Number((item.crTotalGrindAmount / item.crGrindCount).toFixed(2)) : 0,
|
|
brTotalGrindAmount: Number(item.brTotalGrindAmount.toFixed(2)),
|
|
brAvgGrindAmount: item.brGrindCount > 0 ? Number((item.brTotalGrindAmount / item.brGrindCount).toFixed(2)) : 0,
|
|
wrTotalGrindAmount: Number(item.wrTotalGrindAmount.toFixed(2)),
|
|
wrAvgGrindAmount: item.wrGrindCount > 0 ? Number((item.wrTotalGrindAmount / item.wrGrindCount).toFixed(2)) : 0
|
|
})).sort((a, b) => b.totalGrindAmount - a.totalGrindAmount)
|
|
},
|
|
initCharts() {
|
|
this.initTrendChart()
|
|
this.initPieChart()
|
|
},
|
|
initTrendChart() {
|
|
const dom = this.$refs.trendChartRef
|
|
if (!dom) return
|
|
if (this.trendChart) this.trendChart.dispose()
|
|
this.trendChart = echarts.init(dom)
|
|
this.trendChart.setOption({
|
|
tooltip: { trigger: 'axis' },
|
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
xAxis: { type: 'category', data: [], axisLabel: { rotate: 45, fontSize: 11 } },
|
|
yAxis: [
|
|
{ type: 'value', name: '磨削次数', minInterval: 1 },
|
|
{ type: 'value', name: '磨削量(mm)', position: 'right' }
|
|
],
|
|
series: [
|
|
{ name: '磨削次数', type: 'bar', data: [], barMaxWidth: 24 },
|
|
{ name: '磨削量(mm)', type: 'line', yAxisIndex: 1, smooth: true, data: [] }
|
|
],
|
|
color: ['#409eff', '#0a7c42']
|
|
})
|
|
},
|
|
initPieChart() {
|
|
const dom = this.$refs.pieChartRef
|
|
if (!dom) return
|
|
if (this.pieChart) this.pieChart.dispose()
|
|
this.pieChart = echarts.init(dom)
|
|
this.pieChart.setOption({
|
|
tooltip: { trigger: 'item', formatter: '{b}: {c}mm ({d}%)' },
|
|
series: [{
|
|
type: 'pie', radius: ['30%', '60%'], center: ['50%', '50%'],
|
|
label: { formatter: '{b}\n{d}%' },
|
|
data: [],
|
|
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } }
|
|
}],
|
|
color: ['#409eff', '#e6a23c', '#67c23a', '#909399']
|
|
})
|
|
},
|
|
updateCharts() {
|
|
this.updateTrendChart()
|
|
this.updatePieChart()
|
|
},
|
|
updateTrendChart() {
|
|
if (!this.trendChart) this.initTrendChart()
|
|
const dates = this.dailyTrend.map(d => d.grindDate)
|
|
const counts = this.dailyTrend.map(d => d.grindCount)
|
|
const amounts = this.dailyTrend.map(d => Number(d.totalGrindAmount.toFixed(2)))
|
|
this.trendChart.setOption({
|
|
xAxis: { data: dates },
|
|
series: [
|
|
{ data: counts },
|
|
{ data: amounts }
|
|
]
|
|
})
|
|
this.trendChart.resize()
|
|
},
|
|
updatePieChart() {
|
|
if (!this.pieChart) this.initPieChart()
|
|
const data = this.rollTypeDist.map(d => ({ name: d.name, value: Number(d.value.toFixed(2)) }))
|
|
this.pieChart.setOption({ series: [{ data }] })
|
|
this.pieChart.resize()
|
|
},
|
|
resizeCharts() {
|
|
this.trendChart && this.trendChart.resize()
|
|
this.pieChart && this.pieChart.resize()
|
|
},
|
|
handleQuery() {
|
|
this.loadData()
|
|
},
|
|
resetFilter() {
|
|
const now = new Date()
|
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
const pad = n => String(n).padStart(2, '0')
|
|
const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
this.dateRange = [fmt(sevenDaysAgo), fmt(now)]
|
|
this.loadData()
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.roll-report {
|
|
background: #f4f5f7;
|
|
min-height: 100%;
|
|
}
|
|
.mb16 { margin-bottom: 16px; }
|
|
|
|
/* 筛选栏 */
|
|
.filter-panel {
|
|
background: #fff;
|
|
border: 1px solid #dcdee0;
|
|
border-radius: 4px;
|
|
padding: 12px 20px;
|
|
}
|
|
|
|
/* 统计看板 */
|
|
.stat-panel {
|
|
display: flex;
|
|
background: #fff;
|
|
border: 1px solid #dcdee0;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.stat-item {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 18px 20px;
|
|
gap: 14px;
|
|
position: relative;
|
|
}
|
|
.stat-divider {
|
|
position: absolute;
|
|
right: 0; top: 16px; bottom: 16px;
|
|
width: 1px;
|
|
background: #e4e6ea;
|
|
}
|
|
.stat-icon-wrap {
|
|
width: 42px; height: 42px;
|
|
border-radius: 4px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
.total-icon { background: #edf0f3; color: #3d4b5c; }
|
|
.online-icon { background: #e8f5ef; color: #0a7c42; }
|
|
.standby-icon { background: #fdf3e3; color: #8b5c00; }
|
|
.offline-icon { background: #f0f1f2; color: #5f6368; }
|
|
.scrapped-icon { background: #fdecea; color: #a61c00; }
|
|
.stat-body { display: flex; flex-direction: column; }
|
|
.stat-num {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
color: #1f2329;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
.online-num { color: #0a7c42; }
|
|
.standby-num { color: #8b5c00; }
|
|
.offline-num { color: #5f6368; }
|
|
.scrapped-num { color: #a61c00; }
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #8f9099;
|
|
margin-top: 4px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
/* 汇总框 */
|
|
.summary-box {
|
|
background: #fff;
|
|
border: 1px solid #dcdee0;
|
|
border-radius: 4px;
|
|
padding: 18px 24px;
|
|
text-align: center;
|
|
}
|
|
.summary-label {
|
|
font-size: 12px;
|
|
color: #8f9099;
|
|
margin-bottom: 6px;
|
|
}
|
|
.summary-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: #1f2329;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.summary-value.accent { color: #409eff; }
|
|
|
|
/* 图表卡片 */
|
|
.chart-card {
|
|
background: #fff;
|
|
border: 1px solid #dcdee0;
|
|
border-radius: 4px;
|
|
padding: 16px 20px;
|
|
position: relative;
|
|
}
|
|
.chart-header, .card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.chart-title, .card-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #3d4b5c;
|
|
}
|
|
.card-subtitle {
|
|
font-size: 12px;
|
|
color: #9aa0a6;
|
|
margin-left: auto;
|
|
}
|
|
.chart-container { width: 100%; }
|
|
.chart-empty {
|
|
position: absolute;
|
|
top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #c0c4cc;
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* 表格卡片 */
|
|
.table-card {
|
|
background: #fff;
|
|
border: 1px solid #dcdee0;
|
|
border-radius: 4px;
|
|
padding: 16px 20px;
|
|
}
|
|
</style>
|