504 lines
16 KiB
Vue
504 lines
16 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 工具栏 -->
|
||
<div class="toolbar">
|
||
<el-button @click="reload" icon="el-icon-refresh" size="small" :loading="loading">
|
||
刷新
|
||
</el-button>
|
||
<el-button
|
||
v-if="lastSuccess && lastSuccess.lastSendTime"
|
||
type="primary"
|
||
plain
|
||
icon="el-icon-magic-stick"
|
||
size="small"
|
||
@click="applyLastSuccessValues"
|
||
>
|
||
应用上次成功参数
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 计划队列 -->
|
||
<div v-loading="planQueueLoading" class="plan-queue-section">
|
||
<div class="section-header">
|
||
<i class="el-icon-s-order"></i>
|
||
<span>生产队列</span>
|
||
<el-badge :value="sortedPlanQueue.length" class="tab-badge" style="margin-left: 10px;" />
|
||
</div>
|
||
<div class="plan-list">
|
||
<div
|
||
v-for="plan in sortedPlanQueue"
|
||
:key="plan.id"
|
||
class="plan-item"
|
||
:class="{
|
||
'plan-item-producing': plan.status === 'PRODUCING',
|
||
'plan-item-ready': plan.status === 'READY',
|
||
'plan-item-new': plan.status === 'NEW'
|
||
}"
|
||
>
|
||
<div class="plan-status">
|
||
<span class="status-dot" :class="{
|
||
'status-producing': plan.status === 'PRODUCING',
|
||
'status-ready': plan.status === 'READY',
|
||
'status-new': plan.status === 'NEW'
|
||
}"></span>
|
||
<el-tag
|
||
:type="plan.status === 'PRODUCING' ? 'success' :
|
||
plan.status === 'READY' ? 'primary' : 'info'"
|
||
size="mini"
|
||
>
|
||
{{ plan.status === 'PRODUCING' ? '生产中' :
|
||
plan.status === 'READY' ? '就绪' : '新建' }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="plan-content">
|
||
<span class="plan-no">{{ plan.planid }}</span>
|
||
<span class="coil-no">{{ plan.coilid }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="sortedPlanQueue.length === 0 && !planQueueLoading" class="empty-text">
|
||
暂无生产计划
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 卡片列表 -->
|
||
<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">
|
||
<!-- 头部信息:多字段拼接展示 -->
|
||
<div class="card-title-row">
|
||
<span class="card-title">
|
||
计划ID: {{ setup.planid || '-' }}
|
||
| 钢卷号: {{ setup.coilid || '-' }}
|
||
| 钢种: {{ setup.steelGrade || setup.grade || '-' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="card-subtitle">
|
||
<span>入口厚度: {{ setup.entryThick || '-' }}</span>
|
||
<span>入口宽度: {{ setup.entryWidth || '-' }}</span>
|
||
<span>入口重量: {{ setup.entryWeight || '-' }}</span>
|
||
<span>入口长度: {{ setup.entryLength || '-' }}</span>
|
||
</div>
|
||
|
||
<div class="card-subtitle">
|
||
<span>拉伸机延伸率: {{ setup.tlElong || '-' }}</span>
|
||
<span>轧机轧制力: {{ setup.tmRollforce || '-' }}</span>
|
||
<span>轧机弯辊力: {{ setup.tmBendforce || '-' }}</span>
|
||
<span v-if="setup.updateTime">更新时间: {{ formatTime(setup.updateTime) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-right">
|
||
<el-button
|
||
type="primary"
|
||
size="mini"
|
||
icon="el-icon-s-promotion"
|
||
@click="handleSendCurrent(setup)"
|
||
:loading="setup.sending"
|
||
>
|
||
当前计划下发
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
size="mini"
|
||
icon="el-icon-right"
|
||
@click="handleSendNext(setup)"
|
||
:loading="setup.sendingNext"
|
||
>
|
||
下一计划下发
|
||
</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="暂无配置历史数据"></el-empty>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
// 引入接口
|
||
import { listSetup } from '@/api/business/setup'
|
||
import { createSendJob, executeSendJob } from '@/api/l2/sendJob'
|
||
import { getLastSuccess } from '@/api/l2/sendTemplate'
|
||
import { listPlan } from '@/api/l2/plan'
|
||
|
||
// 传动字段定义(中文界面,贴合工业场景)
|
||
const DRIVE_FIELDS = [
|
||
{ key: 'porTension', label: '开卷机张力' },
|
||
{ key: 'celTension', label: '入口活套张力' },
|
||
{ key: 'cleanTension', label: '清洗段张力' },
|
||
{ key: 'furTension', label: '炉区张力' },
|
||
{ key: 'towerTension', label: '冷却塔张力' },
|
||
{ key: 'tmNoneTension', label: '轧机无张力' },
|
||
{ key: 'tmEntryTension', label: '轧机入口张力' },
|
||
{ key: 'tmExitTension', label: '轧机出口张力' },
|
||
{ key: 'tlNoneTension', label: '拉伸机无张力' },
|
||
{ key: 'tlExitTension', label: '拉伸机出口张力' },
|
||
{ key: 'coatTension', label: '后处理段张力' },
|
||
{ key: 'cxlTension', label: '出口活套张力' },
|
||
{ key: 'trTension', label: '卷取机张力' },
|
||
|
||
{ key: 'tlElong', label: '拉伸机延伸率' },
|
||
{ key: 'tlLvlMesh1', label: '拉伸机矫直辊间隙1' },
|
||
{ key: 'tlLvlMesh2', label: '拉伸机矫直辊间隙2' },
|
||
{ key: 'tlAcbMesh', label: '拉伸机防侧弯间隙' },
|
||
|
||
{ key: 'tmBendforce', label: '轧机弯辊力' },
|
||
{ key: 'tmAcrMesh', label: '轧机防皱辊间隙' },
|
||
{ key: 'tmBrMesh', label: '轧机防颤辊间隙' },
|
||
{ key: 'tmRollforce', label: '轧机轧制力' }
|
||
]
|
||
|
||
// 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: [],
|
||
planQueueLoading: false,
|
||
planQueue: [],
|
||
driveFields: DRIVE_FIELDS,
|
||
driveAddress: { ...DRIVE_ADDRESS }
|
||
}
|
||
},
|
||
created() {
|
||
this.reload()
|
||
},
|
||
computed: {
|
||
// 生产队列(不含已完成),按优先级排序:生产中 > 就绪 > 新建
|
||
sortedPlanQueue() {
|
||
const statusPriority = { PRODUCING: 1, READY: 2, NEW: 3 }
|
||
return (this.planQueue || []).slice().sort((a, b) => {
|
||
const pa = statusPriority[a.status] || 999
|
||
const pb = statusPriority[b.status] || 999
|
||
return pa - pb
|
||
})
|
||
},
|
||
// 下一计划:队列中第一个非生产中计划(READY/NEW);若没有则取队列第一个
|
||
nextPlan() {
|
||
const list = this.sortedPlanQueue
|
||
if (!list.length) return null
|
||
const next = list.find(p => p.status !== 'PRODUCING')
|
||
return next || list[0]
|
||
}
|
||
},
|
||
methods: {
|
||
// 获取生产计划队列
|
||
async getPlanQueue() {
|
||
this.planQueueLoading = true
|
||
try {
|
||
// 查询状态为 PRODUCING, READY, NEW 的计划
|
||
const res = await listPlan({
|
||
status: 'PRODUCING,READY,NEW',
|
||
pageSize: 100, // 获取足够多的计划
|
||
pageNum: 1
|
||
})
|
||
this.planQueue = res.data || []
|
||
} catch (e) {
|
||
console.error('获取计划队列失败:', e)
|
||
this.$message.error('获取计划队列失败')
|
||
} finally {
|
||
this.planQueueLoading = false
|
||
}
|
||
},
|
||
|
||
async reload() {
|
||
// 同时加载计划队列和设置
|
||
await Promise.all([
|
||
this.getPlanQueue(),
|
||
this.loadSetups()
|
||
])
|
||
},
|
||
|
||
async loadSetups() {
|
||
this.loading = true
|
||
try {
|
||
// 1. 获取传动模块上次成功下发数据
|
||
const lastRes = await getLastSuccess('DRIVE')
|
||
this.lastSuccess = lastRes && lastRes.code === 200 ? lastRes.data : null
|
||
|
||
// 2. 获取配置历史列表
|
||
const setupRes = await listSetup({ pageNum: 1, pageSize: 20 })
|
||
const setupList = (setupRes && setupRes.rows) || []
|
||
|
||
// 3. 映射配置列表为展示数据
|
||
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]
|
||
|
||
// 优先级:当前配置值 > 上次成功值 > 空字符串
|
||
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,
|
||
sendingNext: false
|
||
}
|
||
})
|
||
} catch (e) {
|
||
console.error(e)
|
||
this.$message.error('加载失败')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
applyLastSuccessValues() {
|
||
if (!this.lastSuccess || !this.lastSuccess.values) {
|
||
this.$message.info('暂无上次成功数据')
|
||
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('上次成功参数应用完成')
|
||
},
|
||
|
||
getPlaceholder(key) {
|
||
const v = this.lastSuccess?.values?.[key]
|
||
if (v !== undefined) return `上次值:${v}`
|
||
return '请输入参数值'
|
||
},
|
||
|
||
formatTime(t) {
|
||
if (!t) return ''
|
||
return new Date(t).toLocaleString()
|
||
},
|
||
|
||
handleSendCurrent(setup) {
|
||
this.$confirm(
|
||
`确认要下发【${setup.coilid || '-'}】钢卷的传动参数吗?`,
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确认',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
).then(() => this.doSend(setup)).catch(() => {})
|
||
},
|
||
|
||
handleSendNext(setup) {
|
||
if (!this.nextPlan) {
|
||
this.$message.warning('暂无下一计划')
|
||
return
|
||
}
|
||
const plan = this.nextPlan
|
||
this.$confirm(
|
||
`确认要按下一计划【${plan.coilid || '-'}】下发传动参数吗?`,
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确认',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
).then(() => this.doSendNextPlan(setup, plan)).catch(() => {})
|
||
},
|
||
|
||
async doSend(setup, plan) {
|
||
const isNextPlan = !!plan
|
||
if (isNextPlan) {
|
||
setup.sendingNext = true
|
||
} else {
|
||
setup.sending = true
|
||
}
|
||
|
||
try {
|
||
const items = this.driveFields.map(f => ({
|
||
paramCode: f.key,
|
||
address: this.driveAddress[f.key],
|
||
valueRaw: String(setup.params[f.key] || ''),
|
||
setTime: new Date()
|
||
})).filter(it => !!it.address)
|
||
|
||
if (!items.length) {
|
||
this.$message.warning('OPC地址未配置,无可下发内容')
|
||
return
|
||
}
|
||
|
||
const bizKey = isNextPlan ? plan.coilid : setup.coilid
|
||
const dto = {
|
||
deviceName: 'CGL_LINE_1',
|
||
bizKey: bizKey,
|
||
groups: [
|
||
{
|
||
groupNo: 1,
|
||
groupType: 'DRIVE',
|
||
groupName: `传动参数_${bizKey || ''}`,
|
||
items
|
||
}
|
||
]
|
||
}
|
||
|
||
const createRes = await createSendJob(dto)
|
||
const jobId = createRes.data
|
||
if (!jobId) throw new Error('创建下发任务失败')
|
||
|
||
await executeSendJob(jobId)
|
||
this.$message.success('下发成功')
|
||
|
||
await this.reload()
|
||
} catch (e) {
|
||
console.error(e)
|
||
this.$message.error(e.message || '下发失败')
|
||
} finally {
|
||
if (isNextPlan) {
|
||
setup.sendingNext = false
|
||
} else {
|
||
setup.sending = false
|
||
}
|
||
}
|
||
},
|
||
|
||
doSendNextPlan(setup, plan) {
|
||
this.doSend(setup, plan)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.toolbar { margin-bottom: 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||
|
||
.plan-queue-section {
|
||
margin-bottom: 20px;
|
||
background: #ffffff;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 15px;
|
||
background: #f5f7fa;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.plan-list {
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.plan-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #ffffff;
|
||
border: 1px solid #e4e7ed;
|
||
border-left: 3px solid #409eff;
|
||
border-radius: 4px;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.plan-item-producing { border-left-color: #67c23a; }
|
||
.plan-item-ready { border-left-color: #409eff; }
|
||
.plan-item-new { border-left-color: #909399; }
|
||
|
||
.plan-status { display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
||
.plan-content { display: flex; flex-direction: column; gap: 2px; }
|
||
.plan-no { font-weight: 600; color: #409eff; }
|
||
.coil-no { font-size: 12px; color: #606266; }
|
||
|
||
.status-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||
.status-producing { background: #67c23a; box-shadow: 0 0 6px rgba(103, 194, 58, 0.6); }
|
||
.status-ready { background: #409eff; box-shadow: 0 0 6px rgba(64, 158, 255, 0.5); }
|
||
.status-new { background: #909399; box-shadow: 0 0 6px rgba(144, 147, 153, 0.4); }
|
||
|
||
.empty-text { padding: 10px; color: #909399; }
|
||
|
||
.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; display: flex; gap: 8px; align-items: center; }
|
||
.last-send-time { font-size: 12px; color:#909399; margin-right:16px; }
|
||
.empty-data { margin-top: 20px; }
|
||
</style> |