feat(report): 新增设备参数异常报表、SPC统计过程控制报表和切换追溯报表功能

- 实现设备参数异常报表,支持日报、班报、月报和自定义时间范围查询
- 添加SPC统计过程控制报表,提供参数趋势图表和关键质量指标分析
- 开发切换追溯报表功能,追踪模具、物料、产品的切换对参数的影响
- 集成参数选项获取和各类报表数据查询API接口
- 设计并实现物料选择器组件用于物料相关报表筛选
- 优化报表界面布局和交互体验,提升数据分析效率
master
zangch@mesnac.com 4 days ago
parent 28423e07f0
commit 9ca9d1439e

@ -0,0 +1,37 @@
import request from '@/utils/request'
// 查询参数选项
export function getParamOptions(query) {
return request({
url: '/report/deviceParamAnalysis/paramOptions',
method: 'get',
params: query
})
}
// 查询参数异常报表
export function listAnomalyReport(query) {
return request({
url: '/report/deviceParamAnalysis/anomaly/list',
method: 'get',
params: query
})
}
// 查询参数趋势/SPC报表
export function getSpcReport(query) {
return request({
url: '/report/deviceParamAnalysis/spc',
method: 'get',
params: query
})
}
// 查询模具/物料/产品切换追溯报表
export function listSwitchTraceReport(query) {
return request({
url: '/report/deviceParamAnalysis/switch/list',
method: 'get',
params: query
})
}

