Merge branch 'master' of http://49.232.154.205:10100/DeXun/l2-g30
This commit is contained in:
36
src/api/l2/sendJob.js
Normal file
36
src/api/l2/sendJob.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import request from '@/utils/L2Request'
|
||||||
|
|
||||||
|
// 创建发送任务
|
||||||
|
export function createSendJob(data) {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendJob',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行发送任务
|
||||||
|
export function executeSendJob(jobId) {
|
||||||
|
return request({
|
||||||
|
url: `/business/sendJob/${jobId}/execute`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询发送任务列表(分页)
|
||||||
|
export function listSendJob(query) {
|
||||||
|
// 若你后端是 GET /business/sendJob/list(现状),这里用 params
|
||||||
|
return request({
|
||||||
|
url: '/business/sendJob/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取发送任务详情
|
||||||
|
export function getSendJob(jobId) {
|
||||||
|
return request({
|
||||||
|
url: `/business/sendJob/${jobId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
45
src/api/l2/sendTemplate.js
Normal file
45
src/api/l2/sendTemplate.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import request from '@/utils/L2Request'
|
||||||
|
|
||||||
|
// 获取发送模板(含明细)
|
||||||
|
export function getSendTemplate(templateCode) {
|
||||||
|
return request({
|
||||||
|
url: `/business/sendTemplate/${templateCode}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近一次成功发送(用于推荐值 + 上次发送时间显示)
|
||||||
|
export function getLastSuccess(groupType) {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendJob/lastSuccess',
|
||||||
|
method: 'get',
|
||||||
|
params: { groupType }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新模板主表(deviceName 等)
|
||||||
|
export function updateSendTemplate(data) {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendTemplate',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新模板明细(address/defaultValueRaw/enabled 等)
|
||||||
|
export function updateSendTemplateItems(data) {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendTemplate/items',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存模板明细(新增/更新/删除)- 仅提交变更,避免请求体过大
|
||||||
|
export function batchSaveSendTemplateItems(data) {
|
||||||
|
return request({
|
||||||
|
url: '/business/sendTemplate/items/batchSave',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
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' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
13
src/api/l2/template.js
Normal file
13
src/api/l2/template.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import request from '@/utils/l2-request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按模板编码查询发送模板(含明细)
|
||||||
|
* @param {string} templateCode 模板编码,如 FURNACE_DEFAULT
|
||||||
|
*/
|
||||||
|
export function getSendTemplate(templateCode) {
|
||||||
|
return request({
|
||||||
|
url: `/business/sendTemplate/${templateCode}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
<div class="title-text">
|
<div class="title-text">
|
||||||
<span>Performance Report</span>
|
<span>Performance Report</span>
|
||||||
<!-- 实绩报表 -->
|
<!-- 实绩报表 -->
|
||||||
<small>Please select time range to view data</small>
|
<small>Displaying data for the last month by default</small>
|
||||||
<!-- 请选择时间范围后查看数据 -->
|
<!-- 默认展示近一个月数据 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" effect="plain">Performance</el-tag>
|
<el-tag size="small" effect="plain">Performance</el-tag>
|
||||||
@@ -70,14 +70,14 @@
|
|||||||
<!-- 时间范围 -->
|
<!-- 时间范围 -->
|
||||||
<span class="info-range">{{ displayTimeRange }}</span>
|
<span class="info-range">{{ displayTimeRange }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Reselect Time</el-button>
|
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Change Time Range</el-button>
|
||||||
<!-- 重新选择时间 -->
|
<!-- 重新选择时间 -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 实绩报表 -->
|
||||||
<ReportBody
|
<ReportBody
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
title="Performance Report"
|
title="Performance Report"
|
||||||
<!-- 实绩报表 -->
|
|
||||||
:summary="reportSummary"
|
:summary="reportSummary"
|
||||||
:dataset="reportDetail"
|
:dataset="reportDetail"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
hasSelectedTime: false,
|
hasSelectedTime: true, // Directly show report content
|
||||||
timeRange: {
|
timeRange: {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: ''
|
endTime: ''
|
||||||
@@ -123,7 +123,31 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.setDefaultTimeRangeAndFetch()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setDefaultTimeRangeAndFetch() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
|
||||||
|
this.timeRange.endTime = this.formatDate(end)
|
||||||
|
this.timeRange.startTime = this.formatDate(start)
|
||||||
|
|
||||||
|
this.fetchReportData()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||||
|
const seconds = date.getSeconds().toString().padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
},
|
||||||
|
|
||||||
handleTimeReset() {
|
handleTimeReset() {
|
||||||
this.timeRange = {
|
this.timeRange = {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
@@ -146,9 +170,7 @@ export default {
|
|||||||
},
|
},
|
||||||
handleReturn() {
|
handleReturn() {
|
||||||
this.hasSelectedTime = false
|
this.hasSelectedTime = false
|
||||||
this.handleTimeReset()
|
// Do not reset time, so user can see the previous selection
|
||||||
this.reportSummary = []
|
|
||||||
this.reportDetail = []
|
|
||||||
},
|
},
|
||||||
async fetchReportData() {
|
async fetchReportData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -157,7 +179,7 @@ export default {
|
|||||||
startTime: this.timeRange.startTime,
|
startTime: this.timeRange.startTime,
|
||||||
endTime: this.timeRange.endTime,
|
endTime: this.timeRange.endTime,
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 1000
|
pageSize: 1000 // Assuming we fetch all data for the report
|
||||||
}
|
}
|
||||||
const res = await getReportSummary(queryParams)
|
const res = await getReportSummary(queryParams)
|
||||||
this.reportSummary = [
|
this.reportSummary = [
|
||||||
@@ -309,4 +331,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<div class="title-text">
|
<div class="title-text">
|
||||||
<span>Roll Change Report</span>
|
<span>Roll Change Report</span>
|
||||||
<!-- 换辊报表 -->
|
<!-- 换辊报表 -->
|
||||||
<small>Please select time range to view data</small>
|
<small>Displaying data for the last month by default</small>
|
||||||
<!-- 请选择时间范围后查看数据 -->
|
<!-- 默认展示近一个月数据 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" effect="plain">Roll Change</el-tag>
|
<el-tag size="small" effect="plain">Roll Change</el-tag>
|
||||||
@@ -23,8 +23,6 @@
|
|||||||
<div class="selector-content">
|
<div class="selector-content">
|
||||||
<el-form :inline="true" size="small">
|
<el-form :inline="true" size="small">
|
||||||
<el-form-item label="Start Time">
|
<el-form-item label="Start Time">
|
||||||
<!-- 开始时间 -->
|
|
||||||
<!-- Select start time / 选择开始时间 -->
|
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="timeRange.startTime"
|
v-model="timeRange.startTime"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
@@ -35,8 +33,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="End Time">
|
<el-form-item label="End Time">
|
||||||
<!-- 结束时间 -->
|
|
||||||
<!-- Select end time / 选择结束时间 -->
|
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="timeRange.endTime"
|
v-model="timeRange.endTime"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
@@ -50,9 +46,7 @@
|
|||||||
<el-button type="primary" :disabled="!canQuery" icon="el-icon-search" @click="handleTimeConfirm">
|
<el-button type="primary" :disabled="!canQuery" icon="el-icon-search" @click="handleTimeConfirm">
|
||||||
Query
|
Query
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- 查询 -->
|
|
||||||
<el-button icon="el-icon-refresh" @click="handleTimeReset">Reset</el-button>
|
<el-button icon="el-icon-refresh" @click="handleTimeReset">Reset</el-button>
|
||||||
<!-- 重置 -->
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,22 +56,17 @@
|
|||||||
<div class="content-toolbar">
|
<div class="content-toolbar">
|
||||||
<div class="toolbar-info">
|
<div class="toolbar-info">
|
||||||
<span class="info-label">Report Type:</span>
|
<span class="info-label">Report Type:</span>
|
||||||
<!-- 报表类型 -->
|
|
||||||
<span class="info-value">Roll Change Report</span>
|
<span class="info-value">Roll Change Report</span>
|
||||||
<!-- 换辊报表 -->
|
|
||||||
<span class="info-divider">|</span>
|
<span class="info-divider">|</span>
|
||||||
<span class="info-label">Time Range:</span>
|
<span class="info-label">Time Range:</span>
|
||||||
<!-- 时间范围 -->
|
|
||||||
<span class="info-range">{{ displayTimeRange }}</span>
|
<span class="info-range">{{ displayTimeRange }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Reselect Time</el-button>
|
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Change Time Range</el-button>
|
||||||
<!-- 重新选择时间 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ReportBody
|
<ReportBody
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
title="Roll Change Report"
|
title="Roll Change Report"
|
||||||
<!-- 换辊报表 -->
|
|
||||||
:summary="reportSummary"
|
:summary="reportSummary"
|
||||||
:dataset="reportDetail"
|
:dataset="reportDetail"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -97,7 +86,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
hasSelectedTime: false,
|
hasSelectedTime: true,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: ''
|
endTime: ''
|
||||||
@@ -114,16 +103,38 @@ export default {
|
|||||||
displayTimeRange() {
|
displayTimeRange() {
|
||||||
if (!this.canQuery) return '-'
|
if (!this.canQuery) return '-'
|
||||||
return `${this.timeRange.startTime} to ${this.timeRange.endTime}`
|
return `${this.timeRange.startTime} to ${this.timeRange.endTime}`
|
||||||
// 至
|
|
||||||
},
|
},
|
||||||
overviewInfo() {
|
overviewInfo() {
|
||||||
return {
|
return {
|
||||||
reportLabel: 'Roll Change Report', // 换辊报表
|
reportLabel: 'Roll Change Report',
|
||||||
rangeText: this.displayTimeRange
|
rangeText: this.displayTimeRange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.setDefaultTimeRangeAndFetch()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setDefaultTimeRangeAndFetch() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
|
||||||
|
this.timeRange.endTime = this.formatDateTime(end)
|
||||||
|
this.timeRange.startTime = this.formatDateTime(start)
|
||||||
|
|
||||||
|
this.fetchReportData()
|
||||||
|
},
|
||||||
|
formatDateTime(date) {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
},
|
||||||
|
|
||||||
handleTimeReset() {
|
handleTimeReset() {
|
||||||
this.timeRange = {
|
this.timeRange = {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
@@ -132,11 +143,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleTimeConfirm() {
|
async handleTimeConfirm() {
|
||||||
if (!this.canQuery) {
|
if (!this.canQuery) {
|
||||||
this.$message.warning('Please select complete time range') // 请选择完整的时间范围
|
this.$message.warning('Please select complete time range')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (new Date(this.timeRange.startTime) > new Date(this.timeRange.endTime)) {
|
if (new Date(this.timeRange.startTime) > new Date(this.timeRange.endTime)) {
|
||||||
this.$message.warning('Start time cannot be later than end time') // 开始时间不能晚于结束时间
|
this.$message.warning('Start time cannot be later than end time')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.hasSelectedTime = true
|
this.hasSelectedTime = true
|
||||||
@@ -144,9 +155,6 @@ export default {
|
|||||||
},
|
},
|
||||||
handleReturn() {
|
handleReturn() {
|
||||||
this.hasSelectedTime = false
|
this.hasSelectedTime = false
|
||||||
this.handleTimeReset()
|
|
||||||
this.reportSummary = []
|
|
||||||
this.reportDetail = []
|
|
||||||
},
|
},
|
||||||
async fetchReportData() {
|
async fetchReportData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -162,26 +170,26 @@ export default {
|
|||||||
...item
|
...item
|
||||||
}))
|
}))
|
||||||
this.columns = [
|
this.columns = [
|
||||||
{ label: 'Change ID', prop: 'changeid' }, // 换辊号
|
{ label: 'Change ID', prop: 'changeid' },
|
||||||
{ label: 'Roll ID', prop: 'rollid' }, // 轧辊号
|
{ label: 'Roll ID', prop: 'rollid' },
|
||||||
{ label: 'Type', prop: 'type' }, // 类型
|
{ label: 'Type', prop: 'type' },
|
||||||
{ label: 'Position', prop: 'position' }, // 位置
|
{ label: 'Position', prop: 'position' },
|
||||||
{ label: 'Diameter', prop: 'diameter' }, // 直径
|
{ label: 'Diameter', prop: 'diameter' },
|
||||||
{ label: 'Roughness', prop: 'rough' }, // 粗糙度
|
{ label: 'Roughness', prop: 'rough' },
|
||||||
{ label: 'Crown', prop: 'crown' }, // 凸度
|
{ label: 'Crown', prop: 'crown' },
|
||||||
{ label: 'Rolled Length', prop: 'rolledLength' }, // 轧制长度
|
{ label: 'Rolled Length', prop: 'rolledLength' },
|
||||||
{ label: 'Rolled Weight', prop: 'rolledWeight' }, // 轧制重量
|
{ label: 'Rolled Weight', prop: 'rolledWeight' },
|
||||||
{ label: 'Rolled Count', prop: 'rolledCount' }, // 轧制次数
|
{ label: 'Rolled Count', prop: 'rolledCount' },
|
||||||
{ label: 'Install Time', prop: 'instalTime' }, // 上线时间
|
{ label: 'Install Time', prop: 'instalTime' },
|
||||||
{ label: 'Uninstall Time', prop: 'deinstalTime' } // 下线时间
|
{ label: 'Uninstall Time', prop: 'deinstalTime' }
|
||||||
]
|
]
|
||||||
this.reportSummary = [
|
this.reportSummary = [
|
||||||
{ label: 'Data Count', value: this.reportDetail.length }, // 数据条数
|
{ label: 'Data Count', value: this.reportDetail.length },
|
||||||
{ label: 'Time Range', value: this.displayTimeRange } // 时间范围
|
{ label: 'Time Range', value: this.displayTimeRange }
|
||||||
]
|
]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
this.$message.error('Failed to fetch report data') // 获取报表数据失败
|
this.$message.error('Failed to fetch report data')
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -300,4 +308,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<div class="title-text">
|
<div class="title-text">
|
||||||
<span>Stoppage Report</span>
|
<span>Stoppage Report</span>
|
||||||
<!-- 停机报表 -->
|
<!-- 停机报表 -->
|
||||||
<small>Please select time range to view data</small>
|
<small>Displaying data for the last month by default</small>
|
||||||
<!-- 请选择时间范围后查看数据 -->
|
<!-- 默认展示近一个月数据 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" effect="plain">Stoppage</el-tag>
|
<el-tag size="small" effect="plain">Stoppage</el-tag>
|
||||||
@@ -18,13 +18,10 @@
|
|||||||
<div class="selector-header">
|
<div class="selector-header">
|
||||||
<i class="el-icon-calendar"></i>
|
<i class="el-icon-calendar"></i>
|
||||||
<span>Please select query date range</span>
|
<span>Please select query date range</span>
|
||||||
<!-- 请选择查询日期范围 -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="selector-content">
|
<div class="selector-content">
|
||||||
<el-form :inline="true" size="small">
|
<el-form :inline="true" size="small">
|
||||||
<el-form-item label="Start Date">
|
<el-form-item label="Start Date">
|
||||||
<!-- 开始日期 -->
|
|
||||||
<!-- Select start date / 选择开始日期 -->
|
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="timeRange.startTime"
|
v-model="timeRange.startTime"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -35,8 +32,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="End Date">
|
<el-form-item label="End Date">
|
||||||
<!-- 结束日期 -->
|
|
||||||
<!-- Select end date / 选择结束日期 -->
|
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="timeRange.endTime"
|
v-model="timeRange.endTime"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -50,9 +45,7 @@
|
|||||||
<el-button type="primary" :disabled="!canQuery" icon="el-icon-search" @click="handleTimeConfirm">
|
<el-button type="primary" :disabled="!canQuery" icon="el-icon-search" @click="handleTimeConfirm">
|
||||||
Query
|
Query
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- 查询 -->
|
|
||||||
<el-button icon="el-icon-refresh" @click="handleTimeReset">Reset</el-button>
|
<el-button icon="el-icon-refresh" @click="handleTimeReset">Reset</el-button>
|
||||||
<!-- 重置 -->
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,22 +55,17 @@
|
|||||||
<div class="content-toolbar">
|
<div class="content-toolbar">
|
||||||
<div class="toolbar-info">
|
<div class="toolbar-info">
|
||||||
<span class="info-label">Report Type:</span>
|
<span class="info-label">Report Type:</span>
|
||||||
<!-- 报表类型 -->
|
|
||||||
<span class="info-value">Stoppage Report</span>
|
<span class="info-value">Stoppage Report</span>
|
||||||
<!-- 停机报表 -->
|
|
||||||
<span class="info-divider">|</span>
|
<span class="info-divider">|</span>
|
||||||
<span class="info-label">Time Range:</span>
|
<span class="info-label">Time Range:</span>
|
||||||
<!-- 时间范围 -->
|
|
||||||
<span class="info-range">{{ displayTimeRange }}</span>
|
<span class="info-range">{{ displayTimeRange }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Reselect Time</el-button>
|
<el-button type="text" icon="el-icon-refresh-left" @click="handleReturn">Change Date Range</el-button>
|
||||||
<!-- 重新选择时间 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ReportBody
|
<ReportBody
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
title="Stoppage Report"
|
title="Stoppage Report"
|
||||||
<!-- 停机报表 -->
|
|
||||||
:summary="reportSummary"
|
:summary="reportSummary"
|
||||||
:dataset="reportDetail"
|
:dataset="reportDetail"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -97,7 +85,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
hasSelectedTime: false,
|
hasSelectedTime: true,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: ''
|
endTime: ''
|
||||||
@@ -105,14 +93,14 @@ export default {
|
|||||||
reportSummary: [],
|
reportSummary: [],
|
||||||
reportDetail: [],
|
reportDetail: [],
|
||||||
columns: [
|
columns: [
|
||||||
{ label: 'Coil ID', prop: 'coilid' }, // 钢卷号
|
{ label: 'Coil ID', prop: 'coilid' },
|
||||||
{ label: 'Shift', prop: 'shift' }, // 班次号
|
{ label: 'Shift', prop: 'shift' },
|
||||||
{ label: 'Group', prop: 'area' }, // 组
|
{ label: 'Group', prop: 'area' },
|
||||||
{ label: 'Start Time', prop: 'startDate' }, // 开始时间
|
{ label: 'Start Time', prop: 'startDate' },
|
||||||
{ label: 'End Time', prop: 'endDate' }, // 结束时间
|
{ label: 'End Time', prop: 'endDate' },
|
||||||
{ label: 'Duration [min]', prop: 'duration' }, // 持续时间[分钟]
|
{ label: 'Duration [min]', prop: 'duration' },
|
||||||
{ label: 'Stoppage Type', prop: 'unit' }, // 停机类型
|
{ label: 'Stoppage Type', prop: 'unit' },
|
||||||
{ label: 'Remark', prop: 'remark' } // 备注
|
{ label: 'Remark', prop: 'remark' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -123,16 +111,35 @@ export default {
|
|||||||
displayTimeRange() {
|
displayTimeRange() {
|
||||||
if (!this.canQuery) return '-'
|
if (!this.canQuery) return '-'
|
||||||
return `${this.timeRange.startTime} to ${this.timeRange.endTime}`
|
return `${this.timeRange.startTime} to ${this.timeRange.endTime}`
|
||||||
// 至
|
|
||||||
},
|
},
|
||||||
overviewInfo() {
|
overviewInfo() {
|
||||||
return {
|
return {
|
||||||
reportLabel: 'Stoppage Report', // 停机报表
|
reportLabel: 'Stoppage Report',
|
||||||
rangeText: this.displayTimeRange
|
rangeText: this.displayTimeRange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.setDefaultDateRangeAndFetch()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setDefaultDateRangeAndFetch() {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setMonth(start.getMonth() - 1)
|
||||||
|
|
||||||
|
this.timeRange.endTime = this.formatDate(end)
|
||||||
|
this.timeRange.startTime = this.formatDate(start)
|
||||||
|
|
||||||
|
this.fetchReportData()
|
||||||
|
},
|
||||||
|
formatDate(date) {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
},
|
||||||
|
|
||||||
handleTimeReset() {
|
handleTimeReset() {
|
||||||
this.timeRange = {
|
this.timeRange = {
|
||||||
startTime: '',
|
startTime: '',
|
||||||
@@ -141,11 +148,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleTimeConfirm() {
|
async handleTimeConfirm() {
|
||||||
if (!this.canQuery) {
|
if (!this.canQuery) {
|
||||||
this.$message.warning('Please select complete date range') // 请选择完整的日期范围
|
this.$message.warning('Please select complete date range')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (new Date(this.timeRange.startTime) > new Date(this.timeRange.endTime)) {
|
if (new Date(this.timeRange.startTime) > new Date(this.timeRange.endTime)) {
|
||||||
this.$message.warning('Start date cannot be later than end date') // 开始日期不能晚于结束日期
|
this.$message.warning('Start date cannot be later than end date')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.hasSelectedTime = true
|
this.hasSelectedTime = true
|
||||||
@@ -153,9 +160,6 @@ export default {
|
|||||||
},
|
},
|
||||||
handleReturn() {
|
handleReturn() {
|
||||||
this.hasSelectedTime = false
|
this.hasSelectedTime = false
|
||||||
this.handleTimeReset()
|
|
||||||
this.reportSummary = []
|
|
||||||
this.reportDetail = []
|
|
||||||
},
|
},
|
||||||
async fetchReportData() {
|
async fetchReportData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -171,14 +175,14 @@ export default {
|
|||||||
|
|
||||||
const res2 = await getStoppageSummary(queryParams)
|
const res2 = await getStoppageSummary(queryParams)
|
||||||
this.reportSummary = [
|
this.reportSummary = [
|
||||||
{ label: 'Statistics Range', value: this.displayTimeRange }, // 统计区间
|
{ label: 'Statistics Range', value: this.displayTimeRange },
|
||||||
{ label: 'Stoppage Count', value: res2.data?.[0] || 0 }, // 停机次数
|
{ label: 'Stoppage Count', value: res2.data?.[0] || 0 },
|
||||||
{ label: 'Stoppage Duration [hours]', value: res2.data?.[1] || 0 }, // 停机时长[小时]
|
{ label: 'Stoppage Duration [hours]', value: res2.data?.[1] || 0 },
|
||||||
{ label: 'Operation Rate', value: (res2.data?.[2] || 0) + ' %' } // 作业率
|
{ label: 'Operation Rate', value: (res2.data?.[2] || 0) + ' %' }
|
||||||
]
|
]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
this.$message.error('Failed to fetch report data') // 获取报表数据失败
|
this.$message.error('Failed to fetch report data')
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -257,7 +261,7 @@ export default {
|
|||||||
|
|
||||||
.selector-content {
|
.selector-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 640px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,4 +301,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
138
src/views/l2/send/components/FurnaceHistoryPanel.vue
Normal file
138
src/views/l2/send/components/FurnaceHistoryPanel.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fh-root">
|
||||||
|
<!-- Query -->
|
||||||
|
<el-form :model="query" inline size="mini" class="fh-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 style="width: 160px">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" border size="mini" height="380">
|
||||||
|
<el-table-column label="Job ID" prop="jobId" width="90" />
|
||||||
|
<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="Action" width="150" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="apply(scope.row)">Apply</el-button>
|
||||||
|
<el-button type="text" size="mini" @click="openDetail(scope.row.jobId)">Detail</el-button>
|
||||||
|
</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-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: 'FurnaceHistoryPanel',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
total: 0,
|
||||||
|
query: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deviceName: '',
|
||||||
|
status: 'COMPLETED',
|
||||||
|
groupType: 'FURNACE'
|
||||||
|
},
|
||||||
|
detailVisible: false,
|
||||||
|
detail: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleQuery() {
|
||||||
|
this.query.pageNum = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.query = { pageNum: 1, pageSize: 10, deviceName: '', status: 'COMPLETED', groupType: 'FURNACE' }
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
async getList() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
async apply(row) {
|
||||||
|
const res = await getSendJob(row.jobId)
|
||||||
|
const detail = res.data
|
||||||
|
const values = {}
|
||||||
|
;(detail.groups || []).forEach(g => {
|
||||||
|
if ((g.groupType || '').toUpperCase() !== 'FURNACE') return
|
||||||
|
;(g.items || []).forEach(it => {
|
||||||
|
if (it.paramCode) values[it.paramCode] = it.valueRaw
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.$emit('apply', { jobId: row.jobId, values })
|
||||||
|
this.$message.success('Values applied')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fh-toolbar { margin-bottom: 10px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
|
</style>
|
||||||
|
|
||||||
316
src/views/l2/send/drive.vue
Normal file
316
src/views/l2/send/drive.vue
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Toolbar / 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
||||||
|
Refresh
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="lastSuccess && lastSuccess.lastSendTime"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-magic-stick"
|
||||||
|
size="small"
|
||||||
|
@click="applyLastSuccessValues"
|
||||||
|
>
|
||||||
|
Apply Last Success Values
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards / 卡片 -->
|
||||||
|
<div v-loading="loading" class="card-grid-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col
|
||||||
|
v-for="setup in setups"
|
||||||
|
:key="setup.ID"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:md="8"
|
||||||
|
class="card-col"
|
||||||
|
>
|
||||||
|
<el-card class="parameter-card" shadow="hover">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<div class="card-header-content">
|
||||||
|
<!-- 头部信息:参考 setup/panels 卡片表头“多字段拼接”的风格 -->
|
||||||
|
<div class="card-title-row">
|
||||||
|
<span class="card-title">
|
||||||
|
Plan ID: {{ setup.planid || '-' }}
|
||||||
|
| Coil ID: {{ setup.coilid || '-' }}
|
||||||
|
| Steel Grade: {{ setup.steelGrade || setup.grade || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-subtitle">
|
||||||
|
<span>Entry Thickness: {{ setup.entryThick || '-' }}</span>
|
||||||
|
<span>Entry Width: {{ setup.entryWidth || '-' }}</span>
|
||||||
|
<span>Entry Weight: {{ setup.entryWeight || '-' }}</span>
|
||||||
|
<span>Entry Length: {{ setup.entryLength || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-subtitle">
|
||||||
|
<span>TL Elongation: {{ setup.tlElong || '-' }}</span>
|
||||||
|
<span>TM Roll Force: {{ setup.tmRollforce || '-' }}</span>
|
||||||
|
<span>TM Bending Force: {{ setup.tmBendforce || '-' }}</span>
|
||||||
|
<span v-if="setup.updateTime">Updated: {{ formatTime(setup.updateTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-s-promotion"
|
||||||
|
@click="handleSend(setup)"
|
||||||
|
:loading="setup.sending"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- 可编辑表单 -->
|
||||||
|
<el-form :model="setup.params" label-position="top" size="mini">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col
|
||||||
|
v-for="item in driveFields"
|
||||||
|
:key="item.key"
|
||||||
|
:span="12"
|
||||||
|
>
|
||||||
|
<el-form-item :label="item.label">
|
||||||
|
<el-input
|
||||||
|
v-model="setup.params[item.key]"
|
||||||
|
:placeholder="getPlaceholder(item.key)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div v-if="setups.length === 0 && !loading" class="empty-data">
|
||||||
|
<el-empty description="No Setup History Data"></el-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Import APIs / 引入接口
|
||||||
|
import { listSetup } from '@/api/business/setup'
|
||||||
|
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
|
||||||
|
import { getLastSuccess } from '@/api/l2/sendTemplate'
|
||||||
|
|
||||||
|
// Drive fields definition (English UI, Chinese comments) / 传动字段定义(英文界面,中文注释)
|
||||||
|
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 / OPC点位映射
|
||||||
|
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 {
|
||||||
|
name: 'DriveSend',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
lastSuccess: null,
|
||||||
|
setups: [], // Changed from 'plans' to 'setups'
|
||||||
|
driveFields: DRIVE_FIELDS,
|
||||||
|
driveAddress: { ...DRIVE_ADDRESS }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async reload() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
// 1. Get last success for DRIVE
|
||||||
|
const lastRes = await getLastSuccess('DRIVE')
|
||||||
|
this.lastSuccess = lastRes && lastRes.code === 200 ? lastRes.data : null
|
||||||
|
|
||||||
|
// 2. Get setup history list (instead of plans)
|
||||||
|
const setupRes = await listSetup({ pageNum: 1, pageSize: 20 }) // Fetch latest 20 for example
|
||||||
|
const setupList = (setupRes && setupRes.rows) || []
|
||||||
|
|
||||||
|
// 3. Map setup list to display data
|
||||||
|
this.setups = setupList.map(s => {
|
||||||
|
const params = {}
|
||||||
|
this.driveFields.forEach(f => {
|
||||||
|
const fromSetup = s ? s[f.key] : undefined
|
||||||
|
const fromLast = this.lastSuccess?.values?.[f.key]
|
||||||
|
|
||||||
|
// Priority: current setup value > last success value > ''
|
||||||
|
if (fromSetup !== undefined && fromSetup !== null && String(fromSetup) !== '') {
|
||||||
|
params[f.key] = String(fromSetup)
|
||||||
|
} else if (fromLast !== undefined && fromLast !== null) {
|
||||||
|
params[f.key] = String(fromLast)
|
||||||
|
} else {
|
||||||
|
params[f.key] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
params,
|
||||||
|
sending: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.$message.error('Load failed')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyLastSuccessValues() {
|
||||||
|
if (!this.lastSuccess || !this.lastSuccess.values) {
|
||||||
|
this.$message.info('No last success data')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setups.forEach(setup => {
|
||||||
|
this.driveFields.forEach(f => {
|
||||||
|
const v = this.lastSuccess.values[f.key]
|
||||||
|
if (v !== undefined) {
|
||||||
|
this.$set(setup.params, f.key, String(v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.$message.success('Last success values applied')
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaceholder(key) {
|
||||||
|
const v = this.lastSuccess?.values?.[key]
|
||||||
|
if (v !== undefined) return `Last: ${v}`
|
||||||
|
return 'Please enter'
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(t) {
|
||||||
|
if (!t) return ''
|
||||||
|
return new Date(t).toLocaleString()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSend(setup) {
|
||||||
|
this.$confirm(
|
||||||
|
`Confirm to send parameters for Coil [${setup.coilid || '-'}]?`,
|
||||||
|
'Warning',
|
||||||
|
{
|
||||||
|
confirmButtonText: 'Confirm',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(() => this.doSend(setup)).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
async doSend(setup) {
|
||||||
|
setup.sending = true
|
||||||
|
try {
|
||||||
|
const items = this.driveFields.map(f => ({
|
||||||
|
paramCode: f.key,
|
||||||
|
address: this.driveAddress[f.key], // OPC address can be empty
|
||||||
|
valueRaw: String(setup.params[f.key] || ''),
|
||||||
|
setTime: new Date()
|
||||||
|
})).filter(it => !!it.address) // Filter out items without an address
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
this.$message.warning('OPC addresses are not configured. Nothing to send.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto = {
|
||||||
|
deviceName: 'CGL_LINE_1',
|
||||||
|
bizKey: setup.coilid, // Use coilid as business key
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupNo: 1,
|
||||||
|
groupType: 'DRIVE',
|
||||||
|
groupName: `Drive Params for ${setup.coilid || ''}`,
|
||||||
|
items
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await createSendJob(dto)
|
||||||
|
const jobId = createRes.data
|
||||||
|
if (!jobId) throw new Error('Create send job failed')
|
||||||
|
|
||||||
|
await executeSendJob(jobId)
|
||||||
|
this.$message.success('Send success')
|
||||||
|
|
||||||
|
await this.reload()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.$message.error(e.message || 'Send failed')
|
||||||
|
} finally {
|
||||||
|
setup.sending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
|
.card-grid-container { min-height: 300px; }
|
||||||
|
.card-col { margin-bottom: 20px; }
|
||||||
|
.parameter-card .card-header { display:flex; justify-content:space-between; align-items:center; }
|
||||||
|
.card-header-content { flex-grow: 1; }
|
||||||
|
.card-title-row { margin-bottom: 4px; }
|
||||||
|
.card-title { font-weight: 600; font-size: 16px; }
|
||||||
|
.card-subtitle { font-size: 12px; color: #909399; display: flex; gap: 12px; }
|
||||||
|
.header-right { flex-shrink: 0; margin-left: 16px; }
|
||||||
|
.last-send-time { font-size: 12px; color:#909399; margin-right:16px; }
|
||||||
|
.empty-data { margin-top: 20px; }
|
||||||
|
</style>
|
||||||
543
src/views/l2/send/furnace.vue
Normal file
543
src/views/l2/send/furnace.vue
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Toolbar / 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
||||||
|
Refresh
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- Edit template switch / 编辑模板开关 -->
|
||||||
|
<el-switch
|
||||||
|
v-model="editTemplate"
|
||||||
|
active-text="Edit Template"
|
||||||
|
inactive-text="View"
|
||||||
|
style="margin: 0 12px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="editTemplate"
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="el-icon-check"
|
||||||
|
size="small"
|
||||||
|
:loading="savingTemplate"
|
||||||
|
@click="saveTemplate"
|
||||||
|
>
|
||||||
|
Save Template
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="lastSuccess && lastSuccess.lastSendTime"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-magic-stick"
|
||||||
|
size="small"
|
||||||
|
@click="applyLastSuccessValues"
|
||||||
|
>
|
||||||
|
Apply Last Success Values
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="el-icon-time"
|
||||||
|
size="small"
|
||||||
|
@click="openHistory"
|
||||||
|
>
|
||||||
|
History
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History Floating Panel -->
|
||||||
|
<FloatingPanel
|
||||||
|
ref="historyPanel"
|
||||||
|
title="History"
|
||||||
|
storageKey="FURNACE_SEND_HISTORY_PANEL"
|
||||||
|
:defaultW="980"
|
||||||
|
:defaultH="520"
|
||||||
|
:defaultX="30"
|
||||||
|
:defaultY="60"
|
||||||
|
>
|
||||||
|
<FurnaceHistoryPanel @apply="applyHistoryValues" />
|
||||||
|
</FloatingPanel>
|
||||||
|
|
||||||
|
<!-- Furnace Parameter Form / 炉火参数表单 -->
|
||||||
|
<div v-loading="loading" class="card-grid-container">
|
||||||
|
<el-card class="parameter-card" shadow="hover">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<span class="card-title">{{ template ? template.templateName : 'Furnace Settings' }}</span>
|
||||||
|
<div class="header-right">
|
||||||
|
<span v-if="lastSuccess && lastSuccess.lastSendTime" class="last-send-time">
|
||||||
|
<i class="el-icon-time"></i>
|
||||||
|
Last Sent: {{ formatTime(lastSuccess.lastSendTime) }}
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-s-promotion"
|
||||||
|
@click="handleSend"
|
||||||
|
:loading="sending"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template-driven editable form / 按模板渲染可编辑表单 -->
|
||||||
|
<el-form :model="form" label-position="top" size="mini">
|
||||||
|
<div class="group-list">
|
||||||
|
<div
|
||||||
|
v-for="group in groupedItems"
|
||||||
|
:key="group.groupKey"
|
||||||
|
class="group-section"
|
||||||
|
>
|
||||||
|
<div class="group-header">
|
||||||
|
<span class="group-title">{{ group.groupTitle }}</span>
|
||||||
|
<span class="group-count">({{ group.items.length }} items)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Three inputs per row / 每行三个输入框 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col
|
||||||
|
:span="8"
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.templateItemId || item.paramCode"
|
||||||
|
>
|
||||||
|
<el-form-item :label="item.labelEn">
|
||||||
|
<el-input
|
||||||
|
v-model="form[item.paramCode]"
|
||||||
|
:placeholder="getPlaceholder(item)"
|
||||||
|
:class="{ 'is-changed': isChangedFromLast(item) }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 辅助信息:上次/默认值(常驻,不会像 placeholder 一样消失) -->
|
||||||
|
<div class="field-hint">
|
||||||
|
<span v-if="getLastValue(item) !== undefined" class="hint-item">
|
||||||
|
Last Success: <b>{{ getLastValue(item) }}</b>
|
||||||
|
</span>
|
||||||
|
<span v-if="getDefaultValue(item) !== undefined" class="hint-item">
|
||||||
|
Default: <b>{{ getDefaultValue(item) }}</b>
|
||||||
|
</span>
|
||||||
|
<span v-if="isChangedFromLast(item)" class="hint-item changed">
|
||||||
|
Modified
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inline address editor / 编辑点位 -->
|
||||||
|
<div v-if="editTemplate" class="addr-inline">
|
||||||
|
<span class="addr-label">Address:</span>
|
||||||
|
<el-input v-model="item.address" size="mini" placeholder="ns=2;s=..." />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div v-if="!loading && templateItems.length === 0" class="empty-data">
|
||||||
|
<el-empty description="Template is empty or not found"></el-empty>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Import APIs / 引入接口
|
||||||
|
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
|
||||||
|
import { getSendTemplate, getLastSuccess, updateSendTemplate, batchSaveSendTemplateItems } from '@/api/l2/sendTemplate'
|
||||||
|
|
||||||
|
// Import Components / 引入组件
|
||||||
|
import FloatingPanel from '@/components/FloatingPanel.vue'
|
||||||
|
import FurnaceHistoryPanel from './components/FurnaceHistoryPanel.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FurnaceSend',
|
||||||
|
components: {
|
||||||
|
FloatingPanel,
|
||||||
|
FurnaceHistoryPanel
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false, // Loading / 加载
|
||||||
|
sending: false, // Sending / 发送中
|
||||||
|
savingTemplate: false, // Saving template / 保存模板中
|
||||||
|
editTemplate: false, // Edit template switch / 编辑模板开关
|
||||||
|
|
||||||
|
template: null, // Template / 模板
|
||||||
|
lastSuccess: null, // Last success / 上次成功
|
||||||
|
form: {}, // Form values / 表单值
|
||||||
|
|
||||||
|
// 仅用于 Edit Template:记录加载时的原始模板项快照,用来计算差异,避免全量提交
|
||||||
|
originalItemsSnapshot: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
|
||||||
|
// 按 paramCode 前缀分组(如 NOF1 / NOF2 / RTF1 / SF ...)
|
||||||
|
groupedItems() {
|
||||||
|
const groupsMap = new Map()
|
||||||
|
const items = this.templateItems
|
||||||
|
|
||||||
|
items.forEach(it => {
|
||||||
|
const key = this.getGroupKey(it)
|
||||||
|
if (!groupsMap.has(key)) groupsMap.set(key, [])
|
||||||
|
groupsMap.get(key).push(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Map -> Array,并按组名排序(NOF1, NOF2... 这种会自然排序更好)
|
||||||
|
const groups = Array.from(groupsMap.entries()).map(([groupKey, groupItems]) => ({
|
||||||
|
groupKey,
|
||||||
|
groupTitle: this.getGroupTitle(groupKey),
|
||||||
|
items: groupItems
|
||||||
|
}))
|
||||||
|
|
||||||
|
groups.sort((a, b) => String(a.groupKey).localeCompare(String(b.groupKey), undefined, { numeric: true }))
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 不 await:避免阻塞页面渲染;异常会在 Promise 链上抛出到控制台/全局拦截
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
pickItemFields(it) {
|
||||||
|
if (!it) return {}
|
||||||
|
// 仅挑后端支持保存的字段,避免把多余字段/响应结构带回去
|
||||||
|
return {
|
||||||
|
templateItemId: it.templateItemId,
|
||||||
|
templateId: it.templateId,
|
||||||
|
itemNo: it.itemNo,
|
||||||
|
paramCode: it.paramCode,
|
||||||
|
labelEn: it.labelEn,
|
||||||
|
groupNameEn: it.groupNameEn,
|
||||||
|
address: it.address,
|
||||||
|
defaultValueRaw: it.defaultValueRaw,
|
||||||
|
enabled: it.enabled,
|
||||||
|
remark: it.remark
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isItemChanged(a, b) {
|
||||||
|
// a/b 都是 pickItemFields 之后的对象
|
||||||
|
const keys = ['itemNo', 'paramCode', 'labelEn', 'groupNameEn', 'address', 'defaultValueRaw', 'enabled', 'remark']
|
||||||
|
return keys.some(k => String(a?.[k] ?? '') !== String(b?.[k] ?? ''))
|
||||||
|
},
|
||||||
|
|
||||||
|
openHistory() {
|
||||||
|
// FloatingPanel 提供 open() 方法
|
||||||
|
this.$refs.historyPanel && this.$refs.historyPanel.open()
|
||||||
|
},
|
||||||
|
|
||||||
|
applyHistoryValues(payload) {
|
||||||
|
const values = payload && payload.values ? payload.values : {}
|
||||||
|
this.templateItems.forEach(item => {
|
||||||
|
const v = values[item.paramCode]
|
||||||
|
if (v !== undefined) {
|
||||||
|
this.$set(this.form, item.paramCode, String(v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getGroupKey(item) {
|
||||||
|
const code = (item && item.paramCode) ? String(item.paramCode) : ''
|
||||||
|
// 取第一段:NOF1_XXX / NOF1.XXX / NOF1-XXX
|
||||||
|
const m = code.match(/^([A-Za-z]+\d+|[A-Za-z]+)(?=[_.-]|$)/)
|
||||||
|
return m ? m[1] : 'OTHER'
|
||||||
|
},
|
||||||
|
|
||||||
|
getGroupTitle(groupKey) {
|
||||||
|
// 可按你们现场习惯改中文名
|
||||||
|
const map = {
|
||||||
|
NOF: 'NOF',
|
||||||
|
RTF: 'RTF',
|
||||||
|
SF: 'SF',
|
||||||
|
PH: 'PH',
|
||||||
|
JCF: 'JCF',
|
||||||
|
LTH: 'LTH',
|
||||||
|
TDS: 'TDS'
|
||||||
|
}
|
||||||
|
const base = String(groupKey).match(/^[A-Za-z]+/)?.[0] || String(groupKey)
|
||||||
|
// 例如 NOF1 -> NOF1(或 “NOF 1”)
|
||||||
|
const prefixTitle = map[base] || base
|
||||||
|
const suffix = String(groupKey).slice(base.length)
|
||||||
|
return suffix ? `${prefixTitle}${suffix}` : prefixTitle
|
||||||
|
},
|
||||||
|
|
||||||
|
async reload() {
|
||||||
|
this.loading = true
|
||||||
|
const [templateRes, lastRes] = await Promise.all([
|
||||||
|
getSendTemplate('FURNACE_DEFAULT'),
|
||||||
|
getLastSuccess('FURNACE')
|
||||||
|
])
|
||||||
|
|
||||||
|
this.template = templateRes && templateRes.code === 200 ? templateRes.data : null
|
||||||
|
this.lastSuccess = lastRes && lastRes.code === 200 ? lastRes.data : null
|
||||||
|
|
||||||
|
// 记录模板项原始快照(用于 Edit Template 计算差异,避免全量提交)
|
||||||
|
this.originalItemsSnapshot = this.template && Array.isArray(this.template.items)
|
||||||
|
? this.template.items.map(it => this.pickItemFields(it))
|
||||||
|
: []
|
||||||
|
|
||||||
|
this.initializeForm()
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeForm() {
|
||||||
|
const init = {}
|
||||||
|
if (!this.templateItems.length) {
|
||||||
|
this.form = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.templateItems.forEach(item => {
|
||||||
|
const lastValue = this.lastSuccess?.values?.[item.paramCode]
|
||||||
|
init[item.paramCode] = lastValue !== undefined ? lastValue : (item.defaultValueRaw || '')
|
||||||
|
})
|
||||||
|
this.form = init
|
||||||
|
},
|
||||||
|
|
||||||
|
applyLastSuccessValues() {
|
||||||
|
if (!this.lastSuccess || !this.lastSuccess.values) {
|
||||||
|
this.$message.info('No last success data')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.templateItems.forEach(item => {
|
||||||
|
const v = this.lastSuccess.values[item.paramCode]
|
||||||
|
if (v !== undefined) {
|
||||||
|
this.$set(this.form, item.paramCode, String(v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$message.success('Last success values applied')
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaceholder() {
|
||||||
|
// placeholder 只保留引导,不承载关键信息(上次/默认值放到下方提示)
|
||||||
|
return 'Please enter'
|
||||||
|
},
|
||||||
|
|
||||||
|
getLastValue(item) {
|
||||||
|
const v = this.lastSuccess?.values?.[item.paramCode]
|
||||||
|
return v === undefined || v === null || v === '' ? undefined : v
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultValue(item) {
|
||||||
|
const v = item?.defaultValueRaw
|
||||||
|
return v === undefined || v === null || v === '' ? undefined : v
|
||||||
|
},
|
||||||
|
|
||||||
|
isChangedFromLast(item) {
|
||||||
|
const last = this.getLastValue(item)
|
||||||
|
if (last === undefined) return false
|
||||||
|
const cur = this.form?.[item.paramCode]
|
||||||
|
// 统一用字符串比较,避免 1 vs "1"
|
||||||
|
return String(cur ?? '') !== String(last)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(t) {
|
||||||
|
if (!t) return ''
|
||||||
|
return new Date(t).toLocaleString()
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveTemplate() {
|
||||||
|
if (!this.template || !this.template.templateId) {
|
||||||
|
this.$message.error('Template not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savingTemplate = true
|
||||||
|
try {
|
||||||
|
// 1) save template main
|
||||||
|
await updateSendTemplate({
|
||||||
|
templateId: this.template.templateId,
|
||||||
|
deviceName: this.template.deviceName,
|
||||||
|
enabled: this.template.enabled,
|
||||||
|
remark: this.template.remark
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2) diff items (ONLY send changed items)
|
||||||
|
const currentAll = (this.template && Array.isArray(this.template.items))
|
||||||
|
? this.template.items.map(it => this.pickItemFields(it))
|
||||||
|
: []
|
||||||
|
|
||||||
|
const originalAll = Array.isArray(this.originalItemsSnapshot)
|
||||||
|
? this.originalItemsSnapshot
|
||||||
|
: []
|
||||||
|
|
||||||
|
const originalById = new Map(originalAll.filter(x => x.templateItemId != null).map(x => [x.templateItemId, x]))
|
||||||
|
const currentById = new Map(currentAll.filter(x => x.templateItemId != null).map(x => [x.templateItemId, x]))
|
||||||
|
|
||||||
|
// deleted: in original but not in current
|
||||||
|
const deleteIds = []
|
||||||
|
originalById.forEach((_, id) => {
|
||||||
|
if (!currentById.has(id)) deleteIds.push(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// upsert: new (no id) OR changed
|
||||||
|
const upserts = []
|
||||||
|
currentAll.forEach(it => {
|
||||||
|
if (!it.templateItemId) {
|
||||||
|
// new item
|
||||||
|
upserts.push(it)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const old = originalById.get(it.templateItemId)
|
||||||
|
if (!old) {
|
||||||
|
upserts.push(it)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isItemChanged(it, old)) {
|
||||||
|
upserts.push(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!upserts.length && !deleteIds.length) {
|
||||||
|
this.$message.success('No changes')
|
||||||
|
this.editTemplate = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await batchSaveSendTemplateItems({
|
||||||
|
templateId: this.template.templateId,
|
||||||
|
items: upserts,
|
||||||
|
deleteIds
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$message.success('Template saved')
|
||||||
|
this.editTemplate = false
|
||||||
|
await this.reload()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.$message.error(e.message || 'Save template failed')
|
||||||
|
} finally {
|
||||||
|
this.savingTemplate = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSend() {
|
||||||
|
this.$confirm('Confirm to send furnace parameters?', 'Warning', {
|
||||||
|
confirmButtonText: 'Confirm',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.doSend()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
|
||||||
|
async doSend() {
|
||||||
|
if (!this.template) {
|
||||||
|
this.$message.error('Template not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.templateItems.length) {
|
||||||
|
this.$message.error('Template is empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sending = true
|
||||||
|
try {
|
||||||
|
// 仅发送“有点位 + 相对 lastSuccess 有变化”的项,避免请求体过大导致超时
|
||||||
|
const items = this.templateItems
|
||||||
|
.map(item => {
|
||||||
|
const cur = String(this.form?.[item.paramCode] ?? '')
|
||||||
|
const last = this.lastSuccess?.values?.[item.paramCode]
|
||||||
|
const hasLast = last !== undefined && last !== null
|
||||||
|
// 有 lastSuccess 时:严格对比;没有 lastSuccess 时:只有非空才发送(避免全空全量下发)
|
||||||
|
const changed = hasLast ? (String(last) !== cur) : (cur !== '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
paramCode: item.paramCode,
|
||||||
|
address: item.address,
|
||||||
|
valueRaw: cur,
|
||||||
|
setTime: new Date(),
|
||||||
|
__changed: changed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(it => !!it.address && it.__changed)
|
||||||
|
.map(({ __changed, ...rest }) => rest)
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
this.$message.info('没有检测到变更项,无需发送')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto = {
|
||||||
|
deviceName: this.template.deviceName,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupNo: 1,
|
||||||
|
groupType: 'FURNACE',
|
||||||
|
groupName: this.template.templateName || 'Furnace Settings',
|
||||||
|
items
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await createSendJob(dto)
|
||||||
|
const jobId = createRes.data
|
||||||
|
if (!jobId) throw new Error('Create send job failed')
|
||||||
|
|
||||||
|
await executeSendJob(jobId)
|
||||||
|
this.$message.success('Send success')
|
||||||
|
|
||||||
|
await this.reload()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.$message.error(e.message || 'Send failed')
|
||||||
|
} finally {
|
||||||
|
this.sending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-title { margin-bottom: 20px; }
|
||||||
|
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.group-list { margin-top: 8px; }
|
||||||
|
.group-section { padding: 10px 0 6px; border-top: 1px solid #ebeef5; }
|
||||||
|
.group-section:first-child { border-top: none; padding-top: 0; }
|
||||||
|
.group-header { display:flex; align-items:baseline; gap:8px; margin: 4px 0 10px; }
|
||||||
|
.group-title { font-weight: 600; }
|
||||||
|
.group-count { color:#909399; font-size: 12px; margin-left: 6px; }
|
||||||
|
|
||||||
|
/* 字段提示信息 */
|
||||||
|
.field-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item.changed {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改过的输入框高亮 */
|
||||||
|
:deep(.el-input.is-changed .el-input__inner) {
|
||||||
|
border-color: #e6a23c;
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- Right main content area / 右侧主内容区 -->
|
<!-- Right main content area / 右侧主内容区 -->
|
||||||
<el-col :span="20" class="content-col">
|
<el-col :span="20" class="content-col">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
@@ -124,7 +124,7 @@ export default {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
background: #e8e8e8;
|
background: #e8e8e8;
|
||||||
border-bottom: 1px solid #d4d4d4;
|
border-bottom: 1px solid #d4d4d4;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -137,23 +137,23 @@ export default {
|
|||||||
border-right: none;
|
border-right: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
::v-deep .el-menu-item {
|
::v-deep .el-menu-item {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-right: 2px solid #999;
|
border-right: 2px solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -177,4 +177,4 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</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