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",
|
"clipboard": "2.0.8",
|
||||||
"core-js": "3.37.1",
|
"core-js": "3.37.1",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
|
"dom-to-image": "^2.6.0",
|
||||||
"echarts": "5.4.0",
|
"echarts": "5.4.0",
|
||||||
"element-ui": "2.15.14",
|
"element-ui": "2.15.14",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fuse.js": "6.4.3",
|
"fuse.js": "6.4.3",
|
||||||
"highlight.js": "9.18.5",
|
"highlight.js": "9.18.5",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"js-beautify": "1.13.0",
|
"js-beautify": "1.13.0",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
"jsencrypt": "3.0.0-rc.1",
|
||||||
|
"jspdf": "^2.5.2",
|
||||||
"konva": "^10.0.2",
|
"konva": "^10.0.2",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
|
|||||||
@@ -14,60 +14,54 @@
|
|||||||
<div class="grid-item label">Coil ID</div>
|
<div class="grid-item label">Coil ID</div>
|
||||||
<!-- 钢卷号 -->
|
<!-- 钢卷号 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.exitMatId" :border="false" class="editable-input"
|
<input v-model="editableData.exitMatId" :border="false" class="editable-input" placeholder="Coil ID">
|
||||||
placeholder="Coil ID"></input>
|
<!-- 钢卷号 -->
|
||||||
<!-- 钢卷号 -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-item label">Hot Coil ID</div>
|
<div class="grid-item label">Hot Coil ID</div>
|
||||||
<!-- 热卷号 -->
|
<!-- 热卷号 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.entryMatId" :border="false" class="editable-input"
|
<input v-model="editableData.entryMatId" :border="false" class="editable-input" placeholder="Hot Coil ID">
|
||||||
placeholder="Hot Coil ID"></input>
|
<!-- 热卷号 -->
|
||||||
<!-- 热卷号 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-item label">Specification</div>
|
<div class="grid-item label">Specification</div>
|
||||||
<!-- 规格 -->
|
<!-- 规格 -->
|
||||||
<div class="grid-item value">
|
<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>
|
||||||
<div class="grid-item label">Material</div>
|
<div class="grid-item label">Material</div>
|
||||||
<!-- 材质 -->
|
<!-- 材质 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.steelGrade" :border="false" class="editable-input"
|
<input v-model="editableData.steelGrade" :border="false" class="editable-input" placeholder="Material">
|
||||||
placeholder="Material"></input>
|
<!-- 材质 -->
|
||||||
<!-- 材质 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-item label">Net Weight</div>
|
<div class="grid-item label">Net Weight</div>
|
||||||
<!-- 净重 -->
|
<!-- 净重 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.actualWeight" :border="false" class="editable-input"
|
<input v-model="editableData.actualWeight" :border="false" class="editable-input" placeholder="Net Weight">
|
||||||
placeholder="Net Weight"></input>
|
<!-- 净重 -->
|
||||||
<!-- 净重 -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-item label">Production Shift</div>
|
<div class="grid-item label">Production Shift</div>
|
||||||
<!-- 生产班组 -->
|
<!-- 生产班组 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.groupNo" :border="false" class="editable-input"
|
<input v-model="editableData.groupNo" :border="false" class="editable-input" placeholder="Production Shift">
|
||||||
placeholder="Production Shift"></input>
|
<!-- 生产班组 -->
|
||||||
<!-- 生产班组 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-item label">Product Name</div>
|
<div class="grid-item label">Product Name</div>
|
||||||
<!-- 产品名称 -->
|
<!-- 产品名称 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.prodCode" :border="false" class="editable-input"
|
<input v-model="editableData.prodCode" :border="false" class="editable-input" placeholder="Product Name">
|
||||||
placeholder="Product Name"></input>
|
<!-- 产品名称 -->
|
||||||
<!-- 产品名称 -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-item label">Production Date</div>
|
<div class="grid-item label">Production Date</div>
|
||||||
<!-- 生产日期 -->
|
<!-- 生产日期 -->
|
||||||
<div class="grid-item value">
|
<div class="grid-item value">
|
||||||
<input v-model="editableData.productionDate" :border="false" class="editable-input"
|
<input v-model="editableData.productionDate" :border="false" class="editable-input"
|
||||||
placeholder="Production Date"></input>
|
placeholder="Production Date">
|
||||||
<!-- 生产日期 -->
|
<!-- 生产日期 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,6 +81,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import printJS from 'print-js';
|
import printJS from 'print-js';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
// 导入 dom-to-image 库
|
||||||
|
import domtoimage from 'dom-to-image';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PdoLabelPrint',
|
name: 'PdoLabelPrint',
|
||||||
@@ -145,22 +141,28 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle print logic / 处理打印逻辑
|
// 替换原dom-to-image的转换逻辑
|
||||||
handlePrint() {
|
async handlePrint() {
|
||||||
// Call printJS to print / 调用printJS进行打印
|
try {
|
||||||
printJS({
|
const printElement = document.getElementById('printContent');
|
||||||
printable: 'printContent', // Element ID to print / 要打印的元素ID
|
// 关键:设置scale提升分辨率(建议3-4)
|
||||||
type: 'html',
|
const imageBase64 = await domtoimage.toPng(printElement, {
|
||||||
header: null, // Don't show default header / 不显示默认页眉
|
backgroundColor: '#ffffff',
|
||||||
footer: null, // Don't show default footer / 不显示默认页脚
|
scale: 2, // 缩放倍数(越大图片越清晰,文件也越大)
|
||||||
// style: printStyle, // Apply print styles / 应用打印样式
|
});
|
||||||
scanStyles: true, // Don't scan existing page styles / 不扫描页面现有样式
|
|
||||||
targetStyles: ['*'], // Allow all target styles / 允许所有目标样式
|
// 打印图片时强制按实际尺寸渲染
|
||||||
documentTitle: 'Label Print', // Print document title / 打印文档标题
|
printJS({
|
||||||
onPrintDialogClose: () => {
|
printable: imageBase64,
|
||||||
console.log('Print dialog closed'); // 打印对话框已关闭
|
type: 'image',
|
||||||
}
|
imageStyle: `
|
||||||
});
|
width: 100mm; /* 与标签实际物理尺寸一致 */
|
||||||
|
height: 80mm;
|
||||||
|
object-fit: contain;
|
||||||
|
`,
|
||||||
|
documentTitle: 'Label Print'
|
||||||
|
});
|
||||||
|
} catch (e) { /* 错误处理 */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,12 +180,17 @@ export default {
|
|||||||
|
|
||||||
/* 标签内容样式(与打印尺寸等比例) */
|
/* 标签内容样式(与打印尺寸等比例) */
|
||||||
.label-content {
|
.label-content {
|
||||||
width: 500px; /* 200mm × 2.5 = 500px */
|
width: 500px;
|
||||||
height: 400px; /* 160mm × 2.5 = 400px */
|
/* 200mm × 2.5 = 500px */
|
||||||
|
height: 400px;
|
||||||
|
/* 160mm × 2.5 = 400px */
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 25px; /* 10mm × 2.5 = 25px */
|
padding: 25px;
|
||||||
|
/* 10mm × 2.5 = 25px */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: SimSun;
|
font-family: SimSun;
|
||||||
|
background: #ffffff;
|
||||||
|
/* 确保DOM背景为白色,转换图片后样式一致 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.company-name {
|
.company-name {
|
||||||
@@ -239,4 +246,4 @@ export default {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
margin-top: 10px;
|
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 / 二维码占位 -->
|
<!-- QR code placeholder / 二维码占位 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -192,6 +192,23 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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 / 底部信息 -->
|
<!-- Footer information / 底部信息 -->
|
||||||
<div class="certificate-footer">
|
<div class="certificate-footer">
|
||||||
<div class="footer-row">
|
<div class="footer-row">
|
||||||
@@ -252,9 +269,11 @@
|
|||||||
<div class="remarks-title">Remarks:</div>
|
<div class="remarks-title">Remarks:</div>
|
||||||
<!-- 备注 -->
|
<!-- 备注 -->
|
||||||
<div class="remarks-content">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,9 +292,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import printJS from 'print-js';
|
import printJS from 'print-js';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
// 1. 导入 dom-to-image 库
|
||||||
|
import domtoimage from 'dom-to-image';
|
||||||
|
// 获取生产过程曲线数据
|
||||||
|
import ParamEcharts from './ParamEcharts.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'QualityCertificate',
|
name: 'QualityCertificate',
|
||||||
|
components: {
|
||||||
|
ParamEcharts
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
// Single coil detail / 单卷详情
|
// Single coil detail / 单卷详情
|
||||||
detail: {
|
detail: {
|
||||||
@@ -301,6 +327,12 @@ export default {
|
|||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
getEnCoilID() {
|
||||||
|
// 优先从detail中取有效线圈ID(与后端接口保持一致)
|
||||||
|
return this.detail.enCoilID || this.detail.exitMatId || this.detail.coilid || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Initialize certificate data for single coil / 初始化单卷质保书数据
|
// Initialize certificate data for single coil / 初始化单卷质保书数据
|
||||||
initCertificateData(detail) {
|
initCertificateData(detail) {
|
||||||
@@ -388,32 +420,46 @@ export default {
|
|||||||
return num.toFixed(2);
|
return num.toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle print / 处理打印
|
// 2. 重构打印方法:DOM -> 高清图片 -> 打印
|
||||||
handlePrint() {
|
async handlePrint() {
|
||||||
const printStyle = `
|
try {
|
||||||
@page {
|
// 获取打印目标DOM元素
|
||||||
size: A4 landscape;
|
const printElement = document.getElementById('qualityCertificateContent');
|
||||||
margin: 10mm;
|
if (!printElement) {
|
||||||
|
this.$message?.error('未找到质保书打印内容');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
.certificate-content {
|
|
||||||
width: 100%;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
printJS({
|
// 将DOM转为高清PNG图片(base64格式),解决虚化问题
|
||||||
printable: 'qualityCertificateContent',
|
const imageBase64 = await domtoimage.toPng(printElement, {
|
||||||
type: 'html',
|
backgroundColor: '#ffffff', // 强制白色背景,避免透明区域
|
||||||
header: null,
|
scale: 4, // 缩放倍数(3-4最佳,越大越清晰,文件体积略有增加)
|
||||||
footer: null,
|
quality: 1.0 // 图片质量拉满
|
||||||
style: printStyle,
|
});
|
||||||
scanStyles: true,
|
|
||||||
targetStyles: ['*'],
|
// 调用printJS打印图片(适配A4横向布局)
|
||||||
documentTitle: 'Quality Certificate',
|
printJS({
|
||||||
onPrintDialogClose: () => {
|
printable: imageBase64, // 传入base64图片
|
||||||
console.log('Print dialog closed');
|
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;
|
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 {
|
@media print {
|
||||||
.print-btn-container {
|
.print-btn-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-certificate-container {
|
.quality-certificate-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
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-3': checkedCount === 3,
|
||||||
'count-4': checkedCount === 4
|
'count-4': checkedCount === 4
|
||||||
}" :style="{ height: '100%', width: '100%' }">
|
}" :style="{ height: '100%', width: '100%' }">
|
||||||
<!-- 预生成4个固定图表容器(初始隐藏) -->
|
<!-- 替换为封装后的 ECharts 组件,循环渲染 -->
|
||||||
<div v-for="(chartKey, index) in presetChartKeys" :key="chartKey" class="preset-chart-item" :style="{
|
<div v-for="(param, index) in checkedNodes" :key="param.value" class="preset-chart-item"
|
||||||
display: index < checkedCount ? 'block' : 'none', // 根据勾选数量显示对应图表
|
:style="{ display: index < maxCheckCount ? 'block' : 'none' }">
|
||||||
}" v-loading="getChartLoading(index)">
|
<!-- 传入 enCoilID 和 paramField(即 param.value) -->
|
||||||
<div class="chart-title">{{ getChartTitle(index) }}</div>
|
<ParamEcharts :enCoilID="enCoilID" :paramField="param.value" />
|
||||||
<div :ref="chartKey" class="chart-content"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无勾选时显示提示 -->
|
<!-- 无勾选时显示提示 -->
|
||||||
@@ -36,13 +35,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as echarts from 'echarts'
|
import ParamEcharts from './ParamEcharts.vue' // 引入封装后的 ECharts 组件
|
||||||
import { getSegmentList } from '@/api/business/segment'
|
|
||||||
// 引入Vue(用于Vue.set实现响应式赋值,Vue2必备)
|
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeviceMonitoring',
|
name: 'DeviceMonitoring',
|
||||||
|
components: { ParamEcharts }, // 注册组件
|
||||||
props: {
|
props: {
|
||||||
enCoilID: {
|
enCoilID: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -79,84 +76,28 @@ export default {
|
|||||||
},
|
},
|
||||||
checkedNodes: [], // 已勾选参数列表
|
checkedNodes: [], // 已勾选参数列表
|
||||||
checkedCount: 0, // 已勾选数量(用于布局控制)
|
checkedCount: 0, // 已勾选数量(用于布局控制)
|
||||||
presetChartKeys: ['chart1', 'chart2', 'chart3', 'chart4'], // 4个固定图表key
|
maxCheckCount: 4 // 最大勾选数量
|
||||||
chartInstances: {}, // 4个固定图表实例 { chart1: echartsInstance, ... }
|
|
||||||
chartDataMap: {}, // 参数数据映射 { paramValue: { timeStamps: [], chartData: [], ... } }
|
|
||||||
statMap: {}, // 参数统计值映射 { paramValue: { latestValue: 0, ... } }
|
|
||||||
loadingMap: {}, // 参数加载状态映射 { paramValue: boolean }
|
|
||||||
maxXAxisLabels: 40,
|
|
||||||
maxCheckCount: 4
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
// 监听线圈ID变化,无需手动更新图表(子组件已自行监听)
|
||||||
enCoilID: {
|
enCoilID: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
console.log('enCoilID changed:', 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
|
immediate: true
|
||||||
},
|
},
|
||||||
// 监听勾选数量变化,触发布局更新(可选,确保响应式)
|
// 监听勾选数量变化,触发布局更新
|
||||||
checkedCount: {
|
checkedCount: {
|
||||||
handler() {
|
handler() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// 窗口resize强制刷新图表尺寸
|
// 窗口resize强制刷新(可选,子组件已监听resize,此处为兜底)
|
||||||
this.handleResize();
|
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: {
|
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个)
|
// 处理树形节点勾选(限制最多4个)
|
||||||
handleCheckChange() {
|
handleCheckChange() {
|
||||||
const checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
const checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
||||||
@@ -170,367 +111,9 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先关闭所有旧勾选参数的loading
|
|
||||||
this.checkedNodes.forEach(param => {
|
|
||||||
Vue.set(this.loadingMap, param.value, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新勾选列表和数量
|
// 更新勾选列表和数量
|
||||||
this.checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
this.checkedNodes = this.$refs.paramTree.getCheckedNodes();
|
||||||
this.checkedCount = this.checkedNodes.length;
|
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>
|
<style lang="scss" scoped>
|
||||||
.monitoring-container {
|
.monitoring-container {
|
||||||
height: 100%; /* 根容器高度100%,可根据父容器调整为100% */
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,30 +171,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图表项通用样式 */
|
/* 图表项容器样式 */
|
||||||
.preset-chart-item {
|
.preset-chart-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
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 {
|
.no-param-tip {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
<el-table-column label="Device" prop="deviceName" />
|
<el-table-column label="Device" prop="deviceName" />
|
||||||
<el-table-column label="Status" prop="status" />
|
<el-table-column label="Status" prop="status" />
|
||||||
<el-table-column label="Create Time" prop="createTime" />
|
<el-table-column label="Create Time" prop="createTime" />
|
||||||
<el-table-column label="Finish Time" prop="finishTime" />
|
<el-table-column label="Finish Time" prop="finishTime" width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="Action" fixed="right">
|
<el-table-column label="Action">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click="apply(scope.row)">Apply</el-button>
|
<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>
|
<el-button type="text" size="mini" @click="openDetail(scope.row.jobId)">Detail</el-button>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 炉火参数表单 -->
|
<!-- 炉火参数表单 -->
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<h3>炉火实时参数</h3>
|
|
||||||
<!-- flex布局容器,开启自动换行 -->
|
<!-- flex布局容器,开启自动换行 -->
|
||||||
<div class="params-list">
|
<div class="params-list">
|
||||||
<!-- 遍历驱动数据的所有键值对 -->
|
<!-- 遍历筛选后的驱动数据键值对 -->
|
||||||
<div
|
<div
|
||||||
v-for="[key, value] in Object.entries(driveData)"
|
v-for="[key, value] in filteredDriveData"
|
||||||
:key="key"
|
:key="key"
|
||||||
:class="['param-item', { blink: changedKeys.includes(key) }]"
|
:class="['param-item', { blink: blinkKeyMap[key] }]"
|
||||||
>
|
>
|
||||||
<!-- 上方label -->
|
<!-- 上方label -->
|
||||||
<span class="param-label">{{ formatLabel(key) }}</span>
|
<span class="param-label">{{ formatLabel(key) }}</span>
|
||||||
@@ -31,7 +30,20 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
prevDriveData: {}, // 存储上一次的驱动数据,用于对比变化
|
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: {
|
watch: {
|
||||||
@@ -44,33 +56,39 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对比新值和旧值,找出变化的属性名
|
// 对比新值和旧值,找出变化的属性名
|
||||||
const changedKeys = [];
|
|
||||||
Object.entries(newVal).forEach(([key, value]) => {
|
Object.entries(newVal).forEach(([key, value]) => {
|
||||||
// 排除首次不存在的属性,仅对比已有属性的变化
|
// 仅对比已有属性的变化,且仅处理筛选后的key(包含Actual)
|
||||||
if (this.prevDriveData.hasOwnProperty(key)) {
|
if (this.prevDriveData.hasOwnProperty(key) && key.toLowerCase().includes('actual')) {
|
||||||
// 由于是数字类型,直接对比值是否不同
|
|
||||||
if (this.prevDriveData[key] !== value) {
|
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));
|
this.prevDriveData = JSON.parse(JSON.stringify(newVal));
|
||||||
console.log('driveData updated:', newVal, 'changed keys:', changedKeys);
|
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
formatLabel(key) {
|
||||||
if (!key) return '';
|
if (!key) return '';
|
||||||
@@ -86,6 +104,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// 组件销毁时清除所有定时器,避免内存泄漏
|
||||||
|
beforeUnmount() {
|
||||||
|
Object.values(this.timerMap).forEach(timer => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -109,7 +133,7 @@ export default {
|
|||||||
width: 200px; /* 固定宽度,可根据需求调整 */
|
width: 200px; /* 固定宽度,可根据需求调整 */
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #e6e6e6;
|
border: 2px solid #e6e6e6;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: flex; /* 内部flex垂直布局 */
|
display: flex; /* 内部flex垂直布局 */
|
||||||
@@ -131,7 +155,7 @@ export default {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 闪烁动画定义 */
|
/* 闪烁动画定义(保留原有动画效果,优化触发逻辑) */
|
||||||
@keyframes borderBlink {
|
@keyframes borderBlink {
|
||||||
0% {
|
0% {
|
||||||
border-color: #4cd964; /* 初始绿色 */
|
border-color: #4cd964; /* 初始绿色 */
|
||||||
@@ -147,9 +171,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 闪烁样式 */
|
/* 闪烁样式:每次blink类添加时,动画重新触发 */
|
||||||
.blink {
|
.blink {
|
||||||
border: 2px solid #4cd964; /* 绿色边框 */
|
border: 2px solid #4cd964; /* 绿色边框 */
|
||||||
animation: borderBlink 1s ease-in-out; /* 执行闪烁动画 */
|
animation: borderBlink 0.5s ease-in-out 1; /* 强制执行1次动画,避免叠加 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user