@ -0,0 +1,162 @@
<template>
<el-dialog
title="选择物料"
:visible.sync="dialogVisible"
width="960px"
append-to-body
@open="handleOpen"
>
<el-form :model="queryParams" size="small" :inline="true" label-width="68px">
<el-form-item label="物料编码">
<el-input
v-model="queryParams.materialCode"
placeholder="请输入物料编码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="物料名称">
<el-input
v-model="queryParams.materialName"
placeholder="请输入物料名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-table
ref="materialTable"
v-loading="loading"
:data="displayList"
height="420"
border
highlight-current-row
row-key="objId"
@current-change="handleCurrentChange"
@row-dblclick="handleRowDblclick"
>
<el-table-column label="物料编码" prop="materialCode" min-width="150" />
<el-table-column label="物料名称" prop="materialName" min-width="320" show-overflow-tooltip />
<el-table-column label="MATKL" prop="materialMatkl" min-width="120" />
<el-table-column label="计量单位" prop="materialUnit" min-width="100" />
<el-table-column label="所属工厂" prop="plantName" min-width="140" />
<el-table-column label="启用标识" prop="isFlag" min-width="90">
<template slot-scope="scope">
<span>{{ scope.row.isFlag === 0 ? '启用' : '停用' }}</span>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<span style="float: left; color: #909399;"> {{ displayList.length }} </span>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { listAllMaterialInfo } from '@/api/base/materialInfo'
export default {
name: 'MaterialSelector',
props: {
visible: {
type: Boolean,
default: false
},
selectedMaterialCode: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
loaded: false,
queryParams: {
materialCode: '',
materialName: ''
},
materialList: [],
displayList: [],
currentRow: null
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(value) {
this.$emit('update:visible', value)
}
}
},
methods: {
handleOpen() {
this.resetQuery()
if (!this.loaded) {
this.loadMaterialList()
return
}
this.syncCurrentSelection()
},
loadMaterialList() {
this.loading = true
listAllMaterialInfo()
.then(response => {
this.materialList = response.data || []
this.loaded = true
this.syncCurrentSelection()
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
const codeKeyword = (this.queryParams.materialCode || '').trim().toLowerCase()
const nameKeyword = (this.queryParams.materialName || '').trim().toLowerCase()
this.displayList = this.materialList.filter(item => {
const materialCode = (item.materialCode || '').toLowerCase()
const materialName = (item.materialName || '').toLowerCase()
return materialCode.includes(codeKeyword) && materialName.includes(nameKeyword)
})
this.syncCurrentSelection()
},
resetQuery() {
this.queryParams.materialCode = ''
this.queryParams.materialName = ''
this.handleQuery()
},
handleCurrentChange(row) {
this.currentRow = row
},
handleRowDblclick(row) {
this.currentRow = row
this.handleConfirm()
},
handleConfirm() {
if (!this.currentRow) {
this.$modal.msgWarning('请选择物料')
return
}
this.$emit('confirm', this.currentRow)
this.dialogVisible = false
},
syncCurrentSelection() {
this.currentRow = this.displayList.find(item => item.materialCode === this.selectedMaterialCode) || null
this.$nextTick(() => {
if (this.$refs.materialTable) {
this.$refs.materialTable.setCurrentRow(this.currentRow || null)
}
})
}
}
}
</script>

@ -0,0 +1,344 @@
<template>
<div class="report-pane">
<el-form :model="queryParams" size="small" :inline="true" label-width="88px" class="search-form">
<el-form-item label="报表类型">
<el-radio-group v-model="periodType">
<el-radio-button label="day">日报</el-radio-button>
<el-radio-button label="shift">班报</el-radio-button>
<el-radio-button label="month">月报</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="periodType === 'day'" label="统计日期">
<el-date-picker
v-model="reportDay"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择日期"
style="width: 160px;"
/>
</el-form-item>
<el-form-item v-if="periodType === 'month'" label="统计月份">
<el-date-picker
v-model="reportMonth"
type="month"
value-format="yyyy-MM"
placeholder="请选择月份"
style="width: 160px;"
/>
</el-form-item>
<template v-if="periodType === 'shift'">
<el-form-item label="班次日期">
<el-date-picker
v-model="shiftDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择日期"
style="width: 160px;"
/>
</el-form-item>
<el-form-item label="班次时段">
<el-time-picker
v-model="shiftRange"
is-range
value-format="HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 260px;"
/>
</el-form-item>
</template>
<el-form-item v-if="periodType === 'custom'" label="时间范围">
<el-date-picker
v-model="customRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px;"
/>
</el-form-item>
<el-form-item label="设备">
<el-select v-model="queryParams.deviceCode" clearable filterable placeholder="全部设备" style="width: 180px;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item label="参数">
<el-select v-model="queryParams.paramCode" clearable filterable placeholder="全部参数" style="width: 220px;">
<el-option
v-for="item in paramOptions"
:key="item.paramCode"
:label="item.paramName"
:value="item.paramCode"
/>
</el-select>
</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="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableList" border stripe>
<el-table-column label="设备" min-width="160">
<template slot-scope="scope">
<div>{{ scope.row.deviceName || '-' }}</div>
<div class="sub-text">{{ scope.row.deviceCode }}</div>
</template>
</el-table-column>
<el-table-column label="参数" min-width="200">
<template slot-scope="scope">
<div>{{ scope.row.paramName }}</div>
<div class="sub-text">{{ scope.row.paramCode }}</div>
</template>
</el-table-column>
<el-table-column label="预警级别" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="getAlertTagType(scope.row.alertLevel)">{{ scope.row.alertLevelName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="阈值范围" width="180" align="center">
<template slot-scope="scope">
{{ formatLimit(scope.row.lowerLimit) }} ~ {{ formatLimit(scope.row.upperLimit) }}
</template>
</el-table-column>
<el-table-column label="超限次数" prop="overLimitCount" width="100" align="center" />
<el-table-column label="超上限" prop="upperAnomalyCount" width="90" align="center" />
<el-table-column label="超下限" prop="lowerAnomalyCount" width="90" align="center" />
<el-table-column label="持续时长(分钟)" prop="abnormalDurationMinutes" width="130" align="center" />
<el-table-column label="最高值" prop="maxValue" width="110" align="center" />
<el-table-column label="最低值" prop="minValue" width="110" align="center" />
<el-table-column label="首次异常时间" width="170">
<template slot-scope="scope">
{{ parseTime(scope.row.firstAbnormalTime) }}
</template>
</el-table-column>
<el-table-column label="最近异常时间" width="170">
<template slot-scope="scope">
{{ parseTime(scope.row.lastAbnormalTime) }}
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { getParamOptions, listAnomalyReport } from '@/api/report/deviceParamAnalysis'
export default {
name: 'AnomalyReport',
props: {
deviceList: {
type: Array,
default: () => []
},
defaultDeviceCode: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
total: 0,
tableList: [],
paramOptions: [],
periodType: 'day',
reportDay: '',
reportMonth: '',
shiftDate: '',
shiftRange: ['08:00:00', '20:00:00'],
customRange: [],
queryParams: {
pageNum: 1,
pageSize: 20,
deviceCode: '',
paramCode: '',
scene: 'anomaly',
startTime: '',
endTime: ''
}
}
},
watch: {
'queryParams.deviceCode'() {
this.queryParams.paramCode = ''
this.loadParamOptions()
}
},
created() {
this.initDefaultDate()
this.queryParams.deviceCode = this.defaultDeviceCode || ''
this.loadParamOptions()
},
methods: {
initDefaultDate() {
const now = new Date()
this.reportDay = this.formatDate(now)
this.reportMonth = `${now.getFullYear()}-${`${now.getMonth() + 1}`.padStart(2, '0')}`
this.shiftDate = this.reportDay
this.customRange = [
`${this.reportDay} 00:00:00`,
`${this.reportDay} 23:59:59`
]
},
loadParamOptions() {
getParamOptions({
scene: 'anomaly',
deviceCode: this.queryParams.deviceCode
}).then(response => {
this.paramOptions = response.data || []
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
getList() {
const range = this.buildRange()
if (!range) {
return
}
this.queryParams.startTime = range.startTime
this.queryParams.endTime = range.endTime
this.loading = true
listAnomalyReport(this.queryParams).then(response => {
this.tableList = response.rows || []
this.total = response.total || 0
this.loading = false
}).catch(() => {
this.loading = false
})
},
resetQuery() {
this.periodType = 'day'
this.initDefaultDate()
this.queryParams = {
pageNum: 1,
pageSize: 20,
deviceCode: this.defaultDeviceCode || '',
paramCode: '',
scene: 'anomaly',
startTime: '',
endTime: ''
}
this.tableList = []
this.total = 0
this.loadParamOptions()
},
buildRange() {
if (this.periodType === 'day') {
if (!this.reportDay) {
this.$message.warning('请选择统计日期')
return null
}
return {
startTime: `${this.reportDay} 00:00:00`,
endTime: `${this.reportDay} 23:59:59`
}
}
if (this.periodType === 'month') {
if (!this.reportMonth) {
this.$message.warning('请选择统计月份')
return null
}
const [year, month] = this.reportMonth.split('-').map(item => Number(item))
const lastDay = new Date(year, month, 0).getDate()
return {
startTime: `${this.reportMonth}-01 00:00:00`,
endTime: `${this.reportMonth}-${`${lastDay}`.padStart(2, '0')} 23:59:59`
}
}
if (this.periodType === 'shift') {
if (!this.shiftDate || !this.shiftRange || this.shiftRange.length !== 2) {
this.$message.warning('请选择班次日期和班次时段')
return null
}
//
const startTime = `${this.shiftDate} ${this.shiftRange[0]}`
let endDate = this.shiftDate
if (this.shiftRange[1] <= this.shiftRange[0]) {
endDate = this.formatDate(this.addDays(new Date(`${this.shiftDate} 00:00:00`), 1))
}
return {
startTime,
endTime: `${endDate} ${this.shiftRange[1]}`
}
}
if (!this.customRange || this.customRange.length !== 2) {
this.$message.warning('请选择自定义时间范围')
return null
}
return {
startTime: this.customRange[0],
endTime: this.customRange[1]
}
},
addDays(date, days) {
const copy = new Date(date.getTime())
copy.setDate(copy.getDate() + days)
return copy
},
formatDate(date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
},
formatLimit(value) {
return value === null || value === undefined || value === '' ? '-' : value
},
getAlertTagType(level) {
if (level === '3') {
return 'danger'
}
if (level === '2') {
return 'warning'
}
return 'info'
}
}
}
</script>
<style scoped>
.report-pane {
padding-top: 8px;
}
.search-form {
padding: 16px 16px 0;
margin-bottom: 12px;
background: #fafafa;
border-radius: 4px;
}
.sub-text {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
</style>

@ -0,0 +1,435 @@
<template>
<div class="report-pane">
<el-form :model="queryParams" size="small" :inline="true" label-width="72px" class="search-form">
<el-form-item label="设备">
<el-select v-model="queryParams.deviceCode" filterable clearable placeholder="请选择设备" style="width: 180px;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item label="参数">
<el-select v-model="queryParams.paramCode" filterable clearable placeholder="请选择数值参数" style="width: 220px;">
<el-option
v-for="item in paramOptions"
:key="item.paramCode"
:label="item.paramName"
:value="item.paramCode"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px;"
/>
</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="resetQuery"></el-button>
</el-form-item>
</el-form>
<div v-loading="loading">
<el-row :gutter="12" class="summary-row">
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">样本数</div>
<div class="summary-value">{{ spcData.sampleSize || 0 }}</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">均值</div>
<div class="summary-value">{{ formatNumber(spcData.mean) }}</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">极差</div>
<div class="summary-value">{{ formatNumber(spcData.rangeValue) }}</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">标准差</div>
<div class="summary-value">{{ formatNumber(spcData.stdDev) }}</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">CPK</div>
<div class="summary-value" :class="getCpkClass(spcData.cpk)">{{ formatNumber(spcData.cpk) }}</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="summary-card">
<div class="summary-label">失控点数</div>
<div class="summary-value danger-text">{{ spcData.outOfControlCount || 0 }}</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" class="chart-card">
<div slot="header" class="chart-card-header">
<span>{{ spcData.deviceName || '-' }} / {{ spcData.paramName || '参数趋势' }}</span>
<span class="chart-meta">
规格限{{ formatNumber(spcData.lowerLimit) }} ~ {{ formatNumber(spcData.upperLimit) }}
</span>
</div>
<div ref="spcChart" class="spc-chart" />
</el-card>
<el-table :data="pointList" border stripe height="320">
<el-table-column label="采集时间" width="180">
<template slot-scope="scope">
{{ parseTime(scope.row.collectTime) }}
</template>
</el-table-column>
<el-table-column label="参数值" prop="paramValue" min-width="120" align="center" />
<el-table-column label="判定" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="getPointTagType(scope.row.paramValue)">{{ getPointStatus(scope.row.paramValue) }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getParamOptions, getSpcReport } from '@/api/report/deviceParamAnalysis'
export default {
name: 'SpcReport',
props: {
deviceList: {
type: Array,
default: () => []
},
defaultDeviceCode: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
dateRange: [],
paramOptions: [],
spcData: {},
chart: null,
queryParams: {
deviceCode: '',
paramCode: '',
scene: 'spc',
startTime: '',
endTime: ''
}
}
},
computed: {
pointList() {
return this.spcData.points || []
}
},
watch: {
'queryParams.deviceCode'(value) {
this.queryParams.paramCode = ''
if (value) {
this.loadParamOptions()
} else {
this.paramOptions = []
}
}
},
created() {
this.initDefaultRange()
this.queryParams.deviceCode = this.defaultDeviceCode || ''
if (this.queryParams.deviceCode) {
this.loadParamOptions()
}
},
mounted() {
window.addEventListener('resize', this.handleTabActivated)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleTabActivated)
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
initDefaultRange() {
const end = new Date()
const start = new Date(end.getTime() - 2 * 60 * 60 * 1000)
this.dateRange = [this.formatDateTime(start), this.formatDateTime(end)]
},
loadParamOptions() {
getParamOptions({
scene: 'spc',
deviceCode: this.queryParams.deviceCode
}).then(response => {
this.paramOptions = response.data || []
})
},
handleQuery() {
if (!this.queryParams.deviceCode) {
this.$message.warning('请选择设备')
return
}
if (!this.queryParams.paramCode) {
this.$message.warning('请选择参数')
return
}
if (!this.dateRange || this.dateRange.length !== 2) {
this.$message.warning('请选择时间范围')
return
}
this.queryParams.startTime = this.dateRange[0]
this.queryParams.endTime = this.dateRange[1]
this.loading = true
getSpcReport(this.queryParams).then(response => {
this.spcData = response.data || {}
this.loading = false
this.$nextTick(() => {
this.renderChart()
})
}).catch(() => {
this.loading = false
})
},
resetQuery() {
this.queryParams = {
deviceCode: this.defaultDeviceCode || '',
paramCode: '',
scene: 'spc',
startTime: '',
endTime: ''
}
this.spcData = {}
this.paramOptions = []
this.initDefaultRange()
if (this.queryParams.deviceCode) {
this.loadParamOptions()
}
this.$nextTick(() => {
this.renderChart()
})
},
renderChart() {
if (!this.$refs.spcChart) {
return
}
if (!this.chart) {
this.chart = echarts.init(this.$refs.spcChart)
}
const points = this.pointList
const xAxisData = points.map(item => this.parseTime(item.collectTime, '{m}-{d} {h}:{i}:{s}'))
const valueData = points.map(item => item.paramValue)
const uclData = points.map(() => this.spcData.ucl)
const clData = points.map(() => this.spcData.cl)
const lclData = points.map(() => this.spcData.lcl)
this.chart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
top: 10,
data: ['参数值', 'UCL', 'CL', 'LCL']
},
grid: {
left: 50,
right: 30,
top: 50,
bottom: 40
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData
},
yAxis: {
type: 'value',
scale: true
},
series: [
{
name: '参数值',
type: 'line',
smooth: false,
symbol: 'circle',
symbolSize: 6,
data: valueData,
lineStyle: {
color: '#409eff',
width: 2
}
},
{
name: 'UCL',
type: 'line',
symbol: 'none',
data: uclData,
lineStyle: {
type: 'dashed',
color: '#f56c6c'
}
},
{
name: 'CL',
type: 'line',
symbol: 'none',
data: clData,
lineStyle: {
type: 'dashed',
color: '#67c23a'
}
},
{
name: 'LCL',
type: 'line',
symbol: 'none',
data: lclData,
lineStyle: {
type: 'dashed',
color: '#e6a23c'
}
}
]
}, true)
this.chart.resize()
},
handleTabActivated() {
if (this.chart) {
this.chart.resize()
}
},
formatDateTime(date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
const second = `${date.getSeconds()}`.padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
},
formatNumber(value) {
if (value === null || value === undefined || value === '') {
return '-'
}
return value
},
getCpkClass(value) {
if (value === null || value === undefined) {
return ''
}
if (value >= 1.33) {
return 'good-text'
}
if (value >= 1) {
return 'warning-text'
}
return 'danger-text'
},
getPointStatus(value) {
if (value === null || value === undefined) {
return '无数据'
}
if (this.spcData.ucl !== null && this.spcData.ucl !== undefined && value > this.spcData.ucl) {
return '超UCL'
}
if (this.spcData.lcl !== null && this.spcData.lcl !== undefined && value < this.spcData.lcl) {
return '低于LCL'
}
return '受控'
},
getPointTagType(value) {
const status = this.getPointStatus(value)
if (status === '受控') {
return 'success'
}
if (status === '无数据') {
return 'info'
}
return 'danger'
}
}
}
</script>
<style scoped>
.report-pane {
padding-top: 8px;
}
.search-form {
padding: 16px 16px 0;
margin-bottom: 12px;
background: #fafafa;
border-radius: 4px;
}
.summary-row {
margin-bottom: 12px;
}
.summary-card {
min-height: 88px;
}
.summary-label {
color: #909399;
font-size: 13px;
}
.summary-value {
margin-top: 12px;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.chart-card {
margin-bottom: 12px;
}
.chart-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-meta {
color: #909399;
font-size: 13px;
}
.spc-chart {
width: 100%;
height: 360px;
}
.good-text {
color: #67c23a;
}
.warning-text {
color: #e6a23c;
}
.danger-text {
color: #f56c6c;
}
</style>

@ -0,0 +1,227 @@
<template>
<div class="report-pane">
<el-form :model="queryParams" size="small" :inline="true" label-width="84px" class="search-form">
<el-form-item label="设备">
<el-select v-model="queryParams.deviceCode" clearable filterable placeholder="全部设备" style="width: 180px;">
<el-option
v-for="item in deviceList"
:key="item.deviceCode"
:label="item.deviceName || item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item label="切换类型">
<el-select v-model="queryParams.switchType" placeholder="请选择切换类型" style="width: 160px;">
<el-option label="全部" value="ALL" />
<el-option label="模具" value="MOLD" />
<el-option label="物料" value="MATERIAL" />
<el-option label="产品" value="PRODUCT" />
</el-select>
</el-form-item>
<el-form-item label="观察窗口">
<el-input-number v-model="queryParams.observeMinutes" :min="1" :max="1440" controls-position="right" />
<span class="form-suffix">分钟</span>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px;"
/>
</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="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableList" border stripe :row-class-name="tableRowClassName">
<el-table-column label="设备" min-width="160">
<template slot-scope="scope">
<div>{{ scope.row.deviceName || '-' }}</div>
<div class="sub-text">{{ scope.row.deviceCode }}</div>
</template>
</el-table-column>
<el-table-column label="切换类型" prop="switchType" width="100" align="center" />
<el-table-column label="参数" min-width="180">
<template slot-scope="scope">
<div>{{ scope.row.paramName }}</div>
<div class="sub-text">{{ scope.row.paramCode }}</div>
</template>
</el-table-column>
<el-table-column label="切换时间" width="170">
<template slot-scope="scope">
{{ parseTime(scope.row.changeTime) }}
</template>
</el-table-column>
<el-table-column label="切换前" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.beforeValue || "-" }}
</template>
</el-table-column>
<el-table-column label="切换后" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.afterValue || "-" }}
</template>
</el-table-column>
<el-table-column label="产量增量" prop="outputIncrement" width="110" align="center" />
<el-table-column label="异常次数" prop="abnormalCount" width="100" align="center" />
<el-table-column label="观察状态" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.latestStatus)">{{ scope.row.latestStatus || "未知" }}</el-tag>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { listSwitchTraceReport } from '@/api/report/deviceParamAnalysis'
export default {
name: 'SwitchTraceReport',
props: {
deviceList: {
type: Array,
default: () => []
},
defaultDeviceCode: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
total: 0,
tableList: [],
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 20,
deviceCode: '',
switchType: 'ALL',
observeMinutes: 30,
scene: 'switch',
startTime: '',
endTime: ''
}
}
},
created() {
this.initDefaultRange()
this.queryParams.deviceCode = this.defaultDeviceCode || ''
},
methods: {
initDefaultRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
this.dateRange = [this.formatDateTime(start), this.formatDateTime(end)]
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
getList() {
if (!this.dateRange || this.dateRange.length !== 2) {
this.$message.warning('请选择时间范围')
return
}
this.queryParams.startTime = this.dateRange[0]
this.queryParams.endTime = this.dateRange[1]
this.loading = true
listSwitchTraceReport(this.queryParams).then(response => {
this.tableList = response.rows || []
this.total = response.total || 0
this.loading = false
}).catch(() => {
this.loading = false
})
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 20,
deviceCode: this.defaultDeviceCode || '',
switchType: 'ALL',
observeMinutes: 30,
scene: 'switch',
startTime: '',
endTime: ''
}
this.tableList = []
this.total = 0
this.initDefaultRange()
},
formatDateTime(date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
const second = `${date.getSeconds()}`.padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
},
getStatusTagType(status) {
if (status === '报警') {
return 'danger'
}
if (status === '停机') {
return 'warning'
}
if (status === '运行') {
return 'success'
}
return 'info'
},
tableRowClassName({ row }) {
if ((row.abnormalCount || 0) > 0 || row.latestStatus === '报警') {
return 'danger-row'
}
return ''
}
}
}
</script>
<style scoped>
.report-pane {
padding-top: 8px;
}
.search-form {
padding: 16px 16px 0;
margin-bottom: 12px;
background: #fafafa;
border-radius: 4px;
}
.form-suffix {
margin-left: 8px;
color: #606266;
}
.sub-text {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
/deep/ .danger-row {
background: #fff7f7;
}
</style>
Loading…
Cancel
Save