新增写入功能,追踪弹窗
This commit is contained in:
@@ -5,7 +5,7 @@ export function createSendJob(data) {
|
|||||||
return request({
|
return request({
|
||||||
url: '/business/sendJob',
|
url: '/business/sendJob',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,8 +17,9 @@ export function executeSendJob(jobId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询发送任务列表(用于历史追溯,此页面暂不使用)
|
// 查询发送任务列表(分页)
|
||||||
export function listSendJob(query) {
|
export function listSendJob(query) {
|
||||||
|
// 若你后端是 GET /business/sendJob/list(现状),这里用 params
|
||||||
return request({
|
return request({
|
||||||
url: '/business/sendJob/list',
|
url: '/business/sendJob/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -26,11 +27,10 @@ export function listSendJob(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取发送任务详情(用于历史追溯,此页面暂不使用)
|
// 获取发送任务详情
|
||||||
export function getSendJob(jobId) {
|
export function getSendJob(jobId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/business/sendJob/${jobId}`,
|
url: `/business/sendJob/${jobId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/api/l2/setupValue.js
Normal file
20
src/api/l2/setupValue.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import request from '@/utils/L2Request'
|
||||||
|
|
||||||
|
// 获取 DRIVE 设定值(最新成功发送)
|
||||||
|
export function getDriveSetupValue() {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendJob/lastSuccess',
|
||||||
|
method: 'get',
|
||||||
|
params: { groupType: 'DRIVE' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 FURNACE 设定值(最新成功发送)
|
||||||
|
export function getFurnaceSetupValue() {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendJob/lastSuccess',
|
||||||
|
method: 'get',
|
||||||
|
params: { groupType: 'FURNACE' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
237
src/components/FloatingPanel.vue
Normal file
237
src/components/FloatingPanel.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="visible" class="fp-root" :style="rootStyle" @mousedown.stop>
|
||||||
|
<div class="fp-header" @mousedown.prevent.stop="onDragStart">
|
||||||
|
<div class="fp-title">{{ title }}</div>
|
||||||
|
<div class="fp-actions">
|
||||||
|
<el-button type="text" class="fp-btn" @click.stop="toggleMinimize">
|
||||||
|
<i :class="minimized ? 'el-icon-caret-bottom' : 'el-icon-caret-top'"></i>
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" class="fp-btn" @click.stop="close">
|
||||||
|
<i class="el-icon-close"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!minimized" class="fp-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handle / 缩放手柄 -->
|
||||||
|
<div v-show="!minimized" class="fp-resize" @mousedown.prevent.stop="onResizeStart"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// English UI + Chinese comments
|
||||||
|
export default {
|
||||||
|
name: 'FloatingPanel',
|
||||||
|
props: {
|
||||||
|
title: { type: String, default: 'Floating Panel' },
|
||||||
|
storageKey: { type: String, required: true },
|
||||||
|
defaultX: { type: Number, default: 20 },
|
||||||
|
defaultY: { type: Number, default: 20 },
|
||||||
|
defaultW: { type: Number, default: 420 },
|
||||||
|
defaultH: { type: Number, default: 520 }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: true,
|
||||||
|
minimized: false,
|
||||||
|
x: this.defaultX,
|
||||||
|
y: this.defaultY,
|
||||||
|
w: this.defaultW,
|
||||||
|
h: this.defaultH,
|
||||||
|
|
||||||
|
dragging: false,
|
||||||
|
resizing: false,
|
||||||
|
startMouseX: 0,
|
||||||
|
startMouseY: 0,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
startW: 0,
|
||||||
|
startH: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rootStyle() {
|
||||||
|
return {
|
||||||
|
left: this.x + 'px',
|
||||||
|
top: this.y + 'px',
|
||||||
|
width: this.w + 'px',
|
||||||
|
height: this.minimized ? 'auto' : this.h + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.restore()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.detachEvents()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 外部可调用:重新打开
|
||||||
|
open() {
|
||||||
|
this.visible = true
|
||||||
|
this.persist()
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.visible = false
|
||||||
|
this.persist()
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
toggleMinimize() {
|
||||||
|
this.minimized = !this.minimized
|
||||||
|
this.persist()
|
||||||
|
},
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(this.storageKey)
|
||||||
|
if (!raw) return
|
||||||
|
const s = JSON.parse(raw)
|
||||||
|
if (typeof s.visible === 'boolean') this.visible = s.visible
|
||||||
|
if (typeof s.minimized === 'boolean') this.minimized = s.minimized
|
||||||
|
if (typeof s.x === 'number') this.x = s.x
|
||||||
|
if (typeof s.y === 'number') this.y = s.y
|
||||||
|
if (typeof s.w === 'number') this.w = s.w
|
||||||
|
if (typeof s.h === 'number') this.h = s.h
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
persist() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
this.storageKey,
|
||||||
|
JSON.stringify({
|
||||||
|
visible: this.visible,
|
||||||
|
minimized: this.minimized,
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
w: this.w,
|
||||||
|
h: this.h
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Drag
|
||||||
|
onDragStart(e) {
|
||||||
|
this.dragging = true
|
||||||
|
this.startMouseX = e.clientX
|
||||||
|
this.startMouseY = e.clientY
|
||||||
|
this.startX = this.x
|
||||||
|
this.startY = this.y
|
||||||
|
this.attachEvents()
|
||||||
|
},
|
||||||
|
onDragMove(e) {
|
||||||
|
if (!this.dragging) return
|
||||||
|
const dx = e.clientX - this.startMouseX
|
||||||
|
const dy = e.clientY - this.startMouseY
|
||||||
|
this.x = Math.max(0, this.startX + dx)
|
||||||
|
this.y = Math.max(0, this.startY + dy)
|
||||||
|
},
|
||||||
|
onDragEnd() {
|
||||||
|
if (!this.dragging) return
|
||||||
|
this.dragging = false
|
||||||
|
this.persist()
|
||||||
|
this.detachEvents()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
onResizeStart(e) {
|
||||||
|
this.resizing = true
|
||||||
|
this.startMouseX = e.clientX
|
||||||
|
this.startMouseY = e.clientY
|
||||||
|
this.startW = this.w
|
||||||
|
this.startH = this.h
|
||||||
|
this.attachEvents()
|
||||||
|
},
|
||||||
|
onResizeMove(e) {
|
||||||
|
if (!this.resizing) return
|
||||||
|
const dx = e.clientX - this.startMouseX
|
||||||
|
const dy = e.clientY - this.startMouseY
|
||||||
|
const minW = 320
|
||||||
|
const minH = 220
|
||||||
|
this.w = Math.max(minW, this.startW + dx)
|
||||||
|
this.h = Math.max(minH, this.startH + dy)
|
||||||
|
},
|
||||||
|
onResizeEnd() {
|
||||||
|
if (!this.resizing) return
|
||||||
|
this.resizing = false
|
||||||
|
this.persist()
|
||||||
|
this.detachEvents()
|
||||||
|
},
|
||||||
|
|
||||||
|
attachEvents() {
|
||||||
|
window.addEventListener('mousemove', this.onGlobalMove)
|
||||||
|
window.addEventListener('mouseup', this.onGlobalUp)
|
||||||
|
},
|
||||||
|
detachEvents() {
|
||||||
|
window.removeEventListener('mousemove', this.onGlobalMove)
|
||||||
|
window.removeEventListener('mouseup', this.onGlobalUp)
|
||||||
|
},
|
||||||
|
onGlobalMove(e) {
|
||||||
|
// 复用一套全局事件
|
||||||
|
this.onDragMove(e)
|
||||||
|
this.onResizeMove(e)
|
||||||
|
},
|
||||||
|
onGlobalUp() {
|
||||||
|
this.onDragEnd()
|
||||||
|
this.onResizeEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fp-root {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.fp-header {
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa, #eef2f7);
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.fp-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.fp-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.fp-btn {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.fp-body {
|
||||||
|
height: calc(100% - 36px);
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.fp-resize {
|
||||||
|
position: absolute;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
cursor: se-resize;
|
||||||
|
background: linear-gradient(135deg, transparent 50%, rgba(64, 158, 255, 0.35) 50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- Page Title / 页面标题 -->
|
|
||||||
<h2 class="page-title">Drive Parameter Sending</h2>
|
|
||||||
|
|
||||||
<!-- Toolbar / 工具栏 -->
|
<!-- Toolbar / 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
||||||
@@ -37,7 +34,6 @@
|
|||||||
<span class="card-title">Steel Grade: {{ plan.steelGrade || '-' }}</span>
|
<span class="card-title">Steel Grade: {{ plan.steelGrade || '-' }}</span>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<!-- Last send time / 上次发送时间 -->
|
|
||||||
<span v-if="lastSuccess && lastSuccess.lastSendTime" class="last-send-time">
|
<span v-if="lastSuccess && lastSuccess.lastSendTime" class="last-send-time">
|
||||||
<i class="el-icon-time"></i>
|
<i class="el-icon-time"></i>
|
||||||
Last Sent: {{ formatTime(lastSuccess.lastSendTime) }}
|
Last Sent: {{ formatTime(lastSuccess.lastSendTime) }}
|
||||||
@@ -56,18 +52,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Template-driven form / 按模板渲染可编辑表单 -->
|
<!-- Plan-driven editable form / 按计划参数渲染可编辑表单(不使用模板) -->
|
||||||
<el-form :model="plan.params" label-position="top" size="mini">
|
<el-form :model="plan.params" label-position="top" size="mini">
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col
|
<el-col
|
||||||
v-for="item in templateItems"
|
v-for="item in driveFields"
|
||||||
:key="item.templateItemId || item.paramCode"
|
:key="item.key"
|
||||||
:span="12"
|
:span="12"
|
||||||
>
|
>
|
||||||
<el-form-item :label="item.labelEn">
|
<el-form-item :label="item.label">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="plan.params[item.paramCode]"
|
v-model="plan.params[item.key]"
|
||||||
:placeholder="getPlaceholder(item)"
|
:placeholder="getPlaceholder(item.key)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -90,57 +86,89 @@
|
|||||||
import { listPlan } from '@/api/l2/plan'
|
import { listPlan } from '@/api/l2/plan'
|
||||||
import { listSetup } from '@/api/business/setup'
|
import { listSetup } from '@/api/business/setup'
|
||||||
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
|
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
|
||||||
import { getSendTemplate, getLastSuccess } from '@/api/l2/sendTemplate'
|
import { getLastSuccess } from '@/api/l2/sendTemplate'
|
||||||
|
|
||||||
|
// Drive fields definition (English UI, Chinese comments) / 传动字段定义(英文界面,中文注释)
|
||||||
|
// 说明:key 必须与 setupForm 字段一致(来自 plan/components/setupForm.vue)
|
||||||
|
const DRIVE_FIELDS = [
|
||||||
|
{ key: 'porTension', label: 'Pay-off Reel Tension' },
|
||||||
|
{ key: 'celTension', label: 'Entry Loop Tension' },
|
||||||
|
{ key: 'cleanTension', label: 'Cleaning Section Tension' },
|
||||||
|
{ key: 'furTension', label: 'Furnace Zone Tension' },
|
||||||
|
{ key: 'towerTension', label: 'Cooling Tower Tension' },
|
||||||
|
{ key: 'tmNoneTension', label: 'TM No Tension' },
|
||||||
|
{ key: 'tmEntryTension', label: 'TM Entry Tension' },
|
||||||
|
{ key: 'tmExitTension', label: 'TM Exit Tension' },
|
||||||
|
{ key: 'tlNoneTension', label: 'TL No Tension' },
|
||||||
|
{ key: 'tlExitTension', label: 'TL Exit Tension' },
|
||||||
|
{ key: 'coatTension', label: 'Post-treatment Tension' },
|
||||||
|
{ key: 'cxlTension', label: 'Exit Loop Tension' },
|
||||||
|
{ key: 'trTension', label: 'Take-up Reel Tension' },
|
||||||
|
|
||||||
|
{ key: 'tlElong', label: 'TL Elongation' },
|
||||||
|
{ key: 'tlLvlMesh1', label: 'TL Leveling Roll Mesh 1' },
|
||||||
|
{ key: 'tlLvlMesh2', label: 'TL Leveling Roll Mesh 2' },
|
||||||
|
{ key: 'tlAcbMesh', label: 'TL Anti-crossbow Mesh' },
|
||||||
|
|
||||||
|
{ key: 'tmBendforce', label: 'TM Bending Force' },
|
||||||
|
{ key: 'tmAcrMesh', label: 'TM Anti-crimping Roll Mesh' },
|
||||||
|
{ key: 'tmBrMesh', label: 'TM Anti-tremor Roll Mesh' },
|
||||||
|
{ key: 'tmRollforce', label: 'TM Roll Force' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// OPC address mapping (must align with back-end OpcMessageIdsManager.pdiSetupIds) / OPC点位映射(需与后端一致)
|
||||||
|
// 中文注释:这里用“字段名->OPC地址”的方式直接组装发送items
|
||||||
|
const DRIVE_ADDRESS = {
|
||||||
|
porTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionPorBR1',
|
||||||
|
celTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR3',
|
||||||
|
cleanTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR1BR2',
|
||||||
|
furTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionFur1',
|
||||||
|
towerTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionFur2',
|
||||||
|
tmNoneTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR5BR6',
|
||||||
|
tmEntryTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR5TM',
|
||||||
|
tmExitTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionTMBR6',
|
||||||
|
tlNoneTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR6BR7',
|
||||||
|
tlExitTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionTLBR7',
|
||||||
|
coatTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR7BR8',
|
||||||
|
cxlTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR8BR9',
|
||||||
|
trTension: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.tensionBR9TR',
|
||||||
|
|
||||||
|
tlElong: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.TLElongation',
|
||||||
|
tlLvlMesh1: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.LevelingMesh1',
|
||||||
|
tlLvlMesh2: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.LevelingMesh2',
|
||||||
|
tlAcbMesh: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.AntiCrossBowUnitMesh',
|
||||||
|
|
||||||
|
tmBendforce: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.TMBendforce',
|
||||||
|
tmAcrMesh: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.ACRMesh',
|
||||||
|
tmBrMesh: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.BRMesh',
|
||||||
|
tmRollforce: 'ns=2;s=ProcessCGL.PLCLine.L2Setup.TMRollforce'
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DriveSend',
|
name: 'DriveSend',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false, // Loading state / 加载状态
|
loading: false,
|
||||||
template: null, // Template data / 模板数据
|
lastSuccess: null,
|
||||||
lastSuccess: null, // Last success data / 上次成功记录
|
plans: [],
|
||||||
plans: [] // Plans with editable params / 带可编辑参数的钢种卡片
|
driveFields: DRIVE_FIELDS
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// Template items sorted by itemNo / 模板明细按序号排序
|
|
||||||
templateItems() {
|
|
||||||
if (!this.template || !Array.isArray(this.template.items)) return []
|
|
||||||
return [...this.template.items]
|
|
||||||
.filter(i => i.enabled === undefined || i.enabled === 1)
|
|
||||||
.sort((a, b) => (a.itemNo || 0) - (b.itemNo || 0))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.reload()
|
this.reload()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Reload all / 重新加载:模板 + 上次成功记录 + 计划卡片
|
|
||||||
async reload() {
|
async reload() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
// 1) Load template + last success in parallel / 并行加载模板与上次成功记录
|
// last success for DRIVE / 获取传动上次成功
|
||||||
const [tplRes, lastRes] = await Promise.all([
|
const lastRes = await getLastSuccess('DRIVE')
|
||||||
getSendTemplate('DRIVE_DEFAULT'),
|
this.lastSuccess = lastRes && lastRes.code === 200 ? lastRes.data : null
|
||||||
getLastSuccess('DRIVE')
|
|
||||||
])
|
|
||||||
|
|
||||||
// 后端返回 AjaxResult:{code,msg,data}
|
// plans / 获取计划
|
||||||
this.template = tplRes ? tplRes.data : null
|
|
||||||
this.lastSuccess = lastRes ? lastRes.data : null
|
|
||||||
|
|
||||||
if (!this.template || !this.templateItems.length) {
|
|
||||||
this.$message.error('DRIVE_DEFAULT template is empty')
|
|
||||||
this.plans = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Load plans / 再加载计划(卡片只展示钢种)
|
|
||||||
const planRes = await listPlan({ status: 'NEW,READY' })
|
const planRes = await listPlan({ status: 'NEW,READY' })
|
||||||
const planList = planRes.rows || []
|
const planList = planRes.rows || []
|
||||||
|
|
||||||
// 3) For each plan fetch setup, and map to template paramCode
|
|
||||||
// 每条计划拉取setup并映射到模板paramCode;优先级:setup值 > 上次成功值 > 模板默认值
|
|
||||||
const tasks = planList.map(async (p) => {
|
const tasks = planList.map(async (p) => {
|
||||||
let setup = {}
|
let setup = {}
|
||||||
try {
|
try {
|
||||||
@@ -151,16 +179,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params = {}
|
const params = {}
|
||||||
this.templateItems.forEach(item => {
|
this.driveFields.forEach(f => {
|
||||||
const fromSetup = setup[item.paramCode]
|
const fromSetup = setup[f.key]
|
||||||
const fromLast = this.lastSuccess && this.lastSuccess.values ? this.lastSuccess.values[item.paramCode] : undefined
|
const fromLast = this.lastSuccess?.values?.[f.key]
|
||||||
|
|
||||||
if (fromSetup !== undefined && fromSetup !== null && String(fromSetup) !== '') {
|
if (fromSetup !== undefined && fromSetup !== null && String(fromSetup) !== '') {
|
||||||
params[item.paramCode] = String(fromSetup)
|
params[f.key] = String(fromSetup)
|
||||||
} else if (fromLast !== undefined && fromLast !== null) {
|
} else if (fromLast !== undefined && fromLast !== null) {
|
||||||
params[item.paramCode] = String(fromLast)
|
params[f.key] = String(fromLast)
|
||||||
} else {
|
} else {
|
||||||
params[item.paramCode] = item.defaultValueRaw || ''
|
params[f.key] = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -181,45 +208,33 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Apply last success values to all cards / 将上次成功值应用到所有卡片(覆盖当前输入)
|
|
||||||
applyLastSuccessValues() {
|
applyLastSuccessValues() {
|
||||||
if (!this.lastSuccess || !this.lastSuccess.values) {
|
if (!this.lastSuccess || !this.lastSuccess.values) {
|
||||||
this.$message.info('No last success data')
|
this.$message.info('No last success data')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.plans.forEach(plan => {
|
this.plans.forEach(plan => {
|
||||||
this.templateItems.forEach(item => {
|
this.driveFields.forEach(f => {
|
||||||
const v = this.lastSuccess.values[item.paramCode]
|
const v = this.lastSuccess.values[f.key]
|
||||||
if (v !== undefined) {
|
if (v !== undefined) {
|
||||||
this.$set(plan.params, item.paramCode, String(v))
|
this.$set(plan.params, f.key, String(v))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$message.success('Last success values applied')
|
this.$message.success('Last success values applied')
|
||||||
},
|
},
|
||||||
|
|
||||||
// Placeholder shows last/default / 占位符显示上次值或默认值
|
getPlaceholder(key) {
|
||||||
getPlaceholder(item) {
|
const v = this.lastSuccess?.values?.[key]
|
||||||
const fromLast = this.lastSuccess && this.lastSuccess.values ? this.lastSuccess.values[item.paramCode] : undefined
|
if (v !== undefined) return `Last: ${v}`
|
||||||
if (fromLast !== undefined) {
|
|
||||||
return `Last: ${fromLast}`
|
|
||||||
}
|
|
||||||
if (item.defaultValueRaw) {
|
|
||||||
return `Default: ${item.defaultValueRaw}`
|
|
||||||
}
|
|
||||||
return 'Please enter'
|
return 'Please enter'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Format time / 格式化时间
|
|
||||||
formatTime(t) {
|
formatTime(t) {
|
||||||
if (!t) return ''
|
if (!t) return ''
|
||||||
const d = new Date(t)
|
return new Date(t).toLocaleString()
|
||||||
return d.toLocaleString()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Send / 发送
|
|
||||||
handleSend(plan) {
|
handleSend(plan) {
|
||||||
this.$confirm(
|
this.$confirm(
|
||||||
`Confirm to send parameters for Steel Grade [${plan.steelGrade || '-'}]?`,
|
`Confirm to send parameters for Steel Grade [${plan.steelGrade || '-'}]?`,
|
||||||
@@ -233,28 +248,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async doSend(plan) {
|
async doSend(plan) {
|
||||||
if (!this.template) {
|
|
||||||
this.$message.error('Template not loaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
plan.sending = true
|
plan.sending = true
|
||||||
try {
|
try {
|
||||||
// Build items from template / 按模板组装发送明细
|
const items = this.driveFields.map(f => ({
|
||||||
const items = this.templateItems.map(item => ({
|
paramCode: f.key,
|
||||||
paramCode: item.paramCode,
|
address: DRIVE_ADDRESS[f.key],
|
||||||
address: item.address,
|
valueRaw: String(plan.params[f.key] || ''),
|
||||||
valueRaw: String(plan.params[item.paramCode] || ''),
|
|
||||||
setTime: new Date()
|
setTime: new Date()
|
||||||
})).filter(it => !!it.address)
|
})).filter(it => !!it.address)
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
this.$message.error('No valid OPC address')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dto = {
|
const dto = {
|
||||||
deviceName: this.template.deviceName,
|
deviceName: 'CGL_LINE_1',
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
groupNo: 1,
|
groupNo: 1,
|
||||||
@@ -272,7 +276,6 @@ export default {
|
|||||||
await executeSendJob(jobId)
|
await executeSendJob(jobId)
|
||||||
this.$message.success('Send success')
|
this.$message.success('Send success')
|
||||||
|
|
||||||
// Refresh last success time / 发送成功后刷新上次成功记录与时间
|
|
||||||
await this.reload()
|
await this.reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -286,36 +289,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-title {
|
.page-title { margin-bottom: 20px; }
|
||||||
margin-bottom: 20px;
|
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
}
|
.card-grid-container { min-height: 300px; }
|
||||||
.toolbar {
|
.card-col { margin-bottom: 20px; }
|
||||||
margin-bottom: 20px;
|
.parameter-card .card-header { display:flex; justify-content:space-between; align-items:center; }
|
||||||
}
|
.card-title { font-weight: 600; }
|
||||||
.card-grid-container {
|
.header-right { display:flex; align-items:center; }
|
||||||
min-height: 300px;
|
.last-send-time { font-size: 12px; color:#909399; margin-right:16px; }
|
||||||
}
|
.empty-data { margin-top: 20px; }
|
||||||
.card-col {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.parameter-card .card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.last-send-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
.empty-data {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- Page Title / 页面标题 -->
|
|
||||||
<h2 class="page-title">Furnace Parameter Sending</h2>
|
|
||||||
|
|
||||||
<!-- Toolbar / 工具栏 -->
|
<!-- Toolbar / 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
||||||
@@ -65,22 +62,20 @@
|
|||||||
|
|
||||||
<!-- Template-driven editable form / 按模板渲染可编辑表单 -->
|
<!-- Template-driven editable form / 按模板渲染可编辑表单 -->
|
||||||
<el-form :model="form" label-position="top" size="mini">
|
<el-form :model="form" label-position="top" size="mini">
|
||||||
|
<!-- Three inputs per row / 每行三个输入框 -->
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col
|
<el-col
|
||||||
:span="12"
|
:span="8"
|
||||||
v-for="item in templateItems"
|
v-for="item in templateItems"
|
||||||
:key="item.templateItemId || item.paramCode"
|
:key="item.templateItemId || item.paramCode"
|
||||||
>
|
>
|
||||||
<el-form-item :label="item.labelEn">
|
<el-form-item :label="item.labelEn">
|
||||||
<!-- Value input / 值输入框 -->
|
<el-input
|
||||||
<el-input v-model="form[item.paramCode]" :placeholder="getPlaceholder(item)">
|
v-model="form[item.paramCode]"
|
||||||
<template v-if="editTemplate" slot="append">
|
:placeholder="getPlaceholder(item)"
|
||||||
<!-- Address edit button / 点位编辑按钮 -->
|
/>
|
||||||
<el-button type="text" @click="openEditAddress(item)">Addr</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<!-- Inline address editor in edit mode / 编辑模式下显示当前点位 -->
|
<!-- Inline address editor / 编辑点位 -->
|
||||||
<div v-if="editTemplate" class="addr-inline">
|
<div v-if="editTemplate" class="addr-inline">
|
||||||
<span class="addr-label">Address:</span>
|
<span class="addr-label">Address:</span>
|
||||||
<el-input v-model="item.address" size="mini" placeholder="ns=2;s=..." />
|
<el-input v-model="item.address" size="mini" placeholder="ns=2;s=..." />
|
||||||
@@ -95,22 +90,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit Address Dialog / 编辑点位弹窗 -->
|
|
||||||
<el-dialog title="Edit OPC Address" :visible.sync="addressDialogVisible" width="650px" append-to-body>
|
|
||||||
<div v-if="editingItem">
|
|
||||||
<div style="margin-bottom: 8px; font-weight: 600;">{{ editingItem.labelEn }}</div>
|
|
||||||
<el-input v-model="editingItem.address" placeholder="ns=2;s=..." />
|
|
||||||
<div style="margin-top: 8px; color: #909399; font-size: 12px;">
|
|
||||||
<!-- 中文注释:这里修改的是模板点位地址,保存后会同步到数据库 -->
|
|
||||||
修改的是模板点位地址,点击 "Save Template" 后同步到数据库。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div slot="footer" class="dialog-footer">
|
|
||||||
<el-button @click="addressDialogVisible=false">Cancel</el-button>
|
|
||||||
<el-button type="primary" @click="addressDialogVisible=false">OK</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -130,10 +109,7 @@ export default {
|
|||||||
|
|
||||||
template: null, // Template / 模板
|
template: null, // Template / 模板
|
||||||
lastSuccess: null, // Last success / 上次成功
|
lastSuccess: null, // Last success / 上次成功
|
||||||
form: {}, // Form values / 表单值
|
form: {} // Form values / 表单值
|
||||||
|
|
||||||
addressDialogVisible: false,
|
|
||||||
editingItem: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -210,11 +186,6 @@ export default {
|
|||||||
return new Date(t).toLocaleString()
|
return new Date(t).toLocaleString()
|
||||||
},
|
},
|
||||||
|
|
||||||
openEditAddress(item) {
|
|
||||||
this.editingItem = item
|
|
||||||
this.addressDialogVisible = true
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveTemplate() {
|
async saveTemplate() {
|
||||||
if (!this.template || !this.template.templateId) {
|
if (!this.template || !this.template.templateId) {
|
||||||
this.$message.error('Template not loaded')
|
this.$message.error('Template not loaded')
|
||||||
@@ -223,7 +194,6 @@ export default {
|
|||||||
|
|
||||||
this.savingTemplate = true
|
this.savingTemplate = true
|
||||||
try {
|
try {
|
||||||
// 1) update template main (deviceName etc.) / 更新模板主表
|
|
||||||
await updateSendTemplate({
|
await updateSendTemplate({
|
||||||
templateId: this.template.templateId,
|
templateId: this.template.templateId,
|
||||||
deviceName: this.template.deviceName,
|
deviceName: this.template.deviceName,
|
||||||
@@ -231,7 +201,6 @@ export default {
|
|||||||
remark: this.template.remark
|
remark: this.template.remark
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2) batch update template items (address/default value) / 批量更新模板明细
|
|
||||||
const itemsPayload = this.templateItems.map(it => ({
|
const itemsPayload = this.templateItems.map(it => ({
|
||||||
templateItemId: it.templateItemId,
|
templateItemId: it.templateItemId,
|
||||||
address: it.address,
|
address: it.address,
|
||||||
@@ -318,46 +287,14 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-title {
|
.page-title { margin-bottom: 20px; }
|
||||||
margin-bottom: 20px;
|
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
}
|
.card-grid-container { min-height: 300px; }
|
||||||
.toolbar {
|
.parameter-card .card-header { display:flex; justify-content:space-between; align-items:center; }
|
||||||
margin-bottom: 20px;
|
.card-title { font-weight: 600; }
|
||||||
display: flex;
|
.header-right { display:flex; align-items:center; }
|
||||||
align-items: center;
|
.last-send-time { font-size: 12px; color:#909399; margin-right:16px; }
|
||||||
flex-wrap: wrap;
|
.empty-data { margin-top: 20px; }
|
||||||
gap: 8px;
|
.addr-inline { margin-top: 6px; }
|
||||||
}
|
.addr-label { display:inline-block; margin-right:6px; color:#909399; font-size:12px; }
|
||||||
.card-grid-container {
|
|
||||||
min-height: 300px;
|
|
||||||
}
|
|
||||||
.parameter-card .card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.last-send-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
.empty-data {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.addr-inline {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
.addr-label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
141
src/views/l2/send/history.vue
Normal file
141
src/views/l2/send/history.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Query / 查询 -->
|
||||||
|
<el-form :model="query" inline size="small" class="toolbar">
|
||||||
|
<el-form-item label="Device">
|
||||||
|
<el-input v-model="query.deviceName" placeholder="Device name" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Status">
|
||||||
|
<el-select v-model="query.status" placeholder="Status" clearable>
|
||||||
|
<el-option label="PENDING" value="PENDING" />
|
||||||
|
<el-option label="IN_PROGRESS" value="IN_PROGRESS" />
|
||||||
|
<el-option label="COMPLETED" value="COMPLETED" />
|
||||||
|
<el-option label="PARTIAL_SUCCESS" value="PARTIAL_SUCCESS" />
|
||||||
|
<el-option label="FAILED" value="FAILED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleQuery">Search</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" @click="resetQuery">Reset</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- Table / 表格 -->
|
||||||
|
<el-table v-loading="loading" :data="list" border>
|
||||||
|
<el-table-column label="Job ID" prop="jobId" width="90" />
|
||||||
|
<el-table-column label="Biz Key" prop="bizKey" width="220" />
|
||||||
|
<el-table-column label="Device" prop="deviceName" width="140" />
|
||||||
|
<el-table-column label="Status" prop="status" width="140" />
|
||||||
|
<el-table-column label="Create Time" prop="createTime" width="170" />
|
||||||
|
<el-table-column label="Finish Time" prop="finishTime" width="170" />
|
||||||
|
<el-table-column label="Remark" prop="remark" min-width="160" />
|
||||||
|
<el-table-column label="Action" width="120" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="openDetail(scope.row.jobId)">Detail</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
:page.sync="query.pageNum"
|
||||||
|
:limit.sync="query.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Detail dialog / 详情弹窗 -->
|
||||||
|
<el-dialog title="Send Detail" :visible.sync="detailVisible" width="90%" append-to-body>
|
||||||
|
<div v-if="detail">
|
||||||
|
<el-descriptions :column="3" border size="small">
|
||||||
|
<el-descriptions-item label="Job ID">{{ detail.jobId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Device">{{ detail.deviceName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Status">{{ detail.status }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Create Time">{{ detail.createTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Finish Time">{{ detail.finishTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Remark">{{ detail.remark }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<el-tabs type="border-card">
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="(g, idx) in (detail.groups || [])"
|
||||||
|
:key="g.groupId || idx"
|
||||||
|
:label="(g.groupName || g.groupType || ('Group ' + (idx+1)))"
|
||||||
|
>
|
||||||
|
<el-table :data="g.items || []" border size="small">
|
||||||
|
<el-table-column label="Param" prop="paramCode" width="180" />
|
||||||
|
<el-table-column label="Address" prop="address" min-width="320" />
|
||||||
|
<el-table-column label="Value" prop="valueRaw" width="160" />
|
||||||
|
<el-table-column label="Result" prop="resultStatus" width="120" />
|
||||||
|
<el-table-column label="Message" prop="resultMsg" min-width="180" />
|
||||||
|
<el-table-column label="Update Time" prop="updateTime" width="170" />
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="detailVisible=false">Close</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listSendJob, getSendJob } from '@/api/l2/sendJob'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SendHistory',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
total: 0,
|
||||||
|
query: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deviceName: '',
|
||||||
|
status: ''
|
||||||
|
},
|
||||||
|
detailVisible: false,
|
||||||
|
detail: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleQuery() {
|
||||||
|
this.query.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.query = { pageNum: 1, pageSize: 10, deviceName: '', status: '' }
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
async getList() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
// RuoYi 列表一般返回 {rows,total,code}
|
||||||
|
const res = await listSendJob(this.query)
|
||||||
|
this.list = res.rows || []
|
||||||
|
this.total = res.total || 0
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openDetail(jobId) {
|
||||||
|
const res = await getSendJob(jobId)
|
||||||
|
this.detail = res.data
|
||||||
|
this.detailVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-title { margin-bottom: 20px; }
|
||||||
|
.toolbar { margin-bottom: 16px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
|
</style>
|
||||||
|
|
||||||
114
src/views/l2/track/components/LatestSetValues.vue
Normal file
114
src/views/l2/track/components/LatestSetValues.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div class="lsv-root">
|
||||||
|
<div class="lsv-meta">
|
||||||
|
<div class="lsv-meta-row">
|
||||||
|
<span class="lsv-meta-label">Drive Last Sent:</span>
|
||||||
|
<span class="lsv-meta-value">{{ formatTime(driveData && driveData.lastSendTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lsv-meta-row">
|
||||||
|
<span class="lsv-meta-label">Furnace Last Sent:</span>
|
||||||
|
<span class="lsv-meta-value">{{ formatTime(furnaceData && furnaceData.lastSendTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tabs v-model="active" type="border-card" class="lsv-tabs">
|
||||||
|
<el-tab-pane label="Drive" name="drive">
|
||||||
|
<div class="lsv-list">
|
||||||
|
<div v-if="!driveList.length" class="lsv-empty">-</div>
|
||||||
|
<div v-for="it in driveList" :key="it.key" class="lsv-item">
|
||||||
|
<span class="lsv-key">{{ it.key }}</span>
|
||||||
|
<span class="lsv-val">{{ it.val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Furnace" name="furnace">
|
||||||
|
<div class="lsv-list">
|
||||||
|
<div v-if="!furnaceList.length" class="lsv-empty">-</div>
|
||||||
|
<div v-for="it in furnaceList" :key="it.key" class="lsv-item">
|
||||||
|
<span class="lsv-key">{{ it.key }}</span>
|
||||||
|
<span class="lsv-val">{{ it.val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LatestSetValues',
|
||||||
|
props: {
|
||||||
|
driveData: { type: Object, default: null },
|
||||||
|
furnaceData: { type: Object, default: null }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: 'drive'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
driveList() {
|
||||||
|
const values = this.driveData && this.driveData.values ? this.driveData.values : {}
|
||||||
|
return Object.keys(values).sort().map(k => ({ key: k, val: values[k] === '' || values[k] == null ? '-' : String(values[k]) }))
|
||||||
|
},
|
||||||
|
furnaceList() {
|
||||||
|
const values = this.furnaceData && this.furnaceData.values ? this.furnaceData.values : {}
|
||||||
|
return Object.keys(values).sort().map(k => ({ key: k, val: values[k] === '' || values[k] == null ? '-' : String(values[k]) }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatTime(t) {
|
||||||
|
if (!t) return '-'
|
||||||
|
return new Date(t).toLocaleString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lsv-meta {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.lsv-meta-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.lsv-meta-label { color: #909399; }
|
||||||
|
.lsv-meta-value { color: #303133; font-weight: 600; }
|
||||||
|
|
||||||
|
.lsv-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.lsv-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #e8f4ff; /* 设定值背景色 */
|
||||||
|
border: 1px solid #c6e2ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.lsv-key {
|
||||||
|
color: #303133;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.lsv-val {
|
||||||
|
color: #1f78d1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.lsv-empty {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user