feat(钢卷跟踪): 从哪开始获取计划?一次性获取多少个计划?不同批次的顺序号相同怎么处理?

已实现的功能:
Systemcount+信号变化才算有效
状态机逻辑:信号1必须配合计数器变化才触发,然后等待信号2
信号2必须配合计数器变化+保持2秒才触发
第一批1-5,第二批2-6,第三批3-7
每次取5个钢卷,顺序号滑动+1
信号2触发时更新Oracle追踪表
OPC页面配置点位
信号1(入口钢卷)节点配置
信号2(焊接完成)节点配置
计数器节点配置
保存后自动重启OPC服务
前端操作中间表 
TrackCoil页面可增删改查临时表
可手动调整顺序
模拟信号1/信号2按钮可测试

- 后端新增钢卷跟踪相关API和数据库表
- 前端添加钢卷跟踪管理页面
- OPC服务增加信号节点监控和状态机处理
- 实现钢卷跟踪的自动更新逻辑
This commit is contained in:
2026-04-11 14:52:47 +08:00
parent 742802d7db
commit 538401017a
11 changed files with 685 additions and 16 deletions

View File

@@ -41,6 +41,7 @@ export default {
nav: [
{ path: '/pdi', label: 'PDI 计划管理', icon: 'el-icon-document' },
{ path: '/trackmap', label: '跟踪图监控', icon: 'el-icon-monitor' },
{ path: '/trackcoil', label: '钢卷跟踪管理', icon: 'el-icon-rank' },
{ path: '/opc', label: 'OPC 配置', icon: 'el-icon-setting' }
]
}

View File

@@ -43,3 +43,15 @@ export const gradeApi = {
getL2ModelGrades: () => http.get('/grades/l2model'),
getNextNumbers: () => http.get('/pdi/next-numbers')
}
// 跟踪钢卷管理 API
export const trackApi = {
getCoils: () => http.get('/track/coils'),
addCoil: (data) => http.post('/track/coils', data),
updateCoil: (id, data) => http.put(`/track/coils/${id}`, data),
deleteCoil: (id) => http.delete(`/track/coils/${id}`),
clearCoils: () => http.delete('/track/coils'),
getCoilsByRange: (start, end) => http.get('/track/coils/range', { params: { start, end } }),
simulateSignal1: () => http.post('/track/simulate/signal1'),
simulateSignal2: () => http.post('/track/simulate/signal2')
}

View File

@@ -3,6 +3,7 @@ import VueRouter from 'vue-router'
import PdiList from './views/PdiList.vue'
import TrackMap from './views/TrackMap.vue'
import OpcConfig from './views/OpcConfig.vue'
import TrackCoil from './views/TrackCoil.vue'
Vue.use(VueRouter)
@@ -11,6 +12,7 @@ export default new VueRouter({
{ path: '/', redirect: '/pdi' },
{ path: '/pdi', component: PdiList, meta: { title: 'PDI计划管理' } },
{ path: '/trackmap', component: TrackMap, meta: { title: '跟踪图监控' } },
{ path: '/trackcoil', component: TrackCoil, meta: { title: '钢卷跟踪管理' } },
{ path: '/opc', component: OpcConfig, meta: { title: 'OPC配置' } }
]
})

View File

