Files
klp-mono/apps/l2/src/views/l2/send/drive.vue
2026-01-05 14:57:22 +08:00

504 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>