740 lines
28 KiB
Vue
740 lines
28 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" /> {{ trendChartTitle }}</span>
|
|
<el-radio-group v-model="trendDimension" size="mini" @change="onTrendDimensionChange" style="margin-left:12px">
|
|
<el-radio-button label="day">按天</el-radio-button>
|
|
<el-radio-button label="week">按周</el-radio-button>
|
|
<el-radio-button label="month">按月</el-radio-button>
|
|
</el-radio-group>
|
|
</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>
|
|
<el-button size="mini" type="primary" plain icon="el-icon-download" style="margin-left:8px" @click="exportOperatorStats" :disabled="!operatorStats.length">导出</el-button>
|
|
</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="在职状态" align="center" width="90">
|
|
<template slot-scope="{row}">
|
|
<el-tag :type="operatorStatusType(row.operator)" size="mini" disable-transitions>
|
|
{{ operatorStatusLabel(row.operator) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<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 class="table-card mt16">
|
|
<div class="card-header">
|
|
<span class="card-title"><i class="el-icon-s-data" /> 月度产线汇总</span>
|
|
<span class="card-subtitle" v-if="monthlyPivot.length">{{ monthlyPivotMonths.length }} 个月 · {{ monthlyPivot.length }} 条产线</span>
|
|
<el-button size="mini" type="primary" plain icon="el-icon-download" style="margin-left:8px" @click="exportMonthlyPivot" :disabled="!monthlyPivot.length">导出</el-button>
|
|
</div>
|
|
<div style="overflow-x:auto" v-if="monthlyPivot.length">
|
|
<el-table :data="monthlyPivot" size="small" border stripe style="min-width:100%"
|
|
:header-cell-style="{ textAlign: 'center', fontWeight: 600 }">
|
|
<el-table-column label="产线" prop="lineName" fixed="left" width="140" align="center" />
|
|
<el-table-column v-for="m in monthlyPivotMonths" :key="m" :label="m" align="center">
|
|
<el-table-column label="磨削次数" align="center" min-width="80">
|
|
<template slot-scope="{row}">{{ row[m + '_count'] || 0 }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="磨削量(mm)" align="right" min-width="110">
|
|
<template slot-scope="{row}">{{ (row[m + '_amount'] || 0).toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
<el-table-column label="合计" align="center">
|
|
<el-table-column label="磨削次数" align="center" min-width="80">
|
|
<template slot-scope="{row}">{{ row.totalCount }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="磨削量(mm)" align="right" min-width="110">
|
|
<template slot-scope="{row}">{{ row.totalAmount.toFixed(2) }}</template>
|
|
</el-table-column>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<el-empty v-else description="暂无磨削记录" />
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as echarts from 'echarts'
|
|
import * as XLSX from 'xlsx'
|
|
import { getRollStats, listRollInfo } from '@/api/mes/roll/rollInfo'
|
|
import { listRollGrindAll } from '@/api/mes/roll/rollGrind'
|
|
import { mapGetters } from 'vuex'
|
|
import rollLineMixin from '../rollLineMixin'
|
|
|
|
export default {
|
|
name: 'RollReport',
|
|
mixins: [rollLineMixin],
|
|
dicts: ['mes_roll_operator'],
|
|
computed: {
|
|
...mapGetters(['productionLines']),
|
|
trendChartTitle() {
|
|
return { day: '每日磨削趋势', week: '每周磨削趋势', month: '每月磨削趋势' }[this.trendDimension] || '磨削趋势'
|
|
}
|
|
},
|
|
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),
|
|
monthlyPivot: [],
|
|
monthlyPivotMonths: [],
|
|
trendDimension: 'day'
|
|
}
|
|
},
|
|
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 })
|
|
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.autoDetectTrendDimension()
|
|
this.computeSummary(allRecords, rollMap)
|
|
this.computeTrendData(allRecords)
|
|
this.computeRollTypeDist(allRecords, rollMap)
|
|
this.computeOperatorStats(allRecords, rollMap)
|
|
|
|
// 5. 月度产线汇总(跨全部产线,不受当前 lineId 限制)
|
|
await this.$store.dispatch('productionLine/getProductionLines')
|
|
const allParams = {}
|
|
if (this._beginTime) allParams.beginTime = this._beginTime
|
|
if (this._endTime) allParams.endTime = this._endTime + ' 23:59:59'
|
|
const allGrindRes = await listRollGrindAll(allParams)
|
|
this.computeMonthlyPivot(allGrindRes.data || [], 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))
|
|
},
|
|
computeWeeklyTrend(records) {
|
|
const weekMap = {}
|
|
const pad = n => String(n).padStart(2, '0')
|
|
records.forEach(r => {
|
|
if (!r.grindTime) return
|
|
const d = new Date(r.grindTime)
|
|
const dayOfWeek = d.getDay() || 7
|
|
const monday = new Date(d)
|
|
monday.setDate(d.getDate() - dayOfWeek + 1)
|
|
const sunday = new Date(monday)
|
|
sunday.setDate(monday.getDate() + 6)
|
|
const key = `${monday.getFullYear()}-${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}`
|
|
const label = `${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}~${pad(sunday.getMonth() + 1)}-${pad(sunday.getDate())}`
|
|
if (!weekMap[key]) weekMap[key] = { grindDate: label, grindCount: 0, totalGrindAmount: 0, _sort: key }
|
|
weekMap[key].grindCount++
|
|
weekMap[key].totalGrindAmount += Number(r.grindAmount || 0)
|
|
})
|
|
this.dailyTrend = Object.values(weekMap).sort((a, b) => a._sort.localeCompare(b._sort))
|
|
},
|
|
computeMonthlyTrend(records) {
|
|
const monthMap = {}
|
|
records.forEach(r => {
|
|
if (!r.grindTime) return
|
|
const m = r.grindTime.substring(0, 7)
|
|
if (!monthMap[m]) monthMap[m] = { grindDate: m, grindCount: 0, totalGrindAmount: 0 }
|
|
monthMap[m].grindCount++
|
|
monthMap[m].totalGrindAmount += Number(r.grindAmount || 0)
|
|
})
|
|
this.dailyTrend = Object.values(monthMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
|
|
},
|
|
computeTrendData(records) {
|
|
this._cachedRecords = records
|
|
if (this.trendDimension === 'week') this.computeWeeklyTrend(records)
|
|
else if (this.trendDimension === 'month') this.computeMonthlyTrend(records)
|
|
else this.computeDailyTrend(records)
|
|
},
|
|
autoDetectTrendDimension() {
|
|
const [b, e] = this.dateRange
|
|
if (!b || !e) return
|
|
const days = (new Date(e) - new Date(b)) / (24 * 60 * 60 * 1000)
|
|
if (days <= 31) this.trendDimension = 'day'
|
|
else if (days <= 90) this.trendDimension = 'week'
|
|
else this.trendDimension = 'month'
|
|
},
|
|
onTrendDimensionChange() {
|
|
if (this._cachedRecords) {
|
|
this.computeTrendData(this._cachedRecords)
|
|
this.updateTrendChart()
|
|
}
|
|
},
|
|
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()
|
|
},
|
|
operatorStatusLabel(name) {
|
|
if (!name) return ''
|
|
const items = this.dict?.type?.mes_roll_operator || []
|
|
const found = items.find(item => item.value === name)
|
|
if (!found || !found.raw) return '未知'
|
|
return found.raw.status === '0' ? '在职' : '离职'
|
|
},
|
|
operatorStatusType(name) {
|
|
if (!name) return ''
|
|
const items = this.dict?.type?.mes_roll_operator || []
|
|
const found = items.find(item => item.value === name)
|
|
if (!found || !found.raw) return 'info'
|
|
return found.raw.status === '0' ? 'success' : 'danger'
|
|
},
|
|
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()
|
|
},
|
|
exportOperatorStats() {
|
|
const rows = [
|
|
['序号', '磨辊人', '在职状态', 'CR磨削次数', 'CR磨削总量(mm)', 'CR平均磨削量(mm)', 'BR磨削次数', 'BR磨削总量(mm)', 'BR平均磨削量(mm)', 'WR磨削次数', 'WR磨削总量(mm)', 'WR平均磨削量(mm)', '合计磨削次数', '合计磨削总量(mm)', '合计平均磨削量(mm)']
|
|
]
|
|
this.operatorStats.forEach((item, idx) => {
|
|
rows.push([
|
|
idx + 1,
|
|
item.operator,
|
|
this.operatorStatusLabel(item.operator),
|
|
item.crGrindCount, item.crTotalGrindAmount, item.crAvgGrindAmount,
|
|
item.brGrindCount, item.brTotalGrindAmount, item.brAvgGrindAmount,
|
|
item.wrGrindCount, item.wrTotalGrindAmount, item.wrAvgGrindAmount,
|
|
item.grindCount, item.totalGrindAmount, item.avgGrindAmount
|
|
])
|
|
})
|
|
const ws = XLSX.utils.aoa_to_sheet(rows)
|
|
const wb = XLSX.utils.book_new()
|
|
XLSX.utils.book_append_sheet(wb, ws, '磨辊人统计')
|
|
const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
|
|
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `磨辊人统计_${this._beginTime || ''}_${this._endTime ? this._endTime.substring(0, 10) : ''}.xlsx`
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
URL.revokeObjectURL(url)
|
|
},
|
|
computeMonthlyPivot(records, rollMap) {
|
|
const lineMonthMap = {}
|
|
const monthSet = new Set()
|
|
records.forEach(r => {
|
|
if (!r.grindTime) return
|
|
const month = r.grindTime.substring(0, 7)
|
|
const roll = rollMap[r.rollId]
|
|
const lid = Number(roll?.lineId) || 0
|
|
if (!lineMonthMap[lid]) {
|
|
const line = this.productionLines.find(l => Number(l.lineId) === lid)
|
|
lineMonthMap[lid] = { lineId: lid, lineName: line ? line.lineName : (r.lineName || '未知产线'), months: {} }
|
|
}
|
|
if (!lineMonthMap[lid].months[month]) {
|
|
lineMonthMap[lid].months[month] = { grindCount: 0, totalGrindAmount: 0 }
|
|
}
|
|
lineMonthMap[lid].months[month].grindCount++
|
|
lineMonthMap[lid].months[month].totalGrindAmount += Number(r.grindAmount || 0)
|
|
monthSet.add(month)
|
|
})
|
|
const months = [...monthSet].sort()
|
|
const rows = this.productionLines.map(line => {
|
|
const ld = lineMonthMap[line.lineId]
|
|
const row = { lineId: line.lineId, lineName: line.lineName, totalCount: 0, totalAmount: 0 }
|
|
months.forEach(m => {
|
|
if (ld && ld.months[m]) {
|
|
row[m + '_count'] = ld.months[m].grindCount
|
|
row[m + '_amount'] = Number(ld.months[m].totalGrindAmount.toFixed(2))
|
|
row.totalCount += ld.months[m].grindCount
|
|
row.totalAmount += ld.months[m].totalGrindAmount
|
|
} else {
|
|
row[m + '_count'] = 0
|
|
row[m + '_amount'] = 0
|
|
}
|
|
})
|
|
row.totalAmount = Number(row.totalAmount.toFixed(2))
|
|
return row
|
|
})
|
|
this.monthlyPivotMonths = months
|
|
this.monthlyPivot = rows
|
|
},
|
|
exportMonthlyPivot() {
|
|
const months = this.monthlyPivotMonths
|
|
const header1 = ['产线']
|
|
const header2 = ['']
|
|
months.forEach(m => { header1.push(m, ''); header2.push('磨削次数', '磨削量(mm)') })
|
|
header1.push('合计', '')
|
|
header2.push('磨削次数', '磨削量(mm)')
|
|
const rows = [header1, header2]
|
|
this.monthlyPivot.forEach(row => {
|
|
const r = [row.lineName]
|
|
months.forEach(m => { r.push(row[m + '_count'] || 0, (row[m + '_amount'] || 0).toFixed(2)) })
|
|
r.push(row.totalCount, row.totalAmount.toFixed(2))
|
|
rows.push(r)
|
|
})
|
|
const ws = XLSX.utils.aoa_to_sheet(rows)
|
|
// merge header row 1 for month groups
|
|
ws['!merges'] = []
|
|
let col = 1
|
|
months.forEach(() => { ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } }); col += 2 })
|
|
ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } })
|
|
const wb = XLSX.utils.book_new()
|
|
XLSX.utils.book_append_sheet(wb, ws, '月度产线汇总')
|
|
const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
|
|
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `月度产线汇总_${this._beginTime || ''}_${this._endTime ? this._endTime.substring(0, 10) : ''}.xlsx`
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.roll-report {
|
|
background: #f4f5f7;
|
|
min-height: 100%;
|
|
}
|
|
.mb16 { margin-bottom: 16px; }
|
|
.mt16 { margin-top: 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>
|