@@ -16,6 +16,18 @@
</el-form-item>
</el-form>
<div class="panel-title" style="margin-top:16px">跟踪信号节点配置</div>
<el-form :model="form" label-width="155px" size="small">
<el-form-item label="信号1(入口钢卷)">
<el-input v-model="form.signal1_node" placeholder="ns=2;s=PL.Signal.EntryCoil" style="width:360px" />
<span class="hint">入口钢卷信号01触发</span>
</el-form-item>
<el-form-item label="信号2(焊接完成)">
<el-input v-model="form.signal2_node" placeholder="ns=2;s=PL.Signal.WeldDone" style="width:360px" />
<span class="hint">焊接完成信号01保持2秒触发</span>
</el-form-item>
</el-form>
<div class="panel-title" style="margin-top:16px">跟踪图节点映射</div>
<div style="font-size:12px;color:#888;margin-bottom:10px">
Oracle列名 OPC节点ID必须包含 <b>position</b>
@@ -41,6 +53,7 @@
<span>服务<el-tag :type="status.running?'success':'danger'" size="mini">{{ status.running?'运行中':'已停止' }}</el-tag></span>
<span>计数器<b>{{ status.last_counter??'--' }}</b></span>
<span style="color:#888">更新{{ status.last_update||'--' }}</span>
<span>跟踪状态<el-tag size="mini" :type="status.track_state === 'WAIT_S1'?'info':'success'">{{ status.track_state||'--' }}</el-tag></span>
</div>
<div class="panel-title">最新日志</div>
<div class="log-box" ref="logBox">
@@ -58,9 +71,9 @@ export default {
data() {
return {
loading: false, saving: false,
form: { opc_url: '', counter_node: '', poll_interval: 2 },
form: { opc_url: '', counter_node: '', poll_interval: 2, signal1_node: '', signal2_node: '' },
nodeList: [],
status: { running: false, last_counter: null, last_update: null, log: [] },
status: { running: false, last_counter: null, last_update: null, log: [], track_state: '' },
statusTimer: null
}
},
@@ -78,6 +91,8 @@ export default {
this.form.opc_url = cfg.opc_url
this.form.counter_node = cfg.counter_node
this.form.poll_interval = cfg.poll_interval
this.form.signal1_node = cfg.signal1_node || ''
this.form.signal2_node = cfg.signal2_node || ''
this.nodeList = Object.entries(cfg.trackmap_nodes || {}).map(([col, node]) => ({ col, node }))
} catch (e) { this.$message.error('加载失败: ' + e.message) }
finally { this.loading = false }

View File

@@ -555,7 +555,5 @@ export default {
</script>
<style>
v-deep .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item {
margin-bottom: 4px;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<div>
<div class="panel" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<el-button size="small" @click="loadCoils">刷新</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="openAdd">新增钢卷</el-button>
<el-button size="small" type="danger" icon="el-icon-delete" @click="clearAll">清空全部</el-button>
<span style="flex:1"></span>
<el-button size="small" type="warning" @click="triggerSignal1">模拟信号1(入口)</el-button>
<el-button size="small" type="success" @click="triggerSignal2">模拟信号2(焊接完成)</el-button>
<span style="font-size:12px;color:#666">提示: Position 1对应产线入口(顺序号1), Position 5对应产线出口(顺序号5)</span>
</div>
<div class="panel" style="padding:0;margin-top:10px">
<el-table :data="coils" stripe border size="small" v-loading="loading" element-loading-text="加载中...">
<el-table-column prop="position" label="Position" width="90" />
<el-table-column prop="coilid" label="钢卷号" width="148" />
<el-table-column prop="sequencenb" label="顺序号" width="90" />
<el-table-column prop="rollprogramnb" label="批次号" width="108" />
<el-table-column prop="created_dt" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="{row}">
<el-button type="text" size="mini" @click.stop="openEdit(row)">编辑</el-button>
<el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveUp(row)" :disabled="row.position === 1">上移</el-button>
<el-button type="text" size="mini" style="color:#e67e22" @click.stop="moveDown(row)" :disabled="row.position === 5">下移</el-button>
<el-button type="text" size="mini" style="color:#c0392b" @click.stop="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="400px" :close-on-click-modal="false">
<el-form :model="form" :rules="rules" ref="trackForm" label-width="80px" size="small">
<el-form-item label="钢卷号" prop="coilid">
<el-input v-model="form.coilid" placeholder="12位钢卷号" />
</el-form-item>
<el-form-item label="顺序号" prop="sequencenb">
<el-input-number v-model="form.sequencenb" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="批次号" prop="rollprogramnb">
<el-input-number v-model="form.rollprogramnb" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="位置" prop="position">
<el-input-number v-model="form.position" :min="1" :max="5" :controls="false" style="width:100%" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="small" @click="doSave" :loading="saving">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { trackApi } from '../api/index'
export default {
name: 'TrackCoil',
data() {
return {
loading: false,
saving: false,
coils: [],
dialogVisible: false,
isEdit: false,
form: {
id: null,
coilid: '',
sequencenb: 0,
rollprogramnb: 0,
position: 1
},
rules: {
coilid: [
{ required: true, message: '钢卷号不能为空', trigger: 'blur' }
]
}
}
},
computed: {
dialogTitle() {
return this.isEdit ? '编辑钢卷' : '新增钢卷'
}
},
created() {
this.loadCoils()
},
methods: {
async loadCoils() {
this.loading = true
try {
const res = await trackApi.getCoils()
this.coils = res.data || []
} catch (e) {
this.$message.error(e.message)
} finally {
this.loading = false
}
},
async fetchRangeCoils() {
try {
const res = await trackApi.getCoilsByRange(1, 5)
const coils = res.data || []
for (const coil of coils) {
await trackApi.addCoil({
coilid: coil.coilid,
sequencenb: coil.sequencenb,
rollprogramnb: coil.rollprogramnb
})
}
this.$message.success(`已添加 ${coils.length} 个钢卷`)
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
}
},
openAdd() {
this.isEdit = false
this.form = {
id: null,
coilid: '',
sequencenb: 0,
rollprogramnb: 0,
position: (this.coils.length || 0) + 1
}
this.dialogVisible = true
this.$nextTick(() => this.$refs.trackForm && this.$refs.trackForm.clearValidate())
},
openEdit(row) {
this.isEdit = true
this.form = { ...row }
this.dialogVisible = true
this.$nextTick(() => this.$refs.trackForm && this.$refs.trackForm.clearValidate())
},
doSave() {
this.$refs.trackForm.validate(async valid => {
if (!valid) return
this.saving = true
try {
if (this.isEdit) {
await trackApi.updateCoil(this.form.id, {
coilid: this.form.coilid,
sequencenb: this.form.sequencenb,
rollprogramnb: this.form.rollprogramnb,
position: this.form.position
})
this.$message.success('更新成功')
} else {
await trackApi.addCoil({
coilid: this.form.coilid,
sequencenb: this.form.sequencenb,
rollprogramnb: this.form.rollprogramnb
})
this.$message.success('添加成功')
}
this.dialogVisible = false
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
} finally {
this.saving = false
}
})
},
async doDelete(row) {
this.$confirm(`确认删除钢卷 [${row.coilid}]`, '删除确认', {
confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
await trackApi.deleteCoil(row.id)
this.$message.success('删除成功')
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
}
}).catch(() => {})
},
async clearAll() {
this.$confirm('确认清空所有钢卷?', '清空确认', {
confirmButtonText: '确认清空', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
await trackApi.clearCoils()
this.$message.success('清空成功')
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
}
}).catch(() => {})
},
async moveUp(row) {
if (row.position <= 1) return
const currentPos = row.position
const other = this.coils.find(c => c.position === currentPos - 1)
if (other) {
await trackApi.updateCoil(row.id, { ...row, position: currentPos - 1 })
await trackApi.updateCoil(other.id, { ...other, position: currentPos })
this.loadCoils()
}
},
async moveDown(row) {
if (row.position >= 5) return
const currentPos = row.position
const other = this.coils.find(c => c.position === currentPos + 1)
if (other) {
await trackApi.updateCoil(row.id, { ...row, position: currentPos + 1 })
await trackApi.updateCoil(other.id, { ...other, position: currentPos })
this.loadCoils()
}
},
async triggerSignal1() {
try {
await trackApi.simulateSignal1()
this.$message.success('信号1已触发 - 已获取下一批钢卷到临时表')
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
}
},
async triggerSignal2() {
try {
await trackApi.simulateSignal2()
this.$message.success('信号2已触发 - 已更新追踪表')
this.loadCoils()
} catch (e) {
this.$message.error(e.message)
}
}
}
}
</script>