From 03709da1ca99290334143181a1ec3d9446ee8e51 Mon Sep 17 00:00:00 2001 From: wangyu <823267011@qq.com> Date: Mon, 29 Jun 2026 14:10:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(linkage):=20=E7=A7=BB=E5=8A=A8=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BB=BB=E6=84=8F=E5=85=A5=E5=8F=A3=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E9=80=89=E6=8B=A9=EF=BC=8C=E4=BB=85=E4=B8=8A=E5=8D=B7=E9=9E=8D?= =?UTF-8?q?=E5=BA=A7=E8=A7=A6=E5=8F=91=E7=94=9F=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 计划新增 position 字段;新增 /plan/{id}/move?position=… 与 /plan/positions/all - line_service.place_at_position:放到任意位置(位置唯一占用),上卷鞍座单独触发生产联动 - 入口跟踪:新增入口位置图(单一鞍座)显示占位;移动按钮弹出位置选择框 - 计划管理:移动按钮同样弹出位置选择框 Co-Authored-By: Claude Opus 4.8 --- backend/app/api/plan.py | 22 ++++ backend/app/database.py | 3 +- backend/app/models/plan.py | 3 +- backend/app/schemas/plan.py | 3 +- backend/app/services/line_service.py | 35 ++++++ frontend/src/api/index.js | 4 +- frontend/src/views/EntryTracking.vue | 167 ++++++++++++++++++--------- frontend/src/views/Plan.vue | 64 ++++++++-- 8 files changed, 233 insertions(+), 68 deletions(-) diff --git a/backend/app/api/plan.py b/backend/app/api/plan.py index fcbe658..2aa5a02 100644 --- a/backend/app/api/plan.py +++ b/backend/app/api/plan.py @@ -142,6 +142,28 @@ async def move_to_saddle(plan_id: int, db: AsyncSession = Depends(get_db), _ = D return Response.ok(PlanOut.model_validate(plan)) +@router.get("/positions/all", response_model=Response[list[str]]) +async def list_positions(_ = Depends(get_current_user)): + """可移动到的入口位置列表(含唯一上卷鞍座)。""" + return Response.ok(line_service.ENTRY_POSITIONS) + + +@router.patch("/{plan_id}/move", response_model=Response[PlanOut]) +async def move_plan(plan_id: int, position: str = Query(...), db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)): + """移动:把计划放到所选入口位置;放到上卷鞍座才触发生产联动。""" + result = await db.execute(select(ProductionPlan).where(ProductionPlan.id == plan_id)) + plan = result.scalar_one_or_none() + if not plan: + raise HTTPException(status_code=404, detail="计划不存在") + try: + await line_service.place_at_position(db, plan, position) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + await db.flush() + await db.refresh(plan) + return Response.ok(PlanOut.model_validate(plan)) + + @router.patch("/{plan_id}/commit", response_model=Response[PlanOut]) async def commit_producing(plan_id: int, db: AsyncSession = Depends(get_db), _ = Depends(get_current_user)): """投入生产:把鞍座上的计划置为生产中(兜底未实时变化的数据)。""" diff --git a/backend/app/database.py b/backend/app/database.py index 805f279..174976e 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -59,7 +59,8 @@ async def _run_migrations(conn): "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_weight DOUBLE PRECISION", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_od DOUBLE PRECISION", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS split_weights JSONB", - # 上卷鞍座 / 生产联动字段 + # 入口位置 / 上卷鞍座 / 生产联动字段 + "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS position VARCHAR(30)", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS on_saddle INTEGER DEFAULT 0", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS saddle_at TIMESTAMP", "ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS run_started_at TIMESTAMP", diff --git a/backend/app/models/plan.py b/backend/app/models/plan.py index ea677e1..5b6746f 100644 --- a/backend/app/models/plan.py +++ b/backend/app/models/plan.py @@ -35,7 +35,8 @@ class ProductionPlan(Base): incoming_od = Column(Float, comment="来料外径 mm") split_weights = Column(JSON, comment="分卷重量 [t,...]") - # 上卷鞍座 / 生产联动 + # 入口位置 / 上卷鞍座 / 生产联动 + position = Column(String(30), comment="当前入口位置(上卷小车/称重位/地辊/倒卷小车/上卷鞍座)") on_saddle = Column(Integer, default=0, comment="是否在上卷鞍座 0/1") saddle_at = Column(DateTime, comment="移动到鞍座时间") run_started_at = Column(DateTime, comment="投入生产(有速度)时间") diff --git a/backend/app/schemas/plan.py b/backend/app/schemas/plan.py index 3e685c0..f6da6ad 100644 --- a/backend/app/schemas/plan.py +++ b/backend/app/schemas/plan.py @@ -78,7 +78,8 @@ class PlanOut(BaseModel): remark: Optional[str] = None created_by: Optional[str] = None created_at: datetime - # 上卷鞍座 / 生产联动 + # 入口位置 / 上卷鞍座 / 生产联动 + position: Optional[str] = None on_saddle: Optional[int] = 0 saddle_at: Optional[datetime] = None run_started_at: Optional[datetime] = None diff --git a/backend/app/services/line_service.py b/backend/app/services/line_service.py index c447ae9..b7aa844 100644 --- a/backend/app/services/line_service.py +++ b/backend/app/services/line_service.py @@ -17,12 +17,45 @@ from app.models.production import ProductionRecord from app.models.downtime import DowntimeRecord from app.models.line_state import LineState +# ── 入口位置 ── +SADDLE_NAME = "上卷鞍座" # 唯一会触发生产联动的位置 +ENTRY_POSITIONS = [ + "1#上卷小车", "2#上卷小车", + "1#称重位", "2#称重位", + "1#地辊", "2#地辊", + "1#倒卷小车", "2#倒卷小车", + SADDLE_NAME, +] + # ── 仿真参数 ── TARGET_LENGTH_M = 2000.0 # 带头目标长度(生产完成阈值) SIM_SPEED_M_MIN = 600.0 # 仿真线速度 m/min(2000m ≈ 200s) DOWNTIME_THRESHOLD_S = 600 # 速度为0持续超过该秒数判定停机(10min) +async def place_at_position(db: AsyncSession, plan: ProductionPlan, position: str): + """把计划放到任意入口位置;位置唯一占用。上卷鞍座单独触发生产联动。""" + if position not in ENTRY_POSITIONS: + raise ValueError("未知入口位置") + if position == SADDLE_NAME: + await move_to_saddle(db, plan) + return + # 普通位置:清除同位置上的其它计划,移出鞍座(若在) + res = await db.execute( + select(ProductionPlan).where(ProductionPlan.position == position, ProductionPlan.id != plan.id) + ) + for o in res.scalars(): + o.position = None + plan.on_saddle = 0 + plan.run_started_at = None + plan.run_speed = 0 + plan.run_length_m = 0 + plan.position = position + if plan.status == "producing": + plan.status = "online" + await ensure_online(db) + + def _shift_of(dt: datetime) -> str: return "甲" if 8 <= dt.hour < 20 else "乙" @@ -60,6 +93,7 @@ async def move_to_saddle(db: AsyncSession, plan: ProductionPlan): if occupied and occupied.id != plan.id: raise ValueError("上卷鞍座已被占用,请等待当前钢卷生产完成") plan.on_saddle = 1 + plan.position = SADDLE_NAME plan.saddle_at = datetime.now() plan.run_started_at = None plan.run_speed = 0 @@ -86,6 +120,7 @@ async def _produce(db: AsyncSession, plan: ProductionPlan): plan.status = "produced" plan.produced_at = now plan.on_saddle = 0 + plan.position = None plan.run_speed = 0 plan.run_length_m = TARGET_LENGTH_M diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index a0f3ff5..a9cc5e1 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -20,8 +20,10 @@ export const getPlans = params => request.get('/plan/', { params }) export const createPlan = data => request.post('/plan/', data) export const updatePlan = (id, data) => request.put(`/plan/${id}`, data) export const confirmPlan = id => request.patch(`/plan/${id}/confirm`) -export const startProducing = id => request.patch(`/plan/${id}/start`) // 移动到上卷鞍座 +export const startProducing = id => request.patch(`/plan/${id}/start`) // 移动到上卷鞍座(兼容) export const moveToSaddle = id => request.patch(`/plan/${id}/start`) +export const movePlan = (id, position) => request.patch(`/plan/${id}/move`, null, { params: { position } }) +export const getPositions = () => request.get('/plan/positions/all') export const commitProducing = id => request.patch(`/plan/${id}/commit`) // 投入生产 export const getSaddle = () => request.get('/plan/saddle/current') export const seedPlans = (count = 50) => request.post('/plan/seed', null, { params: { count } }) diff --git a/frontend/src/views/EntryTracking.vue b/frontend/src/views/EntryTracking.vue index 4cfe2c5..eaa1ae6 100644 --- a/frontend/src/views/EntryTracking.vue +++ b/frontend/src/views/EntryTracking.vue @@ -51,34 +51,31 @@
- 上卷鞍座空闲 — 从下方队列点击「移动」把在线计划推到鞍座 + 上卷鞍座空闲 — 从队列点击「移动」,在弹窗中选择「上卷鞍座」即可开始生产
- +
-
- 待上卷计划 - 在线/准备 {{ queuePlans.length }} 条 -
+
入口位置 {{ positions.length }} 个工位
-
-
-
- {{ p.cold_coil_no || p.plan_no }} - {{ statusLabel(p.status) }} +
+
+
{{ pos }}生产
+
+
{{ occupantOf(pos).cold_coil_no || occupantOf(pos).plan_no }}
+
{{ occupantOf(pos).steel_grade || '—' }}
+
{{ fmt(occupantOf(pos).product_thickness, 2) }}×{{ fmt(occupantOf(pos).product_width, 0) }}
+ {{ statusLabel(occupantOf(pos).status) }}
-
-
钢种{{ p.steel_grade || '—' }}
-
规格{{ fmt(p.product_thickness, 2) }}×{{ fmt(p.product_width, 0) }}
-
重量[t]{{ fmt(p.incoming_weight, 3) }}
-
轧制模式{{ p.rolling_mode || '—' }}
-
- +
-
暂无在线/准备好的计划
@@ -93,7 +90,7 @@ 序号冷卷号热卷号钢种 - 规格(厚×宽)来料重量轧制模式状态操作 + 规格(厚×宽)来料重量轧制模式状态当前位置操作 @@ -105,44 +102,80 @@ {{ fmt(p.product_thickness, 2) }} × {{ fmt(p.product_width, 0) }} {{ fmt(p.incoming_weight, 2) }} {{ p.rolling_mode || '—' }} - {{ statusLabel(p.status) }} - 移动 + {{ statusLabel(p.status) }} + {{ p.position || '—' }} + 移动 - 暂无在线/准备好的计划 + 暂无在线/准备好的计划
+ + +