feat: 添加PDF和图片生成库并优化打印功能
refactor(FurCurrent): 重构参数显示逻辑,优化闪烁效果 style(FurnaceHistoryPanel): 调整表格列宽和溢出处理 feat(LabelPrint): 使用dom-to-image提升打印质量 feat(QualityCertificate): 添加生产过程曲线图表 feat: 新增ParamEcharts组件用于统一图表渲染 refactor(line): 使用ParamEcharts重构监控图表组件
This commit is contained in:
@@ -30,14 +30,17 @@
|
||||
"clipboard": "2.0.8",
|
||||
"core-js": "3.37.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"echarts": "5.4.0",
|
||||
"element-ui": "2.15.14",
|
||||
"file-saver": "2.0.5",
|
||||
"fuse.js": "6.4.3",
|
||||
"highlight.js": "9.18.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"js-beautify": "1.13.0",
|
||||
"js-cookie": "3.0.1",
|
||||
"jsencrypt": "3.0.0-rc.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"konva": "^10.0.2",
|
||||
"nprogress": "0.2.0",
|
||||
"print-js": "^1.6.0",
|
||||
|
||||
@@ -14,60 +14,54 @@
|
||||
<div class="grid-item label">Coil ID</div>
|
||||
<!-- 钢卷号 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.exitMatId" :border="false" class="editable-input"
|
||||
placeholder="Coil ID"></input>
|
||||
<!-- 钢卷号 -->
|
||||
<input v-model="editableData.exitMatId" :border="false" class="editable-input" placeholder="Coil ID">
|
||||
<!-- 钢卷号 -->
|
||||
</div>
|
||||
<div class="grid-item label">Hot Coil ID</div>
|
||||
<!-- 热卷号 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.entryMatId" :border="false" class="editable-input"
|
||||
placeholder="Hot Coil ID"></input>
|
||||
<!-- 热卷号 -->
|
||||
<input v-model="editableData.entryMatId" :border="false" class="editable-input" placeholder="Hot Coil ID">
|
||||
<!-- 热卷号 -->
|
||||
</div>
|
||||
|
||||
<div class="grid-item label">Specification</div>
|
||||
<!-- 规格 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.spec" :border="false" class="editable-input" placeholder="Specification"></input>
|
||||
<input v-model="editableData.spec" :border="false" class="editable-input" placeholder="Specification">
|
||||
<!-- 规格 -->
|
||||
</div>
|
||||
<div class="grid-item label">Material</div>
|
||||
<!-- 材质 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.steelGrade" :border="false" class="editable-input"
|
||||
placeholder="Material"></input>
|
||||
<!-- 材质 -->
|
||||
<input v-model="editableData.steelGrade" :border="false" class="editable-input" placeholder="Material">
|
||||
<!-- 材质 -->
|
||||
</div>
|
||||
|
||||
<div class="grid-item label">Net Weight</div>
|
||||
<!-- 净重 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.actualWeight" :border="false" class="editable-input"
|
||||
placeholder="Net Weight"></input>
|
||||
<!-- 净重 -->
|
||||
<input v-model="editableData.actualWeight" :border="false" class="editable-input" placeholder="Net Weight">
|
||||
<!-- 净重 -->
|
||||
</div>
|
||||
<div class="grid-item label">Production Shift</div>
|
||||
<!-- 生产班组 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.groupNo" :border="false" class="editable-input"
|
||||
placeholder="Production Shift"></input>
|
||||
<!-- 生产班组 -->
|
||||
<input v-model="editableData.groupNo" :border="false" class="editable-input" placeholder="Production Shift">
|
||||
<!-- 生产班组 -->
|
||||
</div>
|
||||
|
||||
<div class="grid-item label">Product Name</div>
|
||||
<!-- 产品名称 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.prodCode" :border="false" class="editable-input"
|
||||
placeholder="Product Name"></input>
|
||||
<!-- 产品名称 -->
|
||||
<input v-model="editableData.prodCode" :border="false" class="editable-input" placeholder="Product Name">
|
||||
<!-- 产品名称 -->
|
||||
</div>
|
||||
<div class="grid-item label">Production Date</div>
|
||||
<!-- 生产日期 -->
|
||||
<div class="grid-item value">
|
||||
<input v-model="editableData.productionDate" :border="false" class="editable-input"
|
||||
placeholder="Production Date"></input>
|
||||
<!-- 生产日期 -->
|
||||
placeholder="Production Date">
|
||||
<!-- 生产日期 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,6 +81,8 @@
|
||||
<script>
|
||||
import printJS from 'print-js';
|
||||
import dayjs from 'dayjs';
|
||||
// 导入 dom-to-image 库
|
||||
import domtoimage from 'dom-to-image';
|
||||
|
||||
export default {
|
||||
name: 'PdoLabelPrint',
|
||||
@@ -145,22 +141,28 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
// Handle print logic / 处理打印逻辑
|
||||
handlePrint() {
|
||||
// Call printJS to print / 调用printJS进行打印
|
||||
printJS({
|
||||
printable: 'printContent', // Element ID to print / 要打印的元素ID
|
||||
type: 'html',
|
||||
header: null, // Don't show default header / 不显示默认页眉
|
||||
footer: null, // Don't show default footer / 不显示默认页脚
|
||||
// style: printStyle, // Apply print styles / 应用打印样式
|
||||
scanStyles: true, // Don't scan existing page styles / 不扫描页面现有样式
|
||||
targetStyles: ['*'], // Allow all target styles / 允许所有目标样式
|
||||
documentTitle: 'Label Print', // Print document title / 打印文档标题
|
||||
onPrintDialogClose: () => {
|
||||
console.log('Print dialog closed'); // 打印对话框已关闭
|
||||
}
|
||||
});
|
||||
// 替换原dom-to-image的转换逻辑
|
||||
async handlePrint() {
|
||||
try {
|
||||
const printElement = document.getElementById('printContent');
|
||||
// 关键:设置scale提升分辨率(建议3-4)
|
||||
const imageBase64 = await domtoimage.toPng(printElement, {
|
||||
backgroundColor: '#ffffff',
|
||||
scale: 2, // 缩放倍数(越大图片越清晰,文件也越大)
|
||||
});
|
||||
|
||||
// 打印图片时强制按实际尺寸渲染
|
||||
printJS({
|
||||
printable: imageBase64,
|
||||
type: 'image',
|
||||
imageStyle: `
|
||||
width: 100mm; /* 与标签实际物理尺寸一致 */
|
||||
height: 80mm;
|
||||
object-fit: contain;
|
||||
`,
|
||||
documentTitle: 'Label Print'
|
||||
});
|
||||
} catch (e) { /* 错误处理 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,12 +180,17 @@ export default {
|
||||
|
||||
/* 标签内容样式(与打印尺寸等比例) */
|
||||
.label-content {
|
||||
width: 500px; /* 200mm × 2.5 = 500px */
|
||||
height: 400px; /* 160mm × 2.5 = 400px */
|
||||
width: 500px;
|
||||
/* 200mm × 2.5 = 500px */
|
||||
height: 400px;
|
||||
/* 160mm × 2.5 = 400px */
|
||||
border: 1px solid #ccc;
|
||||
padding: 25px; /* 10mm × 2.5 = 25px */
|
||||
padding: 25px;
|
||||
/* 10mm × 2.5 = 25px */
|
||||
box-sizing: border-box;
|
||||
font-family: SimSun;
|
||||
background: #ffffff;
|
||||
/* 确保DOM背景为白色,转换图片后样式一致 */
|
||||
}
|
||||
|
||||
.company-name {
|
||||
@@ -239,4 +246,4 @@ export default {
|
||||
text-align: left;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
454
src/views/l2/pdo/components/ParamEcharts.vue
Normal file
454
src/views/l2/pdo/components/ParamEcharts.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="param-echarts-container" v-loading="loading">
|
||||
<!-- 图表标题 -->
|
||||
<div class="chart-title">{{ chartTitle }}</div>
|
||||
<!-- ECharts 渲染容器 -->
|
||||
<div ref="chartDom" class="chart-content"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { getSegmentList } from '@/api/business/segment'
|
||||
|
||||
export default {
|
||||
name: 'ParamEcharts',
|
||||
// 接收父组件传入的核心参数
|
||||
props: {
|
||||
enCoilID: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: '线圈ID,用于请求对应数据'
|
||||
},
|
||||
paramField: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: '参数字段名(对应原 paramFields 的 value)'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartInstance: null, // ECharts 实例
|
||||
loading: false, // 数据加载状态
|
||||
chartDataObj: { // 图表数据
|
||||
timeStamps: [],
|
||||
chartData: [],
|
||||
originalTimeStamps: [],
|
||||
originalChartData: []
|
||||
},
|
||||
stat: { // 参数统计值
|
||||
latestValue: 0,
|
||||
maxValue: 0,
|
||||
minValue: 0,
|
||||
avgValue: 0
|
||||
},
|
||||
maxXAxisLabels: 40, // X轴最大显示标签数
|
||||
paramLabelMap: { // 参数字段与标签的映射(与父组件保持一致)
|
||||
stripSpeed: 'Strip Speed',
|
||||
tensionPorBr1: 'Pay-off Tension 1#',
|
||||
tensionPorBr2: 'Pay-off Tension 2#',
|
||||
cleaningVoltage: 'Cleaning Voltage',
|
||||
cleaningCurrent: 'Cleaning Current',
|
||||
alkaliConcentration: 'Alkali Concentration',
|
||||
alkaliTemperature: 'Alkali Temperature',
|
||||
phfExitStripTemp: 'PH Furnace Exit Temp',
|
||||
rtfExitStripTemp: 'Heating Section Exit Temp',
|
||||
jcsExitStripTemp: 'Cooling Section Exit Temp',
|
||||
scsExitStripTemp: 'Equilibrium Section Exit Temp',
|
||||
potTemperature: 'Pot Temperature',
|
||||
zincPotPower: 'Zinc Pot Power',
|
||||
gasConsumption: 'Gas Consumption',
|
||||
coolingTowerStripTemp: 'Cooling Tower Temp',
|
||||
tensionBr5Tm: 'TM Tension',
|
||||
stripSpeedTmExit: 'TM Exit Speed',
|
||||
tlElongation: 'TL Elongation',
|
||||
tensionTlBr7: 'TL Tension'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 图表标题(根据 paramField 自动匹配)
|
||||
chartTitle() {
|
||||
return this.paramLabelMap[this.paramField] || this.paramField
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听线圈ID变化,重新请求数据
|
||||
enCoilID: {
|
||||
handler(newVal) {
|
||||
this.fetchChartData()
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
// 监听参数字段变化,重新请求数据
|
||||
paramField: {
|
||||
handler(newVal) {
|
||||
this.fetchChartData()
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化 ECharts 实例
|
||||
this.initEchartsInstance()
|
||||
// 监听窗口 resize,自适应图表尺寸
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁 ECharts 实例,释放资源
|
||||
if (this.chartInstance && this.chartInstance.dispose) {
|
||||
this.chartInstance.dispose()
|
||||
}
|
||||
// 移除 resize 监听
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
// 初始化 ECharts 实例
|
||||
initEchartsInstance() {
|
||||
this.$nextTick(() => {
|
||||
const chartDom = this.$refs.chartDom
|
||||
if (!chartDom) {
|
||||
console.warn('ECharts 渲染容器未找到,初始化失败')
|
||||
return
|
||||
}
|
||||
// 创建 ECharts 实例
|
||||
this.chartInstance = echarts.init(chartDom)
|
||||
// 初始绘制空图表
|
||||
this.drawEmptyChart()
|
||||
})
|
||||
},
|
||||
|
||||
// 获取图表数据
|
||||
fetchChartData() {
|
||||
// 前置校验:参数无效时绘制空图表并关闭加载
|
||||
if (!this.enCoilID || !this.paramField) {
|
||||
this.drawEmptyChart()
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
// 开启加载状态
|
||||
this.loading = true
|
||||
|
||||
// 请求数据
|
||||
getSegmentList({
|
||||
enCoilID: this.enCoilID,
|
||||
paramField: this.paramField
|
||||
}).then(res => {
|
||||
if (res.data && res.data.length > 0) {
|
||||
// 处理数据
|
||||
const processedData = this.processChartData(res.data)
|
||||
this.chartDataObj = processedData
|
||||
this.stat = processedData.stat
|
||||
// 绘制折线图
|
||||
this.drawLineChart()
|
||||
} else {
|
||||
// 无数据时绘制无数据图表
|
||||
this.drawNoDataChart()
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(`获取${this.paramField}数据失败:`, err)
|
||||
// 数据请求失败时绘制错误图表
|
||||
this.drawErrorChart()
|
||||
}).finally(() => {
|
||||
// 无论成功失败,都关闭加载状态
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 处理原始数据
|
||||
processChartData(rawData) {
|
||||
// 按 segNo 排序
|
||||
rawData.sort((a, b) => a.segNo - b.segNo)
|
||||
const originalTimeStamps = rawData.map(item => item.segNo)
|
||||
const originalChartData = rawData.map(item => parseFloat(item.value))
|
||||
|
||||
let timeStamps = originalTimeStamps
|
||||
let chartData = originalChartData
|
||||
|
||||
// X轴标签过多时进行采样(保持原有逻辑)
|
||||
if (rawData.length > this.maxXAxisLabels) {
|
||||
const sampleInterval = Math.ceil(rawData.length / this.maxXAxisLabels)
|
||||
timeStamps = []
|
||||
chartData = []
|
||||
for (let i = 0; i < rawData.length; i += sampleInterval) {
|
||||
timeStamps.push(rawData[i].segNo)
|
||||
chartData.push(parseFloat(rawData[i].value))
|
||||
}
|
||||
// 确保最后一个数据点被保留
|
||||
if (timeStamps[timeStamps.length - 1] !== rawData[rawData.length - 1].segNo) {
|
||||
timeStamps.push(rawData[rawData.length - 1].segNo)
|
||||
chartData.push(parseFloat(rawData[rawData.length - 1].value))
|
||||
}
|
||||
}
|
||||
|
||||
// 计算统计值
|
||||
let stat = { latestValue: 0, maxValue: 0, minValue: 0, avgValue: 0 }
|
||||
if (originalChartData.length) {
|
||||
stat.latestValue = originalChartData[originalChartData.length - 1]
|
||||
stat.maxValue = Math.max(...originalChartData)
|
||||
stat.minValue = Math.min(...originalChartData)
|
||||
stat.avgValue = originalChartData.reduce((sum, val) => sum + val, 0) / originalChartData.length
|
||||
}
|
||||
|
||||
return {
|
||||
timeStamps,
|
||||
chartData,
|
||||
originalTimeStamps,
|
||||
originalChartData,
|
||||
stat
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制折线图(核心逻辑)
|
||||
drawLineChart() {
|
||||
if (!this.chartInstance || !this.chartDataObj.timeStamps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'line' },
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#d4d4d4',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333' },
|
||||
formatter: (params) => {
|
||||
const paramItem = params[0]
|
||||
return `
|
||||
Sequence: ${paramItem.name}<br>
|
||||
Value: ${paramItem.value.toFixed(2)}<br>
|
||||
Unit: ${this.getParamUnit()}
|
||||
`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: this.chartDataObj.timeStamps,
|
||||
axisLine: { lineStyle: { color: '#d4d4d4' } },
|
||||
axisLabel: {
|
||||
color: '#666',
|
||||
rotate: 45,
|
||||
interval: (index) => {
|
||||
return index % Math.max(1, Math.ceil(this.chartDataObj.timeStamps.length / this.maxXAxisLabels)) === 0
|
||||
}
|
||||
},
|
||||
splitLine: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: this.getParamUnit(),
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
nameTextStyle: { color: '#666' },
|
||||
axisLine: { lineStyle: { color: '#d4d4d4' } },
|
||||
axisLabel: { color: '#666' },
|
||||
splitLine: {
|
||||
lineStyle: { color: '#e8e8e8', type: 'dashed' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: this.chartTitle,
|
||||
type: 'line',
|
||||
data: this.chartDataObj.chartData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#666', width: 2 },
|
||||
itemStyle: { color: '#999' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(153, 153, 153, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(153, 153, 153, 0.05)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
markPoint: {
|
||||
data: [{ type: 'max', name: 'Max' }, { type: 'min', name: 'Min' }],
|
||||
itemStyle: { color: '#999' },
|
||||
label: { color: '#333' }
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Average' }],
|
||||
lineStyle: { color: '#999', type: 'dashed' },
|
||||
label: { color: '#333' }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.chartInstance.setOption(option, true)
|
||||
},
|
||||
|
||||
// 绘制空图表(初始无参数/无线圈ID时)
|
||||
drawEmptyChart() {
|
||||
if (!this.chartInstance) return
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'Please select performance',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#999'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
}
|
||||
|
||||
this.chartInstance.setOption(option)
|
||||
},
|
||||
|
||||
// 绘制无数据图表
|
||||
drawNoDataChart() {
|
||||
if (!this.chartInstance) return
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'No data',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#999'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
}
|
||||
|
||||
this.chartInstance.setOption(option)
|
||||
},
|
||||
|
||||
// 绘制数据加载失败图表
|
||||
drawErrorChart() {
|
||||
if (!this.chartInstance) return
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'Data loading failed',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#f56c6c'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
}
|
||||
|
||||
this.chartInstance.setOption(option)
|
||||
},
|
||||
|
||||
// 窗口 resize 时自适应图表尺寸
|
||||
handleResize() {
|
||||
if (this.chartInstance && this.chartInstance.resize) {
|
||||
this.chartInstance.resize()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取参数单位(保持原有逻辑)
|
||||
getParamUnit() {
|
||||
switch (this.paramField) {
|
||||
case 'stripSpeed':
|
||||
case 'stripSpeedTmExit':
|
||||
return 'm/s'
|
||||
case 'tensionPorBr1':
|
||||
case 'tensionPorBr2':
|
||||
case 'tensionBr5Tm':
|
||||
case 'tensionTlBr7':
|
||||
return 'N'
|
||||
case 'cleaningVoltage':
|
||||
return 'V'
|
||||
case 'cleaningCurrent':
|
||||
return 'A'
|
||||
case 'alkaliConcentration':
|
||||
case 'tlElongation':
|
||||
return '%'
|
||||
case 'alkaliTemperature':
|
||||
case 'phfExitStripTemp':
|
||||
case 'rtfExitStripTemp':
|
||||
case 'jcsExitStripTemp':
|
||||
case 'scsExitStripTemp':
|
||||
case 'potTemperature':
|
||||
case 'coolingTowerStripTemp':
|
||||
return '°C'
|
||||
case 'zincPotPower':
|
||||
return 'kW'
|
||||
case 'gasConsumption':
|
||||
return 'm³/h'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.param-echarts-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
width: 100%;
|
||||
height: calc(100% - 24px);
|
||||
box-sizing: border-box;
|
||||
min-height: 180px; /* 兜底高度,确保 ECharts 初始化有尺寸 */
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- QR code placeholder / 二维码占位 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="header-info">
|
||||
<div class="info-row">
|
||||
<div class="info-item">
|
||||
@@ -192,6 +192,23 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="certificate-charts">
|
||||
<div class="charts-title">Production Process Curves</div>
|
||||
<!-- 生产过程曲线标题 -->
|
||||
<div class="charts-wrapper">
|
||||
<!-- 带钢速度曲线:stripSpeed -->
|
||||
<div class="chart-item">
|
||||
<div class="chart-subtitle">Strip Speed (m/s)</div>
|
||||
<ParamEcharts :enCoilID="getEnCoilID" paramField="stripSpeed" />
|
||||
</div>
|
||||
<!-- 1#放卷张力曲线:tensionPorBr1 -->
|
||||
<div class="chart-item">
|
||||
<div class="chart-subtitle">Pay-off Tension 1# (N)</div>
|
||||
<ParamEcharts :enCoilID="getEnCoilID" paramField="tensionPorBr1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer information / 底部信息 -->
|
||||
<div class="certificate-footer">
|
||||
<div class="footer-row">
|
||||
@@ -252,9 +269,11 @@
|
||||
<div class="remarks-title">Remarks:</div>
|
||||
<!-- 备注 -->
|
||||
<div class="remarks-content">
|
||||
<p>1. It is certified herein that the products have been produced and tested according to above standards and the products are all qualified.</p>
|
||||
<p>1. It is certified herein that the products have been produced and tested according to above standards and
|
||||
the products are all qualified.</p>
|
||||
<!-- 本产品已按上述标准生产和检验,其结果符合要求,特此证明。 -->
|
||||
<p>2. The original quality certificate is the basis for acceptance, copies are for reference only and not to be used as acceptance vouchers.</p>
|
||||
<p>2. The original quality certificate is the basis for acceptance, copies are for reference only and not to
|
||||
be used as acceptance vouchers.</p>
|
||||
<!-- 质量证明书原件是验收依据,复印件仅供参考,不作为验收凭证。 -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,9 +292,16 @@
|
||||
<script>
|
||||
import printJS from 'print-js';
|
||||
import dayjs from 'dayjs';
|
||||
// 1. 导入 dom-to-image 库
|
||||
import domtoimage from 'dom-to-image';
|
||||
// 获取生产过程曲线数据
|
||||
import ParamEcharts from './ParamEcharts.vue'
|
||||
|
||||
export default {
|
||||
name: 'QualityCertificate',
|
||||
components: {
|
||||
ParamEcharts
|
||||
},
|
||||
props: {
|
||||
// Single coil detail / 单卷详情
|
||||
detail: {
|
||||
@@ -301,6 +327,12 @@ export default {
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getEnCoilID() {
|
||||
// 优先从detail中取有效线圈ID(与后端接口保持一致)
|
||||
return this.detail.enCoilID || this.detail.exitMatId || this.detail.coilid || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Initialize certificate data for single coil / 初始化单卷质保书数据
|
||||
initCertificateData(detail) {
|
||||
@@ -388,32 +420,46 @@ export default {
|
||||
return num.toFixed(2);
|
||||
},
|
||||
|
||||
// Handle print / 处理打印
|
||||
handlePrint() {
|
||||
const printStyle = `
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 10mm;
|
||||
// 2. 重构打印方法:DOM -> 高清图片 -> 打印
|
||||
async handlePrint() {
|
||||
try {
|
||||
// 获取打印目标DOM元素
|
||||
const printElement = document.getElementById('qualityCertificateContent');
|
||||
if (!printElement) {
|
||||
this.$message?.error('未找到质保书打印内容');
|
||||
return;
|
||||
}
|
||||
.certificate-content {
|
||||
width: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
`;
|
||||
|
||||
printJS({
|
||||
printable: 'qualityCertificateContent',
|
||||
type: 'html',
|
||||
header: null,
|
||||
footer: null,
|
||||
style: printStyle,
|
||||
scanStyles: true,
|
||||
targetStyles: ['*'],
|
||||
documentTitle: 'Quality Certificate',
|
||||
onPrintDialogClose: () => {
|
||||
console.log('Print dialog closed');
|
||||
}
|
||||
});
|
||||
// 将DOM转为高清PNG图片(base64格式),解决虚化问题
|
||||
const imageBase64 = await domtoimage.toPng(printElement, {
|
||||
backgroundColor: '#ffffff', // 强制白色背景,避免透明区域
|
||||
scale: 4, // 缩放倍数(3-4最佳,越大越清晰,文件体积略有增加)
|
||||
quality: 1.0 // 图片质量拉满
|
||||
});
|
||||
|
||||
// 调用printJS打印图片(适配A4横向布局)
|
||||
printJS({
|
||||
printable: imageBase64, // 传入base64图片
|
||||
type: 'image', // 打印类型改为image(关键变更)
|
||||
header: null,
|
||||
footer: null,
|
||||
documentTitle: 'Quality Certificate',
|
||||
// 配置图片打印样式,适配A4横向,防止变形
|
||||
imageStyle: `
|
||||
width: 100%;
|
||||
max-width: 297mm; /* A4横向宽度(297mm×210mm) */
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
`,
|
||||
onPrintDialogClose: () => {
|
||||
console.log('Print dialog closed');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 捕获转换或打印异常,提升用户体验
|
||||
console.error('质保书打印失败:', error);
|
||||
this.$message?.error('打印失败,请重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -593,50 +639,56 @@ export default {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Print styles / 打印样式 */
|
||||
/* 生产过程曲线区域样式 */
|
||||
.certificate-charts {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.charts-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.charts-wrapper {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
height: 300px; /* 固定图表区域高度,确保ECharts正常渲染 */
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
flex: 1; /* 两个图表平分宽度 */
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/* Print styles / 打印样式(仅隐藏按钮,图片打印样式已在printJS中配置) */
|
||||
@media print {
|
||||
.print-btn-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.quality-certificate-container {
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.certificate-content {
|
||||
font-size: 10px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.certificate-header {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.certificate-table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 3px 2px;
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.certificate-footer {
|
||||
page-break-before: avoid;
|
||||
}
|
||||
|
||||
.certificate-remarks {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</style>
|
||||
@@ -17,12 +17,11 @@
|
||||
'count-3': checkedCount === 3,
|
||||
'count-4': checkedCount === 4
|
||||
}" :style="{ height: '100%', width: '100%' }">
|
||||
<!-- 预生成4个固定图表容器(初始隐藏) -->
|
||||
<div v-for="(chartKey, index) in presetChartKeys" :key="chartKey" class="preset-chart-item" :style="{
|
||||
display: index < checkedCount ? 'block' : 'none', // 根据勾选数量显示对应图表
|
||||
}" v-loading="getChartLoading(index)">
|
||||
<div class="chart-title">{{ getChartTitle(index) }}</div>
|
||||
<div :ref="chartKey" class="chart-content"></div>
|
||||
<!-- 替换为封装后的 ECharts 组件,循环渲染 -->
|
||||
<div v-for="(param, index) in checkedNodes" :key="param.value" class="preset-chart-item"
|
||||
:style="{ display: index < maxCheckCount ? 'block' : 'none' }">
|
||||
<!-- 传入 enCoilID 和 paramField(即 param.value) -->
|
||||
<ParamEcharts :enCoilID="enCoilID" :paramField="param.value" />
|
||||
</div>
|
||||
|
||||
<!-- 无勾选时显示提示 -->
|
||||
@@ -36,13 +35,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { getSegmentList } from '@/api/business/segment'
|
||||
// 引入Vue(用于Vue.set实现响应式赋值,Vue2必备)
|
||||
import Vue from 'vue'
|
||||
import ParamEcharts from './ParamEcharts.vue' // 引入封装后的 ECharts 组件
|
||||
|
||||
export default {
|
||||
name: 'DeviceMonitoring',
|
||||
components: { ParamEcharts }, // 注册组件
|
||||
props: {
|
||||
enCoilID: {
|
||||
type: String,
|
||||
@@ -79,84 +76,28 @@ export default {
|
||||
},
|
||||
checkedNodes: [], // 已勾选参数列表
|
||||
checkedCount: 0, // 已勾选数量(用于布局控制)
|
||||
presetChartKeys: ['chart1', 'chart2', 'chart3', 'chart4'], // 4个固定图表key
|
||||
chartInstances: {}, // 4个固定图表实例 { chart1: echartsInstance, ... }
|
||||
chartDataMap: {}, // 参数数据映射 { paramValue: { timeStamps: [], chartData: [], ... } }
|
||||
statMap: {}, // 参数统计值映射 { paramValue: { latestValue: 0, ... } }
|
||||
loadingMap: {}, // 参数加载状态映射 { paramValue: boolean }
|
||||
maxXAxisLabels: 40,
|
||||
maxCheckCount: 4
|
||||
maxCheckCount: 4 // 最大勾选数量
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听线圈ID变化,无需手动更新图表(子组件已自行监听)
|
||||
enCoilID: {
|
||||
handler(newVal) {
|
||||
console.log('enCoilID changed:', newVal);
|
||||
if (newVal) {
|
||||
// 批量更新所有已勾选参数的图表
|
||||
this.checkedNodes.forEach((param, index) => {
|
||||
if (index < this.presetChartKeys.length) {
|
||||
this.fetchChartData(param.value, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 清空所有图表,并关闭对应loading
|
||||
this.checkedNodes.forEach((param, index) => {
|
||||
const chartKey = this.presetChartKeys[index];
|
||||
this.drawEmptyChart(chartKey);
|
||||
// 强制关闭loading
|
||||
Vue.set(this.loadingMap, param.value, false);
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
// 监听勾选数量变化,触发布局更新(可选,确保响应式)
|
||||
// 监听勾选数量变化,触发布局更新
|
||||
checkedCount: {
|
||||
handler() {
|
||||
this.$nextTick(() => {
|
||||
// 窗口resize强制刷新图表尺寸
|
||||
this.handleResize();
|
||||
// 窗口resize强制刷新(可选,子组件已监听resize,此处为兜底)
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化4个固定图表实例(预生成,隐藏状态)
|
||||
this.initPresetCharts();
|
||||
// 监听窗口resize
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁4个固定图表实例
|
||||
Object.values(this.chartInstances).forEach(chart => {
|
||||
if (chart && chart.dispose) {
|
||||
chart.dispose();
|
||||
}
|
||||
});
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
// 初始化4个固定图表
|
||||
initPresetCharts() {
|
||||
this.$nextTick(() => {
|
||||
this.presetChartKeys.forEach(chartKey => {
|
||||
// v-for中的ref返回数组,取第0项(单个DOM元素)
|
||||
const chartDomArr = this.$refs[chartKey];
|
||||
// 严格校验DOM是否存在,避免传入无效值
|
||||
const chartDom = chartDomArr && chartDomArr.length > 0 ? chartDomArr[0] : null;
|
||||
if (chartDom) {
|
||||
// 创建实例并存储
|
||||
this.chartInstances[chartKey] = echarts.init(chartDom);
|
||||
// 初始绘制空图表(隐藏状态,不显示)
|
||||
this.drawEmptyChart(chartKey);
|
||||
} else {
|
||||
console.warn(`图表${chartKey} DOM元素未找到,初始化失败`);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 处理树形节点勾选(限制最多4个)
|
||||
handleCheckChange() {
|
||||
const checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
||||
@@ -170,367 +111,9 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// 先关闭所有旧勾选参数的loading
|
||||
this.checkedNodes.forEach(param => {
|
||||
Vue.set(this.loadingMap, param.value, false);
|
||||
});
|
||||
|
||||
// 更新勾选列表和数量
|
||||
this.checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
||||
this.checkedCount = this.checkedNodes.length;
|
||||
|
||||
// 批量更新图表:仅setOption,不重新创建实例
|
||||
this.checkedNodes.forEach((param, index) => {
|
||||
if (index < this.presetChartKeys.length) {
|
||||
const chartKey = this.presetChartKeys[index];
|
||||
if (this.enCoilID) {
|
||||
this.fetchChartData(param.value, index);
|
||||
} else {
|
||||
this.drawEmptyChart(chartKey);
|
||||
// 确保关闭loading
|
||||
Vue.set(this.loadingMap, param.value, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏未用到的图表(已通过display控制,此处可选)
|
||||
for (let i = this.checkedCount; i < this.presetChartKeys.length; i++) {
|
||||
const chartKey = this.presetChartKeys[i];
|
||||
if (this.chartInstances[chartKey] && this.chartInstances[chartKey].clear) {
|
||||
this.chartInstances[chartKey].clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取对应索引图表的加载状态
|
||||
getChartLoading(index) {
|
||||
// 增加兜底校验,避免index越界导致返回undefined(v-loading需要布尔值)
|
||||
if (index >= this.checkedNodes.length || !this.checkedNodes[index]?.value) {
|
||||
return false;
|
||||
}
|
||||
const paramValue = this.checkedNodes[index].value;
|
||||
// 兜底:若loadingMap中无该属性,返回false
|
||||
return this.loadingMap[paramValue] || false;
|
||||
},
|
||||
|
||||
// 获取对应索引图表的标题
|
||||
getChartTitle(index) {
|
||||
if (index >= this.checkedNodes.length) return '';
|
||||
return this.checkedNodes[index].label || '';
|
||||
},
|
||||
|
||||
// 窗口resize调整所有图表尺寸
|
||||
handleResize() {
|
||||
Object.values(this.chartInstances).forEach(chart => {
|
||||
if (chart && chart.resize) {
|
||||
chart.resize();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取单个参数数据并更新对应图表【核心修复:loading响应式赋值】
|
||||
fetchChartData(paramValue, chartIndex) {
|
||||
// 前置校验:参数无效直接关闭loading并绘制无数据图表
|
||||
if (!this.enCoilID || !paramValue || chartIndex >= this.presetChartKeys.length) {
|
||||
const chartKey = this.presetChartKeys[chartIndex];
|
||||
this.drawNoDataChart(chartKey);
|
||||
// 强制关闭loading
|
||||
Vue.set(this.loadingMap, paramValue, false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`请求参数: enCoilID=${this.enCoilID}, paramField=${paramValue}`);
|
||||
|
||||
// 修复1:使用Vue.set实现响应式赋值,让视图感知状态变化
|
||||
Vue.set(this.loadingMap, paramValue, true);
|
||||
|
||||
getSegmentList({
|
||||
enCoilID: this.enCoilID,
|
||||
paramField: paramValue
|
||||
}).then(res => {
|
||||
const chartKey = this.presetChartKeys[chartIndex];
|
||||
if (res.data && res.data.length > 0) {
|
||||
// 处理数据
|
||||
const { timeStamps, chartData, originalTimeStamps, originalChartData, stat } = this.processChartData(res.data);
|
||||
this.chartDataMap[paramValue] = {
|
||||
timeStamps,
|
||||
chartData,
|
||||
originalTimeStamps,
|
||||
originalChartData
|
||||
};
|
||||
this.statMap[paramValue] = stat;
|
||||
// 仅setOption更新图表,不重新初始化
|
||||
console.log(res)
|
||||
this.drawLineChart(chartKey, paramValue);
|
||||
} else {
|
||||
this.drawNoDataChart(chartKey);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(`获取${paramValue}数据失败:`, err);
|
||||
const chartKey = this.presetChartKeys[chartIndex];
|
||||
this.drawErrorChart(chartKey);
|
||||
}).finally(() => {
|
||||
console.log(`完成${paramValue}数据获取, loading关闭`);
|
||||
// 修复2:finally中同样使用Vue.set,确保无论成功失败都关闭loading
|
||||
Vue.set(this.loadingMap, paramValue, false);
|
||||
});
|
||||
},
|
||||
|
||||
// 处理图表数据
|
||||
processChartData(rawData) {
|
||||
rawData.sort((a, b) => a.segNo - b.segNo);
|
||||
const originalTimeStamps = rawData.map(item => item.segNo);
|
||||
const originalChartData = rawData.map(item => parseFloat(item.value));
|
||||
|
||||
let timeStamps = originalTimeStamps;
|
||||
let chartData = originalChartData;
|
||||
|
||||
// 可选:数据采样逻辑
|
||||
// if (rawData.length > this.maxXAxisLabels) {
|
||||
// const sampleInterval = Math.ceil(rawData.length / this.maxXAxisLabels);
|
||||
// timeStamps = [];
|
||||
// chartData = [];
|
||||
// for (let i = 0; i < rawData.length; i += sampleInterval) {
|
||||
// timeStamps.push(rawData[i].segNo);
|
||||
// chartData.push(parseFloat(rawData[i].value));
|
||||
// }
|
||||
// if (timeStamps[timeStamps.length - 1] !== rawData[rawData.length - 1].segNo) {
|
||||
// timeStamps.push(rawData[rawData.length - 1].segNo);
|
||||
// chartData.push(parseFloat(rawData[rawData.length - 1].value));
|
||||
// }
|
||||
// }
|
||||
|
||||
let stat = { latestValue: 0, maxValue: 0, minValue: 0, avgValue: 0 };
|
||||
if (originalChartData.length) {
|
||||
stat.latestValue = originalChartData[originalChartData.length - 1];
|
||||
stat.maxValue = Math.max(...originalChartData);
|
||||
stat.minValue = Math.min(...originalChartData);
|
||||
stat.avgValue = originalChartData.reduce((sum, val) => sum + val, 0) / originalChartData.length;
|
||||
}
|
||||
|
||||
return { timeStamps, chartData, originalTimeStamps, originalChartData, stat };
|
||||
},
|
||||
|
||||
// 绘制折线图(仅setOption)
|
||||
drawLineChart(chartKey, paramValue) {
|
||||
const chart = this.chartInstances[chartKey];
|
||||
const chartDataObj = this.chartDataMap[paramValue];
|
||||
const stat = this.statMap[paramValue];
|
||||
const param = this.paramFields.find(item => item.value === paramValue);
|
||||
if (!chart || !chartDataObj) return;
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
graphic: [],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'line' },
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#d4d4d4',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333' },
|
||||
formatter: (params) => {
|
||||
const paramItem = params[0];
|
||||
return `
|
||||
Sequence: ${paramItem.name}<br>
|
||||
Value: ${paramItem.value.toFixed(2)}<br>
|
||||
Unit: ${this.getParamUnit(paramValue)}
|
||||
`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartDataObj.timeStamps,
|
||||
axisLine: { lineStyle: { color: '#d4d4d4' } },
|
||||
axisLabel: {
|
||||
color: '#666',
|
||||
rotate: 45,
|
||||
interval: (index) => {
|
||||
return index % Math.max(1, Math.ceil(chartDataObj.timeStamps.length / this.maxXAxisLabels)) === 0;
|
||||
}
|
||||
},
|
||||
splitLine: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: this.getParamUnit(paramValue),
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
nameTextStyle: { color: '#666' },
|
||||
axisLine: { lineStyle: { color: '#d4d4d4' } },
|
||||
axisLabel: { color: '#666' },
|
||||
splitLine: {
|
||||
lineStyle: { color: '#e8e8e8', type: 'dashed' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: param?.label || paramValue,
|
||||
type: 'line',
|
||||
data: chartDataObj.chartData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#666', width: 2 },
|
||||
itemStyle: { color: '#999' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(153, 153, 153, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(153, 153, 153, 0.05)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
markPoint: {
|
||||
data: [{ type: 'max', name: 'Max' }, { type: 'min', name: 'Min' }],
|
||||
itemStyle: { color: '#999' },
|
||||
label: { color: '#333' }
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: 'average', name: 'Average' }],
|
||||
lineStyle: { color: '#999', type: 'dashed' },
|
||||
label: { color: '#333' }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option, true);
|
||||
},
|
||||
|
||||
// 绘制空图表
|
||||
drawEmptyChart(chartKey) {
|
||||
const chart = this.chartInstances[chartKey];
|
||||
if (!chart) return;
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'Please select performance',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#999'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
},
|
||||
|
||||
// 绘制无数据图表
|
||||
drawNoDataChart(chartKey) {
|
||||
const chart = this.chartInstances[chartKey];
|
||||
if (!chart) return;
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'No data',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#999'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
},
|
||||
|
||||
// 绘制错误图表
|
||||
drawErrorChart(chartKey) {
|
||||
const chart = this.chartInstances[chartKey];
|
||||
if (!chart) return;
|
||||
|
||||
const option = {
|
||||
graphic: {
|
||||
elements: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: 'Data loading failed',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: '#f56c6c'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
xAxis: { type: 'category', data: [] },
|
||||
yAxis: { type: 'value' },
|
||||
series: []
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
},
|
||||
|
||||
// 获取参数单位
|
||||
getParamUnit(paramValue) {
|
||||
switch (paramValue) {
|
||||
case 'stripSpeed':
|
||||
case 'stripSpeedTmExit':
|
||||
return 'm/s';
|
||||
case 'tensionPorBr1':
|
||||
case 'tensionPorBr2':
|
||||
case 'tensionBr5Tm':
|
||||
case 'tensionTlBr7':
|
||||
return 'N';
|
||||
case 'cleaningVoltage':
|
||||
return 'V';
|
||||
case 'cleaningCurrent':
|
||||
return 'A';
|
||||
case 'alkaliConcentration':
|
||||
case 'tlElongation':
|
||||
return '%';
|
||||
case 'alkaliTemperature':
|
||||
case 'phfExitStripTemp':
|
||||
case 'rtfExitStripTemp':
|
||||
case 'jcsExitStripTemp':
|
||||
case 'scsExitStripTemp':
|
||||
case 'potTemperature':
|
||||
case 'coolingTowerStripTemp':
|
||||
return '°C';
|
||||
case 'zincPotPower':
|
||||
return 'kW';
|
||||
case 'gasConsumption':
|
||||
return 'm³/h';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,7 +121,7 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitoring-container {
|
||||
height: 100%; /* 根容器高度100%,可根据父容器调整为100% */
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -588,30 +171,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* 图表项通用样式 */
|
||||
/* 图表项容器样式 */
|
||||
.preset-chart-item {
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
width: 100%;
|
||||
height: calc(100% - 24px); /* 减去标题高度 */
|
||||
box-sizing: border-box;
|
||||
min-height: 180px; /* 兜底高度,确保ECharts初始化有尺寸 */
|
||||
}
|
||||
|
||||
/* 无勾选提示 */
|
||||
.no-param-tip {
|
||||
width: 100%;
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<el-table-column label="Device" prop="deviceName" />
|
||||
<el-table-column label="Status" prop="status" />
|
||||
<el-table-column label="Create Time" prop="createTime" />
|
||||
<el-table-column label="Finish Time" prop="finishTime" />
|
||||
<el-table-column label="Action" fixed="right">
|
||||
<el-table-column label="Finish Time" prop="finishTime" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="Action">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="apply(scope.row)">Apply</el-button>
|
||||
<el-button type="text" size="mini" @click="openDetail(scope.row.jobId)">Detail</el-button>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<!-- 炉火参数表单 -->
|
||||
<div class="app-container">
|
||||
<h3>炉火实时参数</h3>
|
||||
<!-- flex布局容器,开启自动换行 -->
|
||||
<div class="params-list">
|
||||
<!-- 遍历驱动数据的所有键值对 -->
|
||||
<!-- 遍历筛选后的驱动数据键值对 -->
|
||||
<div
|
||||
v-for="[key, value] in Object.entries(driveData)"
|
||||
v-for="[key, value] in filteredDriveData"
|
||||
:key="key"
|
||||
:class="['param-item', { blink: changedKeys.includes(key) }]"
|
||||
:class="['param-item', { blink: blinkKeyMap[key] }]"
|
||||
>
|
||||
<!-- 上方label -->
|
||||
<span class="param-label">{{ formatLabel(key) }}</span>
|
||||
@@ -31,7 +30,20 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
prevDriveData: {}, // 存储上一次的驱动数据,用于对比变化
|
||||
changedKeys: [] // 存储当前变化的属性名,用于控制闪烁样式
|
||||
blinkKeyMap: {}, // 每个key独立的闪烁状态(对象形式:{ key1: true, key2: false })
|
||||
timerMap: {} // 每个key独立的定时器缓存,用于清除旧定时器
|
||||
}
|
||||
},
|
||||
// 筛选包含Actual(不区分大小写)的键值对
|
||||
computed: {
|
||||
filteredDriveData() {
|
||||
// 校验
|
||||
if (!this.driveData || typeof this.driveData !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(this.driveData).filter(([key]) => {
|
||||
return key.toLowerCase().includes('actual');
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -44,33 +56,39 @@ export default {
|
||||
}
|
||||
|
||||
// 对比新值和旧值,找出变化的属性名
|
||||
const changedKeys = [];
|
||||
Object.entries(newVal).forEach(([key, value]) => {
|
||||
// 排除首次不存在的属性,仅对比已有属性的变化
|
||||
if (this.prevDriveData.hasOwnProperty(key)) {
|
||||
// 由于是数字类型,直接对比值是否不同
|
||||
// 仅对比已有属性的变化,且仅处理筛选后的key(包含Actual)
|
||||
if (this.prevDriveData.hasOwnProperty(key) && key.toLowerCase().includes('actual')) {
|
||||
if (this.prevDriveData[key] !== value) {
|
||||
changedKeys.push(key);
|
||||
this.triggerBlink(key); // 为每个变化的key单独触发闪烁
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (changedKeys.length > 0) {
|
||||
this.changedKeys = changedKeys; // 设置变化的属性,触发闪烁
|
||||
// 1秒后清空变化的属性,移除闪烁样式
|
||||
setTimeout(() => {
|
||||
this.changedKeys = [];
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 更新缓存的旧数据
|
||||
this.prevDriveData = JSON.parse(JSON.stringify(newVal));
|
||||
console.log('driveData updated:', newVal, 'changed keys:', changedKeys);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 单独触发某个key的闪烁(核心优化:独立控制)
|
||||
triggerBlink(key) {
|
||||
// 清除该key的旧定时器,避免1秒内多次更新导致定时器叠加
|
||||
if (this.timerMap[key]) {
|
||||
clearTimeout(this.timerMap[key]);
|
||||
}
|
||||
|
||||
// 设置当前key为闪烁状态(触发动画)
|
||||
this.$set(this.blinkKeyMap, key, true);
|
||||
|
||||
// 单独为该key设置定时器,到期后关闭闪烁状态
|
||||
this.timerMap[key] = setTimeout(() => {
|
||||
this.$set(this.blinkKeyMap, key, false);
|
||||
// 定时器执行后,清除缓存的定时器ID
|
||||
delete this.timerMap[key];
|
||||
}, 500); // 闪烁时长可自定义(1秒)
|
||||
},
|
||||
// 格式化标签名(将驼峰命名转为中文式分段,提升可读性)
|
||||
formatLabel(key) {
|
||||
if (!key) return '';
|
||||
@@ -86,6 +104,12 @@ export default {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
// 组件销毁时清除所有定时器,避免内存泄漏
|
||||
beforeUnmount() {
|
||||
Object.values(this.timerMap).forEach(timer => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -109,7 +133,7 @@ export default {
|
||||
width: 200px; /* 固定宽度,可根据需求调整 */
|
||||
padding: 12px 16px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border: 2px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex; /* 内部flex垂直布局 */
|
||||
@@ -131,7 +155,7 @@ export default {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 闪烁动画定义 */
|
||||
/* 闪烁动画定义(保留原有动画效果,优化触发逻辑) */
|
||||
@keyframes borderBlink {
|
||||
0% {
|
||||
border-color: #4cd964; /* 初始绿色 */
|
||||
@@ -147,9 +171,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* 闪烁样式 */
|
||||
/* 闪烁样式:每次blink类添加时,动画重新触发 */
|
||||
.blink {
|
||||
border: 2px solid #4cd964; /* 绿色边框 */
|
||||
animation: borderBlink 1s ease-in-out; /* 执行闪烁动画 */
|
||||
animation: borderBlink 0.5s ease-in-out 1; /* 强制执行1次动画,避免叠加 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user