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:
118
backend/main.py
118
backend/main.py
@@ -5,7 +5,8 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query, APIRouter
|
||||||
|
import datetime
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -13,9 +14,14 @@ load_dotenv()
|
|||||||
from database import get_connection
|
from database import get_connection
|
||||||
from models import PDIPLTMCreate, PDIPLTMUpdate, OpcConfig
|
from models import PDIPLTMCreate, PDIPLTMUpdate, OpcConfig
|
||||||
from opc_service import opc_service
|
from opc_service import opc_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
from sqlite_sync import (
|
from sqlite_sync import (
|
||||||
init_db, sync_all_from_oracle,
|
init_db, sync_all_from_oracle,
|
||||||
sqlite_upsert_pdi, sqlite_delete_pdi
|
sqlite_upsert_pdi, sqlite_delete_pdi,
|
||||||
|
sqlite_get_coil_track, sqlite_update_coil_track_item,
|
||||||
|
sqlite_add_coil_track_item, sqlite_delete_coil_track_item,
|
||||||
|
sqlite_clear_coil_track, sqlite_get_coils_by_sequencenb_range
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@@ -362,6 +368,8 @@ def get_opc_config():
|
|||||||
"counter_node": opc_service.counter_node,
|
"counter_node": opc_service.counter_node,
|
||||||
"trackmap_nodes": opc_service.trackmap_nodes,
|
"trackmap_nodes": opc_service.trackmap_nodes,
|
||||||
"poll_interval": opc_service.poll_interval,
|
"poll_interval": opc_service.poll_interval,
|
||||||
|
"signal1_node": opc_service.signal1_node,
|
||||||
|
"signal2_node": opc_service.signal2_node,
|
||||||
"running": opc_service.running,
|
"running": opc_service.running,
|
||||||
"last_counter": opc_service.last_counter,
|
"last_counter": opc_service.last_counter,
|
||||||
"last_update": opc_service.last_update,
|
"last_update": opc_service.last_update,
|
||||||
@@ -375,6 +383,8 @@ async def save_opc_config(config: OpcConfig):
|
|||||||
opc_service.counter_node = config.counter_node
|
opc_service.counter_node = config.counter_node
|
||||||
opc_service.trackmap_nodes = config.trackmap_nodes
|
opc_service.trackmap_nodes = config.trackmap_nodes
|
||||||
opc_service.poll_interval = config.poll_interval
|
opc_service.poll_interval = config.poll_interval
|
||||||
|
opc_service.signal1_node = config.signal1_node
|
||||||
|
opc_service.signal2_node = config.signal2_node
|
||||||
try:
|
try:
|
||||||
opc_service.save_config()
|
opc_service.save_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -391,6 +401,7 @@ def opc_status():
|
|||||||
"last_counter": opc_service.last_counter,
|
"last_counter": opc_service.last_counter,
|
||||||
"last_update": opc_service.last_update,
|
"last_update": opc_service.last_update,
|
||||||
"log": opc_service.event_log[-50:],
|
"log": opc_service.event_log[-50:],
|
||||||
|
"track_state": opc_service.track_state,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -471,7 +482,7 @@ def get_l2_model_grades():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/pdi/next-numbers")
|
@app.get("/api/pdi/next-numbers")
|
||||||
def get_next_numbers():
|
def get_next_numbers():
|
||||||
"""
|
"""
|
||||||
获取下一个可用的批次编号和顺序号
|
获取下一个可用的批次编号和顺序号
|
||||||
@@ -536,3 +547,104 @@ def get_next_numbers():
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
if 'conn' in locals():
|
if 'conn' in locals():
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# COIL_TRACK_TEMP 临时跟踪表 CRUD
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/api/track/coils")
|
||||||
|
def get_track_coils():
|
||||||
|
"""获取临时跟踪表中的钢卷列表"""
|
||||||
|
coils = sqlite_get_coil_track()
|
||||||
|
return {"data": coils}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/track/coils")
|
||||||
|
def add_track_coil(data: dict):
|
||||||
|
"""新增一个钢卷到临时跟踪表"""
|
||||||
|
coilid = data.get("coilid")
|
||||||
|
sequencenb = data.get("sequencenb")
|
||||||
|
rollprogramnb = data.get("rollprogramnb")
|
||||||
|
if not coilid:
|
||||||
|
raise HTTPException(status_code=400, detail="coilid不能为空")
|
||||||
|
sqlite_add_coil_track_item(coilid, sequencenb or 0, rollprogramnb or 0)
|
||||||
|
return {"message": "添加成功"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/track/coils/{id}")
|
||||||
|
def update_track_coil(id: int, data: dict):
|
||||||
|
"""更新临时跟踪表中的钢卷"""
|
||||||
|
coilid = data.get("coilid")
|
||||||
|
sequencenb = data.get("sequencenb", 0)
|
||||||
|
rollprogramnb = data.get("rollprogramnb", 0)
|
||||||
|
position = data.get("position", 1)
|
||||||
|
if not coilid:
|
||||||
|
raise HTTPException(status_code=400, detail="coilid不能为空")
|
||||||
|
sqlite_update_coil_track_item(id, coilid, sequencenb, rollprogramnb, position)
|
||||||
|
return {"message": "更新成功"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/track/coils/{id}")
|
||||||
|
def delete_track_coil(id: int):
|
||||||
|
"""删除临时跟踪表中的钢卷"""
|
||||||
|
sqlite_delete_coil_track_item(id)
|
||||||
|
return {"message": "删除成功"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/track/coils")
|
||||||
|
def clear_track_coils():
|
||||||
|
"""清空临时跟踪表"""
|
||||||
|
sqlite_clear_coil_track()
|
||||||
|
return {"message": "清空成功"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/track/coils/range")
|
||||||
|
def get_coils_by_range(start: int = Query(1), end: int = Query(5)):
|
||||||
|
"""根据顺序号范围查询钢卷"""
|
||||||
|
coils = sqlite_get_coils_by_sequencenb_range(start, end)
|
||||||
|
return {"data": coils}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# OPC Signal Configuration
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/api/opc/signals")
|
||||||
|
def get_signal_config():
|
||||||
|
"""获取信号节点配置"""
|
||||||
|
return {
|
||||||
|
"signal1_node": opc_service.signal1_node,
|
||||||
|
"signal2_node": opc_service.signal2_node,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/opc/signals")
|
||||||
|
async def save_signal_config(data: dict):
|
||||||
|
"""保存信号节点配置"""
|
||||||
|
opc_service.signal1_node = data.get("signal1_node", opc_service.signal1_node)
|
||||||
|
opc_service.signal2_node = data.get("signal2_node", opc_service.signal2_node)
|
||||||
|
try:
|
||||||
|
opc_service.save_config()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Save signal config failed: %s", e)
|
||||||
|
raise HTTPException(status_code=500, detail=f"配置保存失败: {e}")
|
||||||
|
return {"message": "信号节点配置已保存"}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 模拟信号接口 (用于测试)
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.post("/api/track/simulate/signal1")
|
||||||
|
async def simulate_signal1():
|
||||||
|
"""模拟信号1触发 - 获取下5个钢卷"""
|
||||||
|
await opc_service._handle_signal1()
|
||||||
|
return {"message": "信号1已触发"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/track/simulate/signal2")
|
||||||
|
async def simulate_signal2():
|
||||||
|
"""模拟信号2触发 - 更新追踪表"""
|
||||||
|
await opc_service._handle_signal2()
|
||||||
|
return {"message": "信号2已触发"}
|
||||||
|
|||||||
@@ -182,3 +182,5 @@ class OpcConfig(BaseModel):
|
|||||||
counter_node: str
|
counter_node: str
|
||||||
poll_interval: int = 2
|
poll_interval: int = 2
|
||||||
trackmap_nodes: Dict[str, str] = {}
|
trackmap_nodes: Dict[str, str] = {}
|
||||||
|
signal1_node: str = ""
|
||||||
|
signal2_node: str = ""
|
||||||
|
|||||||
8
backend/opc_config.json
Normal file
8
backend/opc_config.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opc_url": "opc.tcp://192.168.1.100:4840",
|
||||||
|
"counter_node": "ns=2;s=PL.TRACKMAP.COUNTER",
|
||||||
|
"poll_interval": 2,
|
||||||
|
"trackmap_nodes": {},
|
||||||
|
"signal1_node": "ns=2;s=PL.Signal.EntryCoil",
|
||||||
|
"signal2_node": "ns=2;s=PL.Signal.WeldDone"
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ Logic:
|
|||||||
`trackmap_nodes` (a dict: {oracle_column -> node_id}).
|
`trackmap_nodes` (a dict: {oracle_column -> node_id}).
|
||||||
4. For each row returned by the trackmap nodes build an UPDATE statement
|
4. For each row returned by the trackmap nodes build an UPDATE statement
|
||||||
and apply it to PLTM.CMPT_PL_TRACKMAP.
|
and apply it to PLTM.CMPT_PL_TRACKMAP.
|
||||||
|
5. Monitor two signal nodes:
|
||||||
|
- Signal1 (Entry): When 0->1, fetch next 5 coils from PDI and save to SQLite temp table
|
||||||
|
- Signal2 (WeldDone): When 0->1 and held for 2s, update CMPT_PL_TRACKMAP
|
||||||
|
|
||||||
`trackmap_nodes` example (stored in .env or configured via UI):
|
`trackmap_nodes` example (stored in .env or configured via UI):
|
||||||
{
|
{
|
||||||
@@ -42,6 +45,16 @@ class OpcService:
|
|||||||
"OPC_COUNTER_NODE", "ns=2;s=PL.TRACKMAP.COUNTER"
|
"OPC_COUNTER_NODE", "ns=2;s=PL.TRACKMAP.COUNTER"
|
||||||
)
|
)
|
||||||
self.poll_interval: int = int(os.getenv("OPC_POLL_INTERVAL", "2"))
|
self.poll_interval: int = int(os.getenv("OPC_POLL_INTERVAL", "2"))
|
||||||
|
self.signal1_node: str = os.getenv("OPC_SIGNAL1_NODE", "ns=2;s=PL.Signal.EntryCoil")
|
||||||
|
self.signal2_node: str = os.getenv("OPC_SIGNAL2_NODE", "ns=2;s=PL.Signal.WeldDone")
|
||||||
|
self.signal1_last: Optional[int] = None
|
||||||
|
self.signal2_last: Optional[int] = None
|
||||||
|
self.signal2_rise_time: Optional[datetime] = None
|
||||||
|
self.signal1_coils: List[Dict[str, Any]] = []
|
||||||
|
self.current_seq_start: int = 1
|
||||||
|
# 状态机: WAIT_S1=等待信号1, WAIT_S2=等待信号2
|
||||||
|
self.track_state: str = "WAIT_S1"
|
||||||
|
self.last_counter_at_state_change: Optional[Any] = None
|
||||||
# Mapping: oracle_column_name -> OPC node id
|
# Mapping: oracle_column_name -> OPC node id
|
||||||
# Populated from .env or via API
|
# Populated from .env or via API
|
||||||
self.trackmap_nodes: Dict[str, str] = self._load_trackmap_nodes()
|
self.trackmap_nodes: Dict[str, str] = self._load_trackmap_nodes()
|
||||||
@@ -75,6 +88,8 @@ class OpcService:
|
|||||||
self.counter_node = cfg.get("counter_node", self.counter_node)
|
self.counter_node = cfg.get("counter_node", self.counter_node)
|
||||||
self.poll_interval = int(cfg.get("poll_interval", self.poll_interval))
|
self.poll_interval = int(cfg.get("poll_interval", self.poll_interval))
|
||||||
self.trackmap_nodes = cfg.get("trackmap_nodes", self.trackmap_nodes) or {}
|
self.trackmap_nodes = cfg.get("trackmap_nodes", self.trackmap_nodes) or {}
|
||||||
|
self.signal1_node = cfg.get("signal1_node", self.signal1_node)
|
||||||
|
self.signal2_node = cfg.get("signal2_node", self.signal2_node)
|
||||||
self._log(f"Loaded OPC config from {self.config_path}")
|
self._log(f"Loaded OPC config from {self.config_path}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to load OPC config %s: %s", self.config_path, exc)
|
logger.warning("Failed to load OPC config %s: %s", self.config_path, exc)
|
||||||
@@ -86,6 +101,8 @@ class OpcService:
|
|||||||
"counter_node": self.counter_node,
|
"counter_node": self.counter_node,
|
||||||
"poll_interval": self.poll_interval,
|
"poll_interval": self.poll_interval,
|
||||||
"trackmap_nodes": self.trackmap_nodes,
|
"trackmap_nodes": self.trackmap_nodes,
|
||||||
|
"signal1_node": self.signal1_node,
|
||||||
|
"signal2_node": self.signal2_node,
|
||||||
}
|
}
|
||||||
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
||||||
with open(self.config_path, "w", encoding="utf-8") as f:
|
with open(self.config_path, "w", encoding="utf-8") as f:
|
||||||
@@ -134,7 +151,8 @@ class OpcService:
|
|||||||
self._log(f"Connected to OPC server: {self.opc_url}")
|
self._log(f"Connected to OPC server: {self.opc_url}")
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
await self._tick(client)
|
current_counter = await self._tick_with_counter(client)
|
||||||
|
await self._check_signals(client, current_counter)
|
||||||
await asyncio.sleep(self.poll_interval)
|
await asyncio.sleep(self.poll_interval)
|
||||||
finally:
|
finally:
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
@@ -147,17 +165,17 @@ class OpcService:
|
|||||||
self._log("OPC polling stopped")
|
self._log("OPC polling stopped")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
async def _tick(self, client):
|
async def _tick_with_counter(self, client):
|
||||||
"""Read counter; if changed, fetch trackmap nodes and update Oracle."""
|
"""Read counter and update trackmap; return current_counter."""
|
||||||
try:
|
try:
|
||||||
counter_node = client.get_node(self.counter_node)
|
counter_node = client.get_node(self.counter_node)
|
||||||
current_counter = counter_node.get_value()
|
current_counter = counter_node.get_value()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"Failed to read counter node: {exc}")
|
self._log(f"Failed to read counter node: {exc}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
if current_counter == self.last_counter:
|
if current_counter == self.last_counter:
|
||||||
return # nothing changed
|
return current_counter
|
||||||
|
|
||||||
self._log(
|
self._log(
|
||||||
f"Counter changed: {self.last_counter} -> {current_counter}. "
|
f"Counter changed: {self.last_counter} -> {current_counter}. "
|
||||||
@@ -168,7 +186,7 @@ class OpcService:
|
|||||||
|
|
||||||
if not self.trackmap_nodes:
|
if not self.trackmap_nodes:
|
||||||
self._log("No trackmap nodes configured – skipping DB update")
|
self._log("No trackmap nodes configured – skipping DB update")
|
||||||
return
|
return current_counter
|
||||||
|
|
||||||
# Read all configured nodes
|
# Read all configured nodes
|
||||||
data: Dict[str, Any] = {}
|
data: Dict[str, Any] = {}
|
||||||
@@ -180,16 +198,165 @@ class OpcService:
|
|||||||
self._log(f"Failed to read node {node_id}: {exc}")
|
self._log(f"Failed to read node {node_id}: {exc}")
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return current_counter
|
||||||
|
|
||||||
# Determine POSITION from data (must be one of the mapped columns)
|
# Determine POSITION from data (must be one of the mapped columns)
|
||||||
position = data.get("position")
|
position = data.get("position")
|
||||||
if position is None:
|
if position is None:
|
||||||
self._log("'position' not in trackmap_nodes data – cannot update row")
|
self._log("'position' not in trackmap_nodes data – cannot update row")
|
||||||
return
|
return current_counter
|
||||||
|
|
||||||
await self._update_oracle(position, data)
|
await self._update_oracle(position, data)
|
||||||
|
|
||||||
|
return current_counter
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def _check_signals(self, client, current_counter):
|
||||||
|
"""Check signal1 and signal2 with state machine and counter validation."""
|
||||||
|
try:
|
||||||
|
signal1_node = client.get_node(self.signal1_node)
|
||||||
|
signal1_value = signal1_node.get_value()
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"Failed to read signal1 node: {exc}")
|
||||||
|
signal1_value = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
signal2_node = client.get_node(self.signal2_node)
|
||||||
|
signal2_value = signal2_node.get_value()
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"Failed to read signal2 node: {exc}")
|
||||||
|
signal2_value = None
|
||||||
|
|
||||||
|
counter_changed = current_counter != self.last_counter_at_state_change
|
||||||
|
|
||||||
|
# State machine for signal processing
|
||||||
|
if self.track_state == "WAIT_S1":
|
||||||
|
if signal1_value is not None and self.signal1_last is not None:
|
||||||
|
if self.signal1_last == 0 and signal1_value == 1 and counter_changed:
|
||||||
|
self._log(f"Signal1: Entry coil triggered (0->1) with counter change, state={self.track_state}")
|
||||||
|
await self._handle_signal1()
|
||||||
|
self.track_state = "WAIT_S2"
|
||||||
|
self.last_counter_at_state_change = current_counter
|
||||||
|
|
||||||
|
elif self.track_state == "WAIT_S2":
|
||||||
|
if signal2_value is not None and self.signal2_last is not None:
|
||||||
|
if self.signal2_last == 0 and signal2_value == 1 and counter_changed:
|
||||||
|
self.signal2_rise_time = datetime.now()
|
||||||
|
self._log("Signal2: Weld done rising edge (0->1) with counter change")
|
||||||
|
elif self.signal2_last == 1 and signal2_value == 0:
|
||||||
|
self.signal2_rise_time = None
|
||||||
|
|
||||||
|
if signal2_value == 1 and self.signal2_rise_time and counter_changed:
|
||||||
|
elapsed = (datetime.now() - self.signal2_rise_time).total_seconds()
|
||||||
|
if elapsed >= 2.0:
|
||||||
|
self._log(f"Signal2: Held {elapsed:.1f}s, triggering trackmap update")
|
||||||
|
await self._handle_signal2()
|
||||||
|
self.track_state = "WAIT_S1"
|
||||||
|
self.signal2_rise_time = None
|
||||||
|
self.last_counter_at_state_change = current_counter
|
||||||
|
|
||||||
|
if signal1_value is not None:
|
||||||
|
self.signal1_last = signal1_value
|
||||||
|
if signal2_value is not None:
|
||||||
|
self.signal2_last = signal2_value
|
||||||
|
|
||||||
|
async def _handle_signal1(self):
|
||||||
|
"""Handle signal1: fetch next 5 coils from PDI and save to SQLite temp table."""
|
||||||
|
from sqlite_sync import (
|
||||||
|
sqlite_get_max_sequencenb,
|
||||||
|
sqlite_get_coils_by_sequencenb_range,
|
||||||
|
sqlite_save_coils_to_track
|
||||||
|
)
|
||||||
|
|
||||||
|
def _do_fetch():
|
||||||
|
max_seq = sqlite_get_max_sequencenb()
|
||||||
|
if max_seq is None or max_seq < 1:
|
||||||
|
self._log("Signal1: No PDI data available")
|
||||||
|
return
|
||||||
|
|
||||||
|
start_seq = self.current_seq_start
|
||||||
|
end_seq = min(start_seq + 4, max_seq)
|
||||||
|
|
||||||
|
if end_seq < start_seq:
|
||||||
|
self._log(f"Signal1: Insufficient PDI data (max_seq={max_seq}, start={start_seq})")
|
||||||
|
return
|
||||||
|
|
||||||
|
coils = sqlite_get_coils_by_sequencenb_range(start_seq, end_seq)
|
||||||
|
if len(coils) == 0:
|
||||||
|
self._log(f"Signal1: No coils found")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.signal1_coils = coils
|
||||||
|
sqlite_save_coils_to_track(coils)
|
||||||
|
|
||||||
|
self._log(f"Signal1: Saved {len(coils)} coils (seq {start_seq}-{end_seq}) to temp table")
|
||||||
|
|
||||||
|
self.current_seq_start = start_seq + 1
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, _do_fetch)
|
||||||
|
|
||||||
|
async def _handle_signal2(self):
|
||||||
|
"""Handle signal2: update CMPT_PL_TRACKMAP with temp table data."""
|
||||||
|
from sqlite_sync import sqlite_get_coil_track
|
||||||
|
from database import get_connection
|
||||||
|
|
||||||
|
def _do_update():
|
||||||
|
coils = sqlite_get_coil_track()
|
||||||
|
if not coils:
|
||||||
|
self._log("Signal2: No coils in temp track table")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
coil_count = len(coils)
|
||||||
|
for i in range(5):
|
||||||
|
target_position = i + 1
|
||||||
|
coil_index = target_position - 1
|
||||||
|
if coil_index >= 0 and coil_index < coil_count:
|
||||||
|
coil = coils[coil_index]
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = :pos", {"pos": target_position})
|
||||||
|
exists = cursor.fetchone()[0] > 0
|
||||||
|
if exists:
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE PLTM.CMPT_PL_TRACKMAP
|
||||||
|
SET COILID = :coilid, TOM = SYSDATE
|
||||||
|
WHERE POSITION = :position
|
||||||
|
""", {"coilid": coil["coilid"], "position": target_position})
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO PLTM.CMPT_PL_TRACKMAP (POSITION, COILID, TOM)
|
||||||
|
VALUES (:position, :coilid, SYSDATE)
|
||||||
|
""", {"position": target_position, "coilid": coil["coilid"]})
|
||||||
|
else:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM PLTM.CMPT_PL_TRACKMAP WHERE POSITION = :pos", {"pos": target_position})
|
||||||
|
exists = cursor.fetchone()[0] > 0
|
||||||
|
if exists:
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE PLTM.CMPT_PL_TRACKMAP
|
||||||
|
SET COILID = NULL, TOM = SYSDATE
|
||||||
|
WHERE POSITION = :position
|
||||||
|
""", {"position": target_position})
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO PLTM.CMPT_PL_TRACKMAP (POSITION, COILID, TOM)
|
||||||
|
VALUES (:position, NULL, SYSDATE)
|
||||||
|
""", {"position": target_position})
|
||||||
|
conn.commit()
|
||||||
|
self._log(f"Signal2: Updated 5 positions (coils: {coil_count})")
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"Signal2: Oracle update failed: {exc}")
|
||||||
|
import traceback
|
||||||
|
self._log(f"Signal2 traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, _do_update)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
async def _update_oracle(self, position: Any, data: Dict[str, Any]):
|
async def _update_oracle(self, position: Any, data: Dict[str, Any]):
|
||||||
"""Write fetched OPC values into PLTM.CMPT_PL_TRACKMAP."""
|
"""Write fetched OPC values into PLTM.CMPT_PL_TRACKMAP."""
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ CREATE TABLE IF NOT EXISTS PDI_PLTM (
|
|||||||
C REAL, SI REAL, MN REAL, P REAL, S REAL,
|
C REAL, SI REAL, MN REAL, P REAL, S REAL,
|
||||||
CU REAL, NI REAL, CR REAL, MO REAL, V REAL,
|
CU REAL, NI REAL, CR REAL, MO REAL, V REAL,
|
||||||
TI REAL, SOL_AL REAL, FE REAL, NB REAL, N REAL, B REAL,
|
TI REAL, SOL_AL REAL, FE REAL, NB REAL, N REAL, B REAL,
|
||||||
|
MG REAL, PB REAL, SN REAL, ZN REAL, ZR REAL,
|
||||||
|
NA REAL, LI REAL, GA REAL, CA REAL,
|
||||||
|
BE REAL, BI REAL, W REAL,
|
||||||
|
TA REAL, O REAL, H REAL,
|
||||||
|
AR REAL, AG REAL, AS1 REAL, CD REAL, CL REAL,
|
||||||
|
CO REAL, K REAL, SB REAL, SE REAL,
|
||||||
SEND_FLAG TEXT,
|
SEND_FLAG TEXT,
|
||||||
SEND_DATE TEXT,
|
SEND_DATE TEXT,
|
||||||
TRANSACTION_ID TEXT,
|
TRANSACTION_ID TEXT,
|
||||||
@@ -152,6 +158,16 @@ CREATE TABLE IF NOT EXISTS CMPT_PL_TRACKMAP (
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
COIL_TRACK_DDL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS COIL_TRACK_TEMP (
|
||||||
|
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
COILID TEXT NOT NULL,
|
||||||
|
SEQUENCENB INTEGER NOT NULL,
|
||||||
|
ROLLPROGRAMNB INTEGER NOT NULL,
|
||||||
|
CREATED_DT TEXT DEFAULT (datetime('now')),
|
||||||
|
POSITION INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Create tables if they don't exist."""
|
"""Create tables if they don't exist."""
|
||||||
@@ -159,6 +175,7 @@ def init_db():
|
|||||||
try:
|
try:
|
||||||
conn.execute(PDI_DDL)
|
conn.execute(PDI_DDL)
|
||||||
conn.execute(TRACKMAP_DDL)
|
conn.execute(TRACKMAP_DDL)
|
||||||
|
conn.execute(COIL_TRACK_DDL)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("SQLite schema ready: %s", DB_PATH)
|
logger.info("SQLite schema ready: %s", DB_PATH)
|
||||||
finally:
|
finally:
|
||||||
@@ -301,3 +318,107 @@ def sqlite_upsert_trackmap(row: Dict[str, Any]):
|
|||||||
sc.commit()
|
sc.commit()
|
||||||
finally:
|
finally:
|
||||||
sc.close()
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_get_max_sequencenb() -> Optional[int]:
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = sc.execute("SELECT MAX(SEQUENCENB) FROM PDI_PLTM")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row[0] if row and row[0] is not None else None
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_get_coils_by_sequencenb_range(start_seq: int, end_seq: int) -> List[Dict[str, Any]]:
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = sc.execute("""
|
||||||
|
SELECT COILID, SEQUENCENB, ROLLPROGRAMNB
|
||||||
|
FROM PDI_PLTM
|
||||||
|
WHERE SEQUENCENB >= ? AND SEQUENCENB <= ?
|
||||||
|
ORDER BY SEQUENCENB DESC
|
||||||
|
""", (start_seq, end_seq))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [{"coilid": r[0], "sequencenb": r[1], "rollprogramnb": r[2]} for r in rows]
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_save_coils_to_track(coils: List[Dict[str, Any]]):
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
sc.execute("DELETE FROM COIL_TRACK_TEMP")
|
||||||
|
reversed_coils = list(reversed(coils))
|
||||||
|
for i, coil in enumerate(reversed_coils):
|
||||||
|
sc.execute("""
|
||||||
|
INSERT INTO COIL_TRACK_TEMP (COILID, SEQUENCENB, ROLLPROGRAMNB, POSITION)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (coil["coilid"], coil["sequencenb"], coil["rollprogramnb"], i + 1))
|
||||||
|
sc.commit()
|
||||||
|
logger.info(f"Saved {len(coils)} coils to track temp")
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_get_coil_track() -> List[Dict[str, Any]]:
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = sc.execute("""
|
||||||
|
SELECT ID, COILID, SEQUENCENB, ROLLPROGRAMNB, CREATED_DT, POSITION
|
||||||
|
FROM COIL_TRACK_TEMP
|
||||||
|
ORDER BY POSITION ASC
|
||||||
|
""")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [
|
||||||
|
{"id": r[0], "coilid": r[1], "sequencenb": r[2], "rollprogramnb": r[3], "created_dt": r[4], "position": r[5]}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_update_coil_track_item(id: int, coilid: str, sequencenb: int, rollprogramnb: int, position: int):
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
sc.execute("""
|
||||||
|
UPDATE COIL_TRACK_TEMP
|
||||||
|
SET COILID = ?, SEQUENCENB = ?, ROLLPROGRAMNB = ?, POSITION = ?
|
||||||
|
WHERE ID = ?
|
||||||
|
""", (coilid, sequencenb, rollprogramnb, position, id))
|
||||||
|
sc.commit()
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_add_coil_track_item(coilid: str, sequencenb: int, rollprogramnb: int):
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
cursor = sc.execute("SELECT MAX(POSITION) FROM COIL_TRACK_TEMP")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
max_pos = (row[0] or 0) + 1
|
||||||
|
sc.execute("""
|
||||||
|
INSERT INTO COIL_TRACK_TEMP (COILID, SEQUENCENB, ROLLPROGRAMNB, POSITION)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (coilid, sequencenb, rollprogramnb, max_pos))
|
||||||
|
sc.commit()
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_delete_coil_track_item(id: int):
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
sc.execute("DELETE FROM COIL_TRACK_TEMP WHERE ID = ?", (id,))
|
||||||
|
sc.commit()
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|
||||||
|
|
||||||
|
def sqlite_clear_coil_track():
|
||||||
|
sc = get_sqlite()
|
||||||
|
try:
|
||||||
|
sc.execute("DELETE FROM COIL_TRACK_TEMP")
|
||||||
|
sc.commit()
|
||||||
|
finally:
|
||||||
|
sc.close()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default {
|
|||||||
nav: [
|
nav: [
|
||||||
{ path: '/pdi', label: 'PDI 计划管理', icon: 'el-icon-document' },
|
{ path: '/pdi', label: 'PDI 计划管理', icon: 'el-icon-document' },
|
||||||
{ path: '/trackmap', label: '跟踪图监控', icon: 'el-icon-monitor' },
|
{ path: '/trackmap', label: '跟踪图监控', icon: 'el-icon-monitor' },
|
||||||
|
{ path: '/trackcoil', label: '钢卷跟踪管理', icon: 'el-icon-rank' },
|
||||||
{ path: '/opc', label: 'OPC 配置', icon: 'el-icon-setting' }
|
{ path: '/opc', label: 'OPC 配置', icon: 'el-icon-setting' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,3 +43,15 @@ export const gradeApi = {
|
|||||||
getL2ModelGrades: () => http.get('/grades/l2model'),
|
getL2ModelGrades: () => http.get('/grades/l2model'),
|
||||||
getNextNumbers: () => http.get('/pdi/next-numbers')
|
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')
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import VueRouter from 'vue-router'
|
|||||||
import PdiList from './views/PdiList.vue'
|
import PdiList from './views/PdiList.vue'
|
||||||
import TrackMap from './views/TrackMap.vue'
|
import TrackMap from './views/TrackMap.vue'
|
||||||
import OpcConfig from './views/OpcConfig.vue'
|
import OpcConfig from './views/OpcConfig.vue'
|
||||||
|
import TrackCoil from './views/TrackCoil.vue'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ export default new VueRouter({
|
|||||||
{ path: '/', redirect: '/pdi' },
|
{ path: '/', redirect: '/pdi' },
|
||||||
{ path: '/pdi', component: PdiList, meta: { title: 'PDI计划管理' } },
|
{ path: '/pdi', component: PdiList, meta: { title: 'PDI计划管理' } },
|
||||||
{ path: '/trackmap', component: TrackMap, meta: { title: '跟踪图监控' } },
|
{ path: '/trackmap', component: TrackMap, meta: { title: '跟踪图监控' } },
|
||||||
|
{ path: '/trackcoil', component: TrackCoil, meta: { title: '钢卷跟踪管理' } },
|
||||||
{ path: '/opc', component: OpcConfig, meta: { title: 'OPC配置' } }
|
{ path: '/opc', component: OpcConfig, meta: { title: 'OPC配置' } }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</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">入口钢卷信号,0→1触发</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">焊接完成信号,0→1保持2秒触发</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<div class="panel-title" style="margin-top:16px">跟踪图节点映射</div>
|
<div class="panel-title" style="margin-top:16px">跟踪图节点映射</div>
|
||||||
<div style="font-size:12px;color:#888;margin-bottom:10px">
|
<div style="font-size:12px;color:#888;margin-bottom:10px">
|
||||||
Oracle列名 → OPC节点ID。必须包含 <b>position</b> 列。
|
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>服务:<el-tag :type="status.running?'success':'danger'" size="mini">{{ status.running?'运行中':'已停止' }}</el-tag></span>
|
||||||
<span>计数器:<b>{{ status.last_counter??'--' }}</b></span>
|
<span>计数器:<b>{{ status.last_counter??'--' }}</b></span>
|
||||||
<span style="color:#888">更新:{{ status.last_update||'--' }}</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>
|
||||||
<div class="panel-title">最新日志</div>
|
<div class="panel-title">最新日志</div>
|
||||||
<div class="log-box" ref="logBox">
|
<div class="log-box" ref="logBox">
|
||||||
@@ -58,9 +71,9 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false, saving: false,
|
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: [],
|
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
|
statusTimer: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,6 +91,8 @@ export default {
|
|||||||
this.form.opc_url = cfg.opc_url
|
this.form.opc_url = cfg.opc_url
|
||||||
this.form.counter_node = cfg.counter_node
|
this.form.counter_node = cfg.counter_node
|
||||||
this.form.poll_interval = cfg.poll_interval
|
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 }))
|
this.nodeList = Object.entries(cfg.trackmap_nodes || {}).map(([col, node]) => ({ col, node }))
|
||||||
} catch (e) { this.$message.error('加载失败: ' + e.message) }
|
} catch (e) { this.$message.error('加载失败: ' + e.message) }
|
||||||
finally { this.loading = false }
|
finally { this.loading = false }
|
||||||
|
|||||||
@@ -555,7 +555,5 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
v-deep .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
231
frontend/src/views/TrackCoil.vue
Normal file
231
frontend/src/views/TrackCoil.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user