feat: 同步本地未提交的前后端更新(plan/quality/material/inspection/production 等模块)

This commit is contained in:
2026-06-20 18:19:06 +08:00
parent 970afe10b4
commit db3945c263
19 changed files with 1681 additions and 961 deletions

View File

@@ -5,7 +5,7 @@ from typing import Optional
from datetime import datetime from datetime import datetime
from app.database import get_db from app.database import get_db
from app.models.plan import ProductionPlan, PlanStatus from app.models.plan import ProductionPlan
from app.schemas.plan import PlanCreate, PlanUpdate, PlanOut from app.schemas.plan import PlanCreate, PlanUpdate, PlanOut
from app.schemas.common import Response, PageResponse from app.schemas.common import Response, PageResponse
from app.services.auth_service import get_current_user from app.services.auth_service import get_current_user
@@ -34,10 +34,7 @@ async def list_plans(
): ):
query = select(ProductionPlan).order_by(desc(ProductionPlan.plan_date)) query = select(ProductionPlan).order_by(desc(ProductionPlan.plan_date))
if status: if status:
try: query = query.where(ProductionPlan.status == status)
query = query.where(ProductionPlan.status == PlanStatus(status))
except ValueError:
pass
_sd = _parse_dt(start_date) _sd = _parse_dt(start_date)
if _sd: if _sd:
query = query.where(ProductionPlan.plan_date >= _sd) query = query.where(ProductionPlan.plan_date >= _sd)
@@ -98,6 +95,6 @@ async def confirm_plan(plan_id: int, db: AsyncSession = Depends(get_db), _ = Dep
plan = result.scalar_one_or_none() plan = result.scalar_one_or_none()
if not plan: if not plan:
raise HTTPException(status_code=404, detail="计划不存在") raise HTTPException(status_code=404, detail="计划不存在")
plan.status = PlanStatus.CONFIRMED plan.status = "online"
await db.flush() await db.flush()
return Response.ok(PlanOut.model_validate(plan)) return Response.ok(PlanOut.model_validate(plan))

View File

@@ -9,7 +9,7 @@ from app.models.quality import QcTask, QcTaskItem, QcDefect
from app.schemas.quality import ( from app.schemas.quality import (
QcTaskCreate, QcTaskUpdate, QcTaskOut, QcTaskCreate, QcTaskUpdate, QcTaskOut,
QcTaskItemCreate, QcTaskItemUpdate, QcTaskItemOut, QcTaskItemCreate, QcTaskItemUpdate, QcTaskItemOut,
QcDefectCreate, QcDefectUpdate, QcDefectOut, QcDefectCreate, QcDefectUpdate, QcDefectOut, QcDefectBulkSave,
) )
from app.schemas.common import Response, PageResponse from app.schemas.common import Response, PageResponse
from app.services.auth_service import get_current_user from app.services.auth_service import get_current_user
@@ -158,6 +158,41 @@ async def create_defect(body: QcDefectCreate, db: AsyncSession = Depends(get_db)
return Response.ok(QcDefectOut.model_validate(defect)) return Response.ok(QcDefectOut.model_validate(defect))
@router.get("/defects/by-coil/{coil_no}", response_model=Response[list[QcDefectOut]])
async def list_defects_by_coil(coil_no: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(
select(QcDefect)
.where(QcDefect.coil_no == coil_no, QcDefect.del_flag == 0)
.order_by(QcDefect.seq_no.asc().nulls_last(), QcDefect.id.asc())
)
return Response.ok([QcDefectOut.model_validate(d) for d in r.scalars()])
@router.post("/defects/bulk-save", response_model=Response[list[QcDefectOut]])
async def bulk_save_defects(body: QcDefectBulkSave, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
"""按卷号替换全部缺陷记录(软删旧记录后批量插入)"""
coil_no = body.coil_no
# 软删旧记录
old = await db.execute(select(QcDefect).where(QcDefect.coil_no == coil_no, QcDefect.del_flag == 0))
for d in old.scalars():
d.del_flag = 1
# 插入新记录
saved: list[QcDefect] = []
for idx, item in enumerate(body.defects, start=1):
data = item.model_dump()
data["coil_no"] = coil_no
if not data.get("seq_no"):
data["seq_no"] = idx
# 自动算长度
if data.get("start_position") is not None and data.get("end_position") is not None and not data.get("length_val"):
data["length_val"] = round(float(data["end_position"]) - float(data["start_position"]), 3)
d = QcDefect(**data)
db.add(d)
saved.append(d)
await db.flush()
return Response.ok([QcDefectOut.model_validate(d) for d in saved])
@router.put("/defects/{defect_id}", response_model=Response[QcDefectOut]) @router.put("/defects/{defect_id}", response_model=Response[QcDefectOut])
async def update_defect(defect_id: int, body: QcDefectUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): async def update_defect(defect_id: int, body: QcDefectUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id, QcDefect.del_flag == 0)) r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id, QcDefect.del_flag == 0))

View File

@@ -31,3 +31,69 @@ async def get_db():
async def init_db(): async def init_db():
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
await _run_migrations(conn)
async def _run_migrations(conn):
"""Postgres 专用:为已存在表追加新列(幂等)"""
from sqlalchemy import text
is_pg = engine.dialect.name == "postgresql"
if not is_pg:
return
statements = [
# production_plans 新字段
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS cold_coil_no VARCHAR(30)",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS hot_coil_no VARCHAR(30)",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_thickness DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS product_thickness DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS deviation_upper DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS deviation_lower DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS incoming_width DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS product_width DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS packaging_req VARCHAR(30)",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS trim_req VARCHAR(30)",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS rolling_mode VARCHAR(30)",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS split_count INTEGER DEFAULT 1",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS coil_diameter DOUBLE PRECISION",
"ALTER TABLE production_plans ADD COLUMN IF NOT EXISTS next_process VARCHAR(30)",
# 状态列改为 VARCHAR 以适配新值
"ALTER TABLE production_plans ALTER COLUMN status TYPE VARCHAR(20) USING status::text",
# production_records 新字段
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS sub_coil_no VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS hot_coil_no VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS team VARCHAR(10)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS steel_grade VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS incoming_thickness DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS deviation_upper DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS deviation_lower DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS incoming_width DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS outlet_width DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS incoming_weight DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS weighed_weight DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS packaging_req VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS trim_req VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS surface_quality VARCHAR(30)",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS product_quality DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS product_length DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS length_per_ton DOUBLE PRECISION",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS offline_time TIMESTAMP",
"ALTER TABLE production_records ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'UNWEIGH'",
# qc_defect 新字段
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS seq_no INTEGER",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS defect_desc VARCHAR(200)",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS start_position DOUBLE PRECISION",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS end_position DOUBLE PRECISION",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS length_val DOUBLE PRECISION",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS upper_surface BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS lower_surface BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS side_op BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS side_middle BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS side_drive BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS is_main BOOLEAN DEFAULT FALSE",
"ALTER TABLE qc_defect ADD COLUMN IF NOT EXISTS image_url VARCHAR(255)",
]
for s in statements:
try:
await conn.execute(text(s))
except Exception:
pass

View File

@@ -1,14 +1,9 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Enum, Text, func from sqlalchemy import Column, Integer, String, Float, DateTime, Text, func
from app.database import Base from app.database import Base
import enum
class PlanStatus(str, enum.Enum): # 计划状态:准备好/在线/生产中/产出
DRAFT = "draft" # 草稿 PLAN_STATUS = ("ready", "online", "producing", "produced")
CONFIRMED = "confirmed" # 已确认
IN_PROGRESS = "in_progress" # 执行中
COMPLETED = "completed" # 完成
CANCELLED = "cancelled" # 取消
class ProductionPlan(Base): class ProductionPlan(Base):
@@ -17,16 +12,34 @@ class ProductionPlan(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
plan_no = Column(String(30), unique=True, nullable=False, index=True, comment="计划号") plan_no = Column(String(30), unique=True, nullable=False, index=True, comment="计划号")
plan_date = Column(DateTime, nullable=False, comment="计划日期") plan_date = Column(DateTime, nullable=False, comment="计划时间")
status = Column(String(20), default="ready", comment="状态: ready/online/producing/produced")
# 新结构:卷号 / 钢种 / 厚宽 / 偏差 / 工艺
cold_coil_no = Column(String(30), index=True, comment="冷卷号")
hot_coil_no = Column(String(30), index=True, comment="热卷号")
steel_grade = Column(String(30), comment="钢种")
incoming_thickness = Column(Float, comment="来料厚度 mm")
product_thickness = Column(Float, comment="产品厚度 mm")
deviation_upper = Column(Float, comment="偏差上限 mm")
deviation_lower = Column(Float, comment="偏差下限 mm")
incoming_width = Column(Float, comment="来料宽度 mm")
product_width = Column(Float, comment="产品宽度 mm")
packaging_req = Column(String(30), comment="包装要求")
trim_req = Column(String(30), comment="切边要求")
rolling_mode = Column(String(30), comment="轧制模式")
coil_diameter = Column(Float, comment="卷径 mm")
split_count = Column(Integer, default=1, comment="分卷数")
next_process = Column(String(30), comment="下工序")
# 兼容历史字段
shift = Column(String(10), comment="班次") shift = Column(String(10), comment="班次")
plan_quantity = Column(Integer, default=0, comment="计划数量(卷)") plan_quantity = Column(Integer, default=0)
plan_weight = Column(Float, default=0, comment="计划重量kg") plan_weight = Column(Float, default=0)
actual_quantity = Column(Integer, default=0, comment="实际数量(卷)") actual_quantity = Column(Integer, default=0)
actual_weight = Column(Float, default=0, comment="实际重量kg") actual_weight = Column(Float, default=0)
status = Column(Enum(PlanStatus), default=PlanStatus.DRAFT) spec_range = Column(String(50))
steel_grade = Column(String(30), comment="主要钢种") priority = Column(Integer, default=5)
spec_range = Column(String(50), comment="规格范围")
priority = Column(Integer, default=5, comment="优先级1-10")
remark = Column(Text) remark = Column(Text)
created_by = Column(String(50)) created_by = Column(String(50))
created_at = Column(DateTime, server_default=func.now()) created_at = Column(DateTime, server_default=func.now())

View File

@@ -7,21 +7,42 @@ class ProductionRecord(Base):
__tablename__ = "production_records" __tablename__ = "production_records"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), nullable=False, index=True) coil_no = Column(String(30), nullable=False, index=True) # 兼容旧字段,等同于 sub_coil_no
sub_coil_no = Column(String(30), index=True, comment="子卷号")
hot_coil_no = Column(String(30), index=True, comment="热卷号")
plan_id = Column(Integer, ForeignKey("production_plans.id"), nullable=True) plan_id = Column(Integer, ForeignKey("production_plans.id"), nullable=True)
shift = Column(String(10), comment="次: 甲/乙/丙/丁") shift = Column(String(10), comment="")
shift_date = Column(DateTime, comment="班期") team = Column(String(10), comment="")
start_time = Column(DateTime, comment="开始时间") steel_grade = Column(String(30), comment="钢种")
end_time = Column(DateTime, comment="结束时间") incoming_thickness = Column(Float, comment="来料厚度 mm")
process_length = Column(Float, comment="处理长度m") outlet_thickness = Column(Float, comment="出口厚度 mm")
process_weight = Column(Float, comment="处理重量kg") deviation_upper = Column(Float, comment="偏差上限")
avg_speed = Column(Float, comment="平均速度m/min") deviation_lower = Column(Float, comment="偏差下限")
max_speed = Column(Float, comment="最大速度m/min") incoming_width = Column(Float, comment="来料宽度 mm")
acid_consumption = Column(Float, comment="酸耗量L") outlet_width = Column(Float, comment="出口宽度 mm")
inlet_thickness = Column(Float, comment="入口厚度mm") incoming_weight = Column(Float, comment="来料重量 t")
outlet_thickness = Column(Float, comment="出口厚度mm") weighed_weight = Column(Float, comment="称重重量 t")
inlet_width = Column(Float, comment="入口宽度mm") packaging_req = Column(String(30), comment="包装要求")
quality_grade = Column(String(10), comment="质量等级") trim_req = Column(String(30), comment="切边要求")
surface_quality = Column(String(30), comment="表面质量")
product_quality = Column(Float, comment="成品质量 %")
product_length = Column(Float, comment="成品长度 m")
length_per_ton = Column(Float, comment="吨钢长度 m/t")
offline_time = Column(DateTime, comment="下线时间")
status = Column(String(20), default="UNWEIGH", comment="状态: UNWEIGH/PRODUCT")
# 兼容历史字段
shift_date = Column(DateTime)
start_time = Column(DateTime)
end_time = Column(DateTime)
process_length = Column(Float)
process_weight = Column(Float)
avg_speed = Column(Float)
max_speed = Column(Float)
acid_consumption = Column(Float)
inlet_thickness = Column(Float)
inlet_width = Column(Float)
quality_grade = Column(String(10))
operator = Column(String(50)) operator = Column(String(50))
remark = Column(Text) remark = Column(Text)
created_at = Column(DateTime, server_default=func.now()) created_at = Column(DateTime, server_default=func.now())

View File

@@ -46,6 +46,20 @@ class QcDefect(Base):
__tablename__ = "qc_defect" __tablename__ = "qc_defect"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
coil_no = Column(String(30), nullable=True, index=True) coil_no = Column(String(30), nullable=True, index=True)
seq_no = Column(Integer, nullable=True, comment="序号")
defect_desc = Column(String(200), nullable=True, comment="缺陷描述")
start_position = Column(Float, nullable=True, comment="开始位置")
end_position = Column(Float, nullable=True, comment="结束位置")
length_val = Column(Float, nullable=True, comment="长度")
upper_surface = Column(Boolean, default=False, comment="上板面")
lower_surface = Column(Boolean, default=False, comment="下板面")
side_op = Column(Boolean, default=False, comment="操作侧")
side_middle = Column(Boolean, default=False, comment="中间")
side_drive = Column(Boolean, default=False, comment="驱动侧")
is_main = Column(Boolean, default=False, comment="主缺陷")
image_url = Column(String(255), nullable=True, comment="缺陷图片URL")
# 兼容旧字段
production_line = Column(String(50), nullable=True) production_line = Column(String(50), nullable=True)
position = Column(String(50), nullable=True) position = Column(String(50), nullable=True)
plate_surface = Column(String(20), nullable=True) plate_surface = Column(String(20), nullable=True)
@@ -53,7 +67,7 @@ class QcDefect(Base):
defect_type = Column(String(50), nullable=True, index=True) defect_type = Column(String(50), nullable=True, index=True)
defect_rate = Column(Float, nullable=True) defect_rate = Column(Float, nullable=True)
defect_weight = Column(Float, nullable=True) defect_weight = Column(Float, nullable=True)
degree = Column(String(20), nullable=True) # light/normal/serious degree = Column(String(20), nullable=True) # light/medium/serious
judge_level = Column(String(20), nullable=True) judge_level = Column(String(20), nullable=True)
judge_by = Column(String(50), nullable=True) judge_by = Column(String(50), nullable=True)
judge_time = Column(DateTime, nullable=True) judge_time = Column(DateTime, nullable=True)

View File

@@ -1,30 +1,48 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
from app.models.plan import PlanStatus
class PlanCreate(BaseModel): class PlanCreate(BaseModel):
plan_no: str plan_no: str
plan_date: datetime plan_date: datetime
shift: Optional[str] = None cold_coil_no: Optional[str] = None
plan_quantity: int = 0 hot_coil_no: Optional[str] = None
plan_weight: float = 0
steel_grade: Optional[str] = None steel_grade: Optional[str] = None
spec_range: Optional[str] = None incoming_thickness: Optional[float] = None
priority: int = 5 product_thickness: Optional[float] = None
deviation_upper: Optional[float] = None
deviation_lower: Optional[float] = None
incoming_width: Optional[float] = None
product_width: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
rolling_mode: Optional[str] = None
coil_diameter: Optional[float] = None
split_count: Optional[int] = 1
next_process: Optional[str] = None
status: Optional[str] = "ready"
remark: Optional[str] = None remark: Optional[str] = None
class PlanUpdate(BaseModel): class PlanUpdate(BaseModel):
plan_date: Optional[datetime] = None plan_date: Optional[datetime] = None
shift: Optional[str] = None cold_coil_no: Optional[str] = None
plan_quantity: Optional[int] = None hot_coil_no: Optional[str] = None
plan_weight: Optional[float] = None steel_grade: Optional[str] = None
actual_quantity: Optional[int] = None incoming_thickness: Optional[float] = None
actual_weight: Optional[float] = None product_thickness: Optional[float] = None
status: Optional[PlanStatus] = None deviation_upper: Optional[float] = None
priority: Optional[int] = None deviation_lower: Optional[float] = None
incoming_width: Optional[float] = None
product_width: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
rolling_mode: Optional[str] = None
coil_diameter: Optional[float] = None
split_count: Optional[int] = None
next_process: Optional[str] = None
status: Optional[str] = None
remark: Optional[str] = None remark: Optional[str] = None
@@ -32,16 +50,24 @@ class PlanOut(BaseModel):
id: int id: int
plan_no: str plan_no: str
plan_date: datetime plan_date: datetime
shift: Optional[str] status: Optional[str] = None
plan_quantity: int cold_coil_no: Optional[str] = None
plan_weight: float hot_coil_no: Optional[str] = None
actual_quantity: int steel_grade: Optional[str] = None
actual_weight: float incoming_thickness: Optional[float] = None
status: PlanStatus product_thickness: Optional[float] = None
steel_grade: Optional[str] deviation_upper: Optional[float] = None
spec_range: Optional[str] deviation_lower: Optional[float] = None
priority: int incoming_width: Optional[float] = None
created_by: Optional[str] product_width: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
rolling_mode: Optional[str] = None
coil_diameter: Optional[float] = None
split_count: Optional[int] = 1
next_process: Optional[str] = None
remark: Optional[str] = None
created_by: Optional[str] = None
created_at: datetime created_at: datetime
class Config: class Config:

View File

@@ -5,52 +5,82 @@ from datetime import datetime
class ProductionRecordCreate(BaseModel): class ProductionRecordCreate(BaseModel):
coil_no: str coil_no: str
sub_coil_no: Optional[str] = None
hot_coil_no: Optional[str] = None
plan_id: Optional[int] = None plan_id: Optional[int] = None
shift: Optional[str] = None shift: Optional[str] = None
shift_date: Optional[datetime] = None team: Optional[str] = None
start_time: Optional[datetime] = None steel_grade: Optional[str] = None
end_time: Optional[datetime] = None incoming_thickness: Optional[float] = None
process_length: Optional[float] = None
process_weight: Optional[float] = None
avg_speed: Optional[float] = None
max_speed: Optional[float] = None
acid_consumption: Optional[float] = None
inlet_thickness: Optional[float] = None
outlet_thickness: Optional[float] = None outlet_thickness: Optional[float] = None
inlet_width: Optional[float] = None deviation_upper: Optional[float] = None
quality_grade: Optional[str] = None deviation_lower: Optional[float] = None
operator: Optional[str] = None incoming_width: Optional[float] = None
outlet_width: Optional[float] = None
incoming_weight: Optional[float] = None
weighed_weight: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
surface_quality: Optional[str] = None
product_quality: Optional[float] = None
product_length: Optional[float] = None
length_per_ton: Optional[float] = None
offline_time: Optional[datetime] = None
status: Optional[str] = "UNWEIGH"
remark: Optional[str] = None remark: Optional[str] = None
class ProductionRecordUpdate(BaseModel): class ProductionRecordUpdate(BaseModel):
sub_coil_no: Optional[str] = None
hot_coil_no: Optional[str] = None
shift: Optional[str] = None shift: Optional[str] = None
end_time: Optional[datetime] = None team: Optional[str] = None
process_length: Optional[float] = None steel_grade: Optional[str] = None
process_weight: Optional[float] = None incoming_thickness: Optional[float] = None
avg_speed: Optional[float] = None outlet_thickness: Optional[float] = None
acid_consumption: Optional[float] = None deviation_upper: Optional[float] = None
quality_grade: Optional[str] = None deviation_lower: Optional[float] = None
incoming_width: Optional[float] = None
outlet_width: Optional[float] = None
incoming_weight: Optional[float] = None
weighed_weight: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
surface_quality: Optional[str] = None
product_quality: Optional[float] = None
product_length: Optional[float] = None
length_per_ton: Optional[float] = None
offline_time: Optional[datetime] = None
status: Optional[str] = None
remark: Optional[str] = None remark: Optional[str] = None
class ProductionRecordOut(BaseModel): class ProductionRecordOut(BaseModel):
id: int id: int
coil_no: str coil_no: str
plan_id: Optional[int] sub_coil_no: Optional[str] = None
shift: Optional[str] hot_coil_no: Optional[str] = None
shift_date: Optional[datetime] plan_id: Optional[int] = None
start_time: Optional[datetime] shift: Optional[str] = None
end_time: Optional[datetime] team: Optional[str] = None
process_length: Optional[float] steel_grade: Optional[str] = None
process_weight: Optional[float] incoming_thickness: Optional[float] = None
avg_speed: Optional[float] outlet_thickness: Optional[float] = None
max_speed: Optional[float] deviation_upper: Optional[float] = None
acid_consumption: Optional[float] deviation_lower: Optional[float] = None
inlet_thickness: Optional[float] incoming_width: Optional[float] = None
outlet_thickness: Optional[float] outlet_width: Optional[float] = None
quality_grade: Optional[str] incoming_weight: Optional[float] = None
operator: Optional[str] weighed_weight: Optional[float] = None
packaging_req: Optional[str] = None
trim_req: Optional[str] = None
surface_quality: Optional[str] = None
product_quality: Optional[float] = None
product_length: Optional[float] = None
length_per_ton: Optional[float] = None
offline_time: Optional[datetime] = None
status: Optional[str] = None
remark: Optional[str] = None
created_at: datetime created_at: datetime
class Config: class Config:

View File

@@ -85,8 +85,78 @@ class QcTaskOut(BaseModel):
from_attributes = True from_attributes = True
class QcDefectCreate(BaseModel): class QcDefectBase(BaseModel):
coil_no: Optional[str] = None coil_no: Optional[str] = None
seq_no: Optional[int] = None
defect_desc: Optional[str] = None
start_position: Optional[float] = None
end_position: Optional[float] = None
length_val: Optional[float] = None
upper_surface: Optional[bool] = False
lower_surface: Optional[bool] = False
side_op: Optional[bool] = False
side_middle: Optional[bool] = False
side_drive: Optional[bool] = False
is_main: Optional[bool] = False
image_url: Optional[str] = None
defect_code: Optional[str] = None
defect_type: Optional[str] = None
degree: Optional[str] = None
remark: Optional[str] = None
class QcDefectCreate(QcDefectBase):
production_line: Optional[str] = None
position: Optional[str] = None
plate_surface: Optional[str] = None
defect_rate: Optional[float] = None
defect_weight: Optional[float] = None
judge_level: Optional[str] = None
judge_by: Optional[str] = None
judge_time: Optional[datetime] = None
main_mark: Optional[int] = None
whole_coil_mark: Optional[int] = None
class QcDefectUpdate(BaseModel):
seq_no: Optional[int] = None
defect_desc: Optional[str] = None
start_position: Optional[float] = None
end_position: Optional[float] = None
length_val: Optional[float] = None
upper_surface: Optional[bool] = None
lower_surface: Optional[bool] = None
side_op: Optional[bool] = None
side_middle: Optional[bool] = None
side_drive: Optional[bool] = None
is_main: Optional[bool] = None
image_url: Optional[str] = None
defect_code: Optional[str] = None
defect_type: Optional[str] = None
degree: Optional[str] = None
remark: Optional[str] = None
class QcDefectBulkSave(BaseModel):
coil_no: str
defects: List[QcDefectCreate]
class QcDefectOut(BaseModel):
id: int
coil_no: Optional[str]
seq_no: Optional[int] = None
defect_desc: Optional[str] = None
start_position: Optional[float] = None
end_position: Optional[float] = None
length_val: Optional[float] = None
upper_surface: Optional[bool] = None
lower_surface: Optional[bool] = None
side_op: Optional[bool] = None
side_middle: Optional[bool] = None
side_drive: Optional[bool] = None
is_main: Optional[bool] = None
image_url: Optional[str] = None
production_line: Optional[str] = None production_line: Optional[str] = None
position: Optional[str] = None position: Optional[str] = None
plate_surface: Optional[str] = None plate_surface: Optional[str] = None
@@ -101,36 +171,6 @@ class QcDefectCreate(BaseModel):
main_mark: Optional[int] = None main_mark: Optional[int] = None
whole_coil_mark: Optional[int] = None whole_coil_mark: Optional[int] = None
remark: Optional[str] = None remark: Optional[str] = None
class QcDefectUpdate(BaseModel):
defect_type: Optional[str] = None
defect_rate: Optional[float] = None
defect_weight: Optional[float] = None
degree: Optional[str] = None
judge_level: Optional[str] = None
judge_by: Optional[str] = None
judge_time: Optional[datetime] = None
remark: Optional[str] = None
class QcDefectOut(BaseModel):
id: int
coil_no: Optional[str]
production_line: Optional[str]
position: Optional[str]
plate_surface: Optional[str]
defect_code: Optional[str]
defect_type: Optional[str]
defect_rate: Optional[float]
defect_weight: Optional[float]
degree: Optional[str]
judge_level: Optional[str]
judge_by: Optional[str]
judge_time: Optional[datetime]
main_mark: Optional[int]
whole_coil_mark: Optional[int]
remark: Optional[str]
created_at: datetime created_at: datetime
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -16,7 +16,8 @@
"echarts": "^5.5.0", "echarts": "^5.5.0",
"vue-echarts": "^6.7.3", "vue-echarts": "^6.7.3",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"nprogress": "^0.2.0" "nprogress": "^0.2.0",
"qrcode": "^1.5.3"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^5.0.8",

View File

@@ -73,3 +73,5 @@ export const getQcDefects = params => request.get('/quality/defects', { params }
export const createQcDefect = data => request.post('/quality/defects', data) export const createQcDefect = data => request.post('/quality/defects', data)
export const updateQcDefect = (id, data) => request.put(`/quality/defects/${id}`, data) export const updateQcDefect = (id, data) => request.put(`/quality/defects/${id}`, data)
export const deleteQcDefect = id => request.delete(`/quality/defects/${id}`) export const deleteQcDefect = id => request.delete(`/quality/defects/${id}`)
export const getQcDefectsByCoil = coilNo => request.get(`/quality/defects/by-coil/${encodeURIComponent(coilNo)}`)
export const bulkSaveQcDefects = data => request.post('/quality/defects/bulk-save', data)

View File

@@ -46,18 +46,6 @@ const routes = [
component: () => import('@/views/Downtime.vue'), component: () => import('@/views/Downtime.vue'),
meta: { title: '停机管理', icon: 'el-icon-warning-outline', requiresAuth: true } meta: { title: '停机管理', icon: 'el-icon-warning-outline', requiresAuth: true }
}, },
{
path: 'equipment',
name: 'Equipment',
component: () => import('@/views/Equipment.vue'),
meta: { title: '设备管理', icon: 'el-icon-set-up', requiresAuth: true }
},
{
path: 'message',
name: 'Message',
component: () => import('@/views/Message.vue'),
meta: { title: '报文监控', icon: 'el-icon-connection', requiresAuth: true }
},
{ {
path: 'process-model', path: 'process-model',
name: 'ProcessModel', name: 'ProcessModel',
@@ -68,7 +56,7 @@ const routes = [
path: 'tension-model', path: 'tension-model',
name: 'TensionModel', name: 'TensionModel',
component: () => import('@/views/TensionModel.vue'), component: () => import('@/views/TensionModel.vue'),
meta: { title: '张力设定', icon: 'el-icon-odometer', requiresAuth: true } meta: { title: '张力模型', icon: 'el-icon-odometer', requiresAuth: true }
}, },
{ {
path: 'inspection', path: 'inspection',

View File

@@ -36,7 +36,7 @@
<div class="flex-row" style="gap:8px;"> <div class="flex-row" style="gap:8px;">
<span :class="['badge', cl_is_active_badge]">{{ selectedCl.is_active ? '启用中' : '已停用' }}</span> <span :class="['badge', cl_is_active_badge]">{{ selectedCl.is_active ? '启用中' : '已停用' }}</span>
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openEditChecklistDialog()">编辑</button> <button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openEditChecklistDialog()">编辑</button>
<button class="btn btn-primary" style="padding:2px 10px;font-size:11px;" @click="openInspectDialog()">开始巡检</button> <button class="btn btn-primary" style="padding:2px 10px;font-size:11px;" @click="openQrDialog()">巡检</button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -269,6 +269,27 @@
</div> </div>
</div> </div>
<!-- 二维码弹窗 -->
<div v-if="qrDialogVisible" class="modal-mask" @click.self="qrDialogVisible = false">
<div class="modal-box" style="width:320px;">
<div class="modal-header">
扫码巡检
<span class="modal-close" @click="qrDialogVisible = false"></span>
</div>
<div class="modal-body" style="display:flex;flex-direction:column;align-items:center;gap:10px;">
<div class="qr-box">
<canvas ref="qrCanvas"></canvas>
</div>
<div style="font-size:12px;color:#8b949e;">设备编号</div>
<div style="font-family:monospace;font-size:14px;color:#e6edf3;">{{ selectedCl && selectedCl.equipment_code || '—' }}</div>
<div style="font-size:11px;color:#8b949e;text-align:center;">使用手机/PDA 扫描上方二维码进入巡检填报</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="qrDialogVisible = false">关闭</button>
</div>
</div>
</div>
<!-- 开始巡检弹窗 --> <!-- 开始巡检弹窗 -->
<div v-if="inspectDialogVisible" class="modal-mask" @click.self="inspectDialogVisible = false"> <div v-if="inspectDialogVisible" class="modal-mask" @click.self="inspectDialogVisible = false">
<div class="modal-box" style="width:560px;"> <div class="modal-box" style="width:560px;">
@@ -381,6 +402,7 @@ import {
getChecklistItems, createChecklistItem, getChecklistItems, createChecklistItem,
getInspectionRecords, createInspectionRecord, getInspectionRecordDetails, getInspectionRecords, createInspectionRecord, getInspectionRecordDetails,
} from '@/api' } from '@/api'
import QRCode from 'qrcode'
const PERIOD_MAP = { const PERIOD_MAP = {
daily: { label: '每日', badge: 'badge-blue' }, daily: { label: '每日', badge: 'badge-blue' },
@@ -418,6 +440,9 @@ export default {
itemDialogVisible: false, itemDialogVisible: false,
itemForm: { item_name: '', item_standard: '', sort_order: 0 }, itemForm: { item_name: '', item_standard: '', sort_order: 0 },
// 二维码弹窗
qrDialogVisible: false,
// 开始巡检 // 开始巡检
inspectDialogVisible: false, inspectDialogVisible: false,
inspectForm: { inspector: '', inspect_time: '', status: 'ok', overall_result: '', remark: '' }, inspectForm: { inspector: '', inspect_time: '', status: 'ok', overall_result: '', remark: '' },
@@ -447,6 +472,17 @@ export default {
this.selectedCl = cl this.selectedCl = cl
await Promise.all([this.fetchClItems(), this.fetchRecords()]) await Promise.all([this.fetchClItems(), this.fetchRecords()])
}, },
openQrDialog() {
if (!this.selectedCl) return
this.qrDialogVisible = true
this.$nextTick(() => {
const canvas = this.$refs.qrCanvas
if (!canvas) return
const text = this.selectedCl.equipment_code || this.selectedCl.name || ''
if (!text) return
QRCode.toCanvas(canvas, text, { width: 220, margin: 1, color: { dark: '#0a0a0a', light: '#ffffff' } }).catch(() => {})
})
},
async fetchClItems() { async fetchClItems() {
if (!this.selectedCl) return if (!this.selectedCl) return
try { try {
@@ -688,6 +724,9 @@ export default {
} }
.sec-title { font-size: 11px; color: $text-muted; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; } .sec-title { font-size: 11px; color: $text-muted; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; }
.qr-box { padding: 10px; background: #fff; border-radius: 4px; display: inline-flex; }
.qr-box canvas { display: block; }
.form-field { display: flex; flex-direction: column; gap: 5px; } .form-field { display: flex; flex-direction: column; gap: 5px; }
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 8px; &:hover { text-decoration: underline; } } .action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 8px; &:hover { text-decoration: underline; } }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }

View File

@@ -71,15 +71,13 @@ const IC = {
const MENU = [ const MENU = [
{ path: '/dashboard', title: '生产看板', icon: IC.dashboard }, { path: '/dashboard', title: '生产看板', icon: IC.dashboard },
{ path: '/plan', title: '计划管理', icon: IC.plan },
{ path: '/material', title: '物料跟踪', icon: IC.material }, { path: '/material', title: '物料跟踪', icon: IC.material },
{ path: '/production', title: '实绩管理', icon: IC.production }, { path: '/production', title: '实绩管理', icon: IC.production },
{ path: '/plan', title: '计划管理', icon: IC.plan },
{ path: '/downtime', title: '停机管理', icon: IC.downtime },
{ path: '/equipment', title: '设备管理', icon: IC.equipment },
{ path: '/inspection', title: '设备巡检', icon: IC.inspection },
{ path: '/message', title: '报文监控', icon: IC.message },
{ path: '/process-model', title: '工艺段模型', icon: IC.process }, { path: '/process-model', title: '工艺段模型', icon: IC.process },
{ path: '/tension-model', title: '张力设定', icon: IC.tension }, { path: '/tension-model', title: '张力模型', icon: IC.tension },
{ path: '/downtime', title: '停机管理', icon: IC.downtime },
{ path: '/inspection', title: '设备巡检', icon: IC.inspection },
{ path: '/quality', title: '质量管理', icon: IC.quality }, { path: '/quality', title: '质量管理', icon: IC.quality },
{ path: '/capacity', title: '产能分析', icon: IC.capacity }, { path: '/capacity', title: '产能分析', icon: IC.capacity },
] ]

View File

@@ -1,154 +1,286 @@
<template> <template>
<div> <div class="mat-page">
<!-- 搜索栏 --> <!-- 顶部状态条 -->
<div class="card"> <div class="status-bar">
<div class="card-body" style="padding:10px 14px;"> <div class="status-item">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;"> <span class="kv-label">当前卷号</span>
<div class="flex-row"> <span class="kv-value">{{ current.coil_no || '—' }}</span>
<span class="kv-label">卷号</span> </div>
<input v-model="query.coil_no" class="kv-input" style="width:150px;" @keyup.enter="fetchData" /> <div class="status-item">
</div> <span class="kv-label">工艺段速度</span>
<div class="flex-row"> <span class="kv-value">{{ current.speed.toFixed(1) }} <span class="kv-unit">m/min</span></span>
<span class="kv-label">状态</span> </div>
<select v-model="query.status" class="kv-input" style="width:110px;"> <div class="status-item">
<option value="">全部</option> <span class="kv-label">焊缝位置</span>
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option> <span class="kv-value">{{ (weld.position * 100).toFixed(1) }} <span class="kv-unit">%</span></span>
</select> </div>
</div> <div class="status-item">
<div class="flex-row"> <span class="kv-label">当前设备</span>
<button class="btn btn-primary" @click="fetchData">查询</button> <span class="kv-value">{{ currentEquipment.label }}</span>
<button class="btn btn-outline" @click="openDialog()"> 新增钢卷</button> </div>
</div> <div class="status-item">
<div style="margin-left:auto;" class="flex-row"> <span class="kv-label">开卷张力</span>
<span class="kv-label"> <span class="kv-value">{{ total }}</span> </span> <span class="kv-value">{{ uncoiler.tension.toFixed(1) }} <span class="kv-unit">kN</span></span>
</div> </div>
</div> <div class="status-item">
<span class="kv-label">收卷张力</span>
<span class="kv-value">{{ recoiler.tension.toFixed(1) }} <span class="kv-unit">kN</span></span>
</div>
<div class="status-item" style="margin-left:auto;">
<span :class="['badge', l1Online ? 'badge-green' : 'badge-yellow']">{{ l1Online ? 'L1 在线' : '模拟数据' }}</span>
<span class="kv-label" style="margin-left:8px;">{{ rtItems.length }} 测点</span>
</div> </div>
</div> </div>
<!-- 数据表 --> <!-- 产线总图 -->
<div class="card"> <div class="line-wrap card">
<div class="card-header"> <div class="card-header">推拉酸洗线 - 物料跟踪总图</div>
📦 钢卷台账 <div class="line-body">
<span class="ch-badge">{{ tableData.length }} / {{ total }}</span> <svg viewBox="0 0 1900 280" preserveAspectRatio="xMidYMid meet" class="line-svg">
</div> <rect x="0" y="0" width="1900" height="280" fill="#0a1218" />
<div class="table-scroll" v-loading="loading">
<table class="data-table"> <!-- 顶部标签 -->
<thead> <g v-for="eq in equipments" :key="'lab-'+eq.k" font-family="Arial,sans-serif">
<tr> <text :x="eq.x" y="20" text-anchor="middle" font-size="10.5" fill="#c8d4e0">{{ eq.label }}</text>
<th>卷号</th><th>钢种</th><th>厚度(mm)</th><th>宽度(mm)</th> </g>
<th>毛重(kg)</th><th>净重(kg)</th><th>状态</th><th>创建时间</th><th>操作</th>
</tr> <!-- 主带钢线 -->
</thead> <path d="M 40 160 L 1860 160" stroke="#5a6a75" stroke-width="3" fill="none"/>
<tbody> <path d="M 40 160 L 1860 160" stroke="#aabbcc" stroke-width="1.2" fill="none" stroke-dasharray="6 10">
<tr v-for="row in tableData" :key="row.coil_no"> <animate attributeName="stroke-dashoffset" from="16" to="0" dur="0.7s" repeatCount="indefinite"/>
<td class="td-num">{{ row.coil_no }}</td> </path>
<td>{{ row.steel_grade || '—' }}</td>
<td class="td-num">{{ row.spec_thickness || '—' }}</td> <!-- 各设备图形 -->
<td class="td-num">{{ row.spec_width || '—' }}</td> <g v-for="eq in equipments" :key="eq.k" :transform="`translate(${eq.x}, 160)`">
<td class="td-num">{{ row.gross_weight || '—' }}</td> <!-- 开卷机 -->
<td class="td-num">{{ row.net_weight || '—' }}</td> <template v-if="eq.type==='coiler'">
<td> <circle r="38" fill="#1a232c" stroke="#3a4a55" stroke-width="2"/>
<span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span> <circle r="22" fill="#0a1218" stroke="#5a6a75" stroke-width="1.5"/>
</td> <circle r="8" fill="#2a3a48" stroke="#5a7090" stroke-width="1"/>
<td class="td-muted">{{ fmtTime(row.created_at) }}</td> <path d="M-38 0 a38 38 0 0 1 76 0" stroke="#00c8ff" stroke-width="1" fill="none" opacity="0.5">
<td> <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite"/>
<span class="action-link" @click="viewTracking(row)">跟踪</span> </path>
<span class="action-link" @click="openDialog(row)">编辑</span> <text y="58" text-anchor="middle" font-size="10" fill="#b8c4cf">DC-1</text>
</td> </template>
</tr>
<tr v-if="!tableData.length && !loading"> <!-- 九辊矫直机5上4下 -->
<td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td> <template v-else-if="eq.type==='rolls9'">
</tr> <rect x="-44" y="-26" width="88" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="3"/>
</tbody> <g v-for="i in 5" :key="'t'+i">
</table> <circle :cx="-36 + (i-1)*18" cy="-10" r="6" fill="#2a3540" stroke="#7090a8" stroke-width="1"/>
</div> </g>
<!-- 分页 --> <g v-for="i in 4" :key="'b'+i">
<div class="card-body" style="padding:8px 14px;" v-if="total > query.page_size"> <circle :cx="-27 + (i-1)*18" cy="10" r="6" fill="#2a3540" stroke="#7090a8" stroke-width="1"/>
<div class="flex-row"> </g>
<button class="btn btn-outline" :disabled="query.page <= 1" @click="query.page--; fetchData()">上一页</button> <text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">STR-9</text>
<span class="kv-label"> {{ query.page }} / {{ Math.ceil(total/query.page_size) }} </span> </template>
<button class="btn btn-outline" :disabled="query.page >= Math.ceil(total/query.page_size)" @click="query.page++; fetchData()">下一页</button>
</div> <!-- 切头/切尾剪 -->
<template v-else-if="eq.type==='shear'">
<rect x="-30" y="-26" width="60" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<line x1="-18" y1="-16" x2="18" y2="16" stroke="#da3633" stroke-width="2.2"/>
<line x1="-18" y1="16" x2="18" y2="-16" stroke="#da3633" stroke-width="2.2"/>
<circle cx="-18" cy="-16" r="3" fill="#da3633"/>
<circle cx="18" cy="-16" r="3" fill="#da3633"/>
<circle cx="0" cy="0" r="3" fill="#ffdd44"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">{{ eq.code }}</text>
</template>
<!-- 酸洗槽 -->
<template v-else-if="eq.type==='acid'">
<path d="M -32 -24 L 32 -24 L 28 26 L -28 26 Z" fill="#3a2a18" stroke="#a06030" stroke-width="2"/>
<path d="M -30 -10 L 30 -10 L 27 24 L -27 24 Z" fill="#ffaa44" opacity="0.55">
<animate attributeName="opacity" values="0.5;0.7;0.5" dur="2.5s" repeatCount="indefinite"/>
</path>
<path d="M -22 -10 q 4 -6 8 0 t 8 0 t 8 0 t 8 0" stroke="#ffd28a" stroke-width="1" fill="none" opacity="0.7"/>
<!-- 蒸汽 -->
<g opacity="0.6">
<circle cx="-12" cy="-30" r="3" fill="#cccccc">
<animate attributeName="cy" values="-30;-46;-30" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite"/>
</circle>
<circle cx="6" cy="-32" r="2.5" fill="#cccccc">
<animate attributeName="cy" values="-32;-50;-32" dur="2.3s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.5;0;0.5" dur="2.3s" repeatCount="indefinite"/>
</circle>
</g>
<text x="0" y="44" text-anchor="middle" font-size="9" fill="#ffaa44">{{ acid[eq.idx].temp.toFixed(0) }}°C · {{ acid[eq.idx].conc.toFixed(0) }}g/L</text>
</template>
<!-- 漂洗段 -->
<template v-else-if="eq.type==='rinse'">
<path d="M -34 -24 L 34 -24 L 30 26 L -30 26 Z" fill="#142a2e" stroke="#4080a0" stroke-width="2"/>
<path d="M -32 -8 L 32 -8 L 29 24 L -29 24 Z" fill="#3aa0c8" opacity="0.55">
<animate attributeName="opacity" values="0.5;0.7;0.5" dur="2.5s" repeatCount="indefinite"/>
</path>
<path d="M -22 -8 q 4 -5 8 0 t 8 0 t 8 0 t 8 0" stroke="#bce4f0" stroke-width="1" fill="none" opacity="0.7"/>
<text y="44" text-anchor="middle" font-size="9" fill="#3aa0c8">5级逆流</text>
</template>
<!-- 热风烘干段 -->
<template v-else-if="eq.type==='dryer'">
<rect x="-36" y="-26" width="72" height="52" fill="#2a2010" stroke="#a08030" stroke-width="2" rx="3"/>
<g stroke="#ffaa00" stroke-width="1.6" fill="none">
<path d="M -26 -12 q 4 -6 8 0 t 8 0 t 8 0 t 8 0 t 8 0">
<animate attributeName="opacity" values="0.4;1;0.4" dur="1.4s" repeatCount="indefinite"/>
</path>
<path d="M -26 4 q 4 -6 8 0 t 8 0 t 8 0 t 8 0 t 8 0">
<animate attributeName="opacity" values="0.7;0.3;0.7" dur="1.4s" repeatCount="indefinite"/>
</path>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#ffaa00">{{ dryer.t1.toFixed(0) }}/{{ dryer.t2.toFixed(0) }}/{{ dryer.t3.toFixed(0) }}°C</text>
</template>
<!-- 夹送辊 / 挤干辊 (两辊上下) -->
<template v-else-if="eq.type==='pinch'">
<rect x="-30" y="-26" width="60" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<ellipse cx="0" cy="-12" rx="22" ry="6" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<ellipse cx="0" cy="12" rx="22" ry="6" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<line x1="-22" y1="-12" x2="-22" y2="12" stroke="#5a6a75" stroke-width="1"/>
<line x1="22" y1="-12" x2="22" y2="12" stroke="#5a6a75" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">{{ eq.code }}</text>
</template>
<!-- 活套坑 -->
<template v-else-if="eq.type==='loop'">
<rect x="-40" y="-26" width="80" height="58" fill="#1a232c" stroke="#3a4a55" stroke-width="2" rx="3"/>
<path d="M -32 -16 Q -20 32 -8 -16 Q 4 32 16 -16 Q 28 32 36 -16" stroke="#00c8ff" stroke-width="1.8" fill="none">
<animate attributeName="opacity" values="0.6;1;0.6" dur="1.6s" repeatCount="indefinite"/>
</path>
<text y="48" text-anchor="middle" font-size="9" fill="#b8c4cf">LOOP</text>
</template>
<!-- 三辊张力装置 -->
<template v-else-if="eq.type==='tension3'">
<rect x="-32" y="-26" width="64" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="3"/>
<circle cx="-16" cy="-8" r="8" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<circle cx="16" cy="-8" r="8" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<circle cx="0" cy="12" r="9" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">TEN-3</text>
</template>
<!-- 平整机 -->
<template v-else-if="eq.type==='leveler'">
<rect x="-34" y="-26" width="68" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="2" rx="2"/>
<circle cx="0" cy="-14" r="11" fill="#3a4a55" stroke="#90a0b0" stroke-width="1.4"/>
<circle cx="0" cy="14" r="11" fill="#3a4a55" stroke="#90a0b0" stroke-width="1.4"/>
<line x1="-28" y1="0" x2="-12" y2="0" stroke="#7090a8" stroke-width="1"/>
<line x1="12" y1="0" x2="28" y2="0" stroke="#7090a8" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">SPM</text>
</template>
<!-- 静电涂油机 -->
<template v-else-if="eq.type==='oiler'">
<rect x="-26" y="-26" width="52" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<path d="M 0 -14 L -10 4 L 10 4 Z" fill="#3a4a55" stroke="#90a0b0" stroke-width="1"/>
<g fill="#88ccff">
<circle cx="-6" cy="10" r="1.6">
<animate attributeName="cy" values="6;22;6" dur="1.2s" repeatCount="indefinite"/>
</circle>
<circle cx="0" cy="14" r="1.4">
<animate attributeName="cy" values="8;22;8" dur="1.4s" repeatCount="indefinite"/>
</circle>
<circle cx="6" cy="10" r="1.6">
<animate attributeName="cy" values="6;22;6" dur="1.3s" repeatCount="indefinite"/>
</circle>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">EOL</text>
</template>
<!-- 卷取机 -->
<template v-else-if="eq.type==='recoiler'">
<circle r="38" fill="#1a232c" stroke="#3a4a55" stroke-width="2"/>
<circle r="22" fill="#0a1218" stroke="#5a6a75" stroke-width="1.5"/>
<circle r="8" fill="#2a3a48" stroke="#5a7090" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#00c8ff" stroke-width="1" fill="none" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="3s" repeatCount="indefinite"/>
</path>
<text y="58" text-anchor="middle" font-size="10" fill="#b8c4cf">REC-1</text>
</template>
<!-- 当前设备高亮光环 -->
<circle v-if="eq.k === currentEquipment.k" r="48" fill="none" stroke="#ffdd44" stroke-width="2" stroke-dasharray="4 4" opacity="0.7">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="6s" repeatCount="indefinite"/>
</circle>
</g>
<!-- 焊缝标记 -->
<g :transform="`translate(${weldX}, 160)`">
<circle r="11" fill="#ffdd00" opacity="0.35">
<animate attributeName="r" values="9;22;9" dur="1.0s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.7;0.05;0.7" dur="1.0s" repeatCount="indefinite"/>
</circle>
<circle r="6" fill="#ffee44">
<animate attributeName="fill" values="#ffee44;#ff7700;#ffee44" dur="0.6s" repeatCount="indefinite"/>
</circle>
<text y="-18" text-anchor="middle" font-size="11" fill="#ffdd44" font-weight="bold">WELD</text>
</g>
<!-- 图例 -->
<g transform="translate(20,260)" font-size="10" fill="#8b949e">
<circle cx="6" cy="-3" r="5" fill="#ffee44"/>
<text x="18" y="0">焊缝位置 {{ (weld.position * 100).toFixed(1) }}%</text>
<rect x="160" y="-7" width="12" height="8" fill="#ffaa44" opacity="0.5"/>
<text x="178" y="0">酸洗液</text>
<rect x="230" y="-7" width="12" height="8" fill="#3aa0c8" opacity="0.5"/>
<text x="248" y="0">漂洗水</text>
<circle cx="310" cy="-3" r="5" fill="none" stroke="#ffdd44" stroke-width="1.5" stroke-dasharray="2 2"/>
<text x="322" y="0">当前设备</text>
<text x="420" y="0" fill="#aabbcc"> 带钢运行方向 </text>
</g>
</svg>
</div> </div>
</div> </div>
<!-- 新增/编辑 Modal --> <!-- 下半: 跟踪表 | 实时数据 -->
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false"> <div class="split-row">
<div class="modal-box"> <div class="card split-left">
<div class="modal-header"> <div class="card-header">物料跟踪表 <span class="hd-cnt"> {{ equipments.length }} 台设备</span></div>
{{ editRow ? '编辑钢卷' : '新增钢卷' }} <div class="card-body" style="padding:0;">
<span class="modal-close" @click="dialogVisible=false"></span> <div class="track-scroll">
</div> <table class="data-table compact tracking-table">
<div class="modal-body"> <thead>
<div class="grid-2" style="gap:12px;"> <tr>
<div class="form-field"> <th style="width:32px;">#</th>
<div class="kv-label">卷号 <span style="color:var(--accent-red)">*</span></div> <th>设备</th>
<input v-model="form.coil_no" class="kv-input" :disabled="!!editRow" /> <th style="width:64px;">状态</th>
</div> <th>当前钢卷</th>
<div class="form-field"> <th style="width:80px;">辊缝 (mm)</th>
<div class="kv-label">钢种</div> <th style="width:78px;">速度</th>
<input v-model="form.steel_grade" class="kv-input" /> <th style="width:78px;">张力/温度</th>
</div> </tr>
<div class="form-field"> </thead>
<div class="kv-label">规格厚度 (mm)</div> <tbody>
<input v-model.number="form.spec_thickness" type="number" class="kv-input" /> <tr v-for="(eq, i) in equipments" :key="eq.k"
</div> :class="{ 'row-active': eq.k === currentEquipment.k, 'row-passed': i < currentEquipment.idx, 'row-pending': i > currentEquipment.idx }">
<div class="form-field"> <td class="td-num">{{ i + 1 }}</td>
<div class="kv-label">规格宽度 (mm)</div> <td>{{ eq.label }}</td>
<input v-model.number="form.spec_width" type="number" class="kv-input" /> <td>
</div> <span v-if="eq.k === currentEquipment.k" class="badge badge-yellow">加工中</span>
<div class="form-field"> <span v-else-if="i < currentEquipment.idx" class="badge badge-blue">已过</span>
<div class="kv-label">毛重 (kg)</div> <span v-else class="badge badge-gray">待入</span>
<input v-model.number="form.gross_weight" type="number" class="kv-input" /> </td>
</div> <td class="td-num">{{ rowOf(eq, i).coil }}</td>
<div class="form-field"> <td class="td-num">{{ rowOf(eq, i).gap }}</td>
<div class="kv-label">净重 (kg)</div> <td class="td-num">{{ rowOf(eq, i).speed }}</td>
<input v-model.number="form.net_weight" type="number" class="kv-input" /> <td class="td-num">{{ rowOf(eq, i).aux }}</td>
</div> </tr>
<div class="form-field"> </tbody>
<div class="kv-label">内径 (mm)</div> </table>
<input v-model.number="form.inner_diameter" type="number" class="kv-input" />
</div>
</div> </div>
</div> </div>
<div class="modal-footer">
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="saveCoil">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div> </div>
</div>
<!-- 跟踪详情 Modal --> <div class="card split-right">
<div v-if="trackingVisible" class="modal-mask" @click.self="trackingVisible=false"> <div class="card-header">实时数据 <span class="hd-cnt">{{ rtItems.length }} </span></div>
<div class="modal-box" style="width:860px;max-width:95vw;"> <div class="card-body sec-body">
<div class="modal-header"> <div class="dg">
物料跟踪记录 <span style="color:var(--sms-highlight)">{{ trackingCoil }}</span> <div v-for="it in rtItems" :key="it.k" class="dg-item">
<span class="modal-close" @click="trackingVisible=false"></span> <span class="lbl">{{ it.label }}</span>
</div> <span class="vbox">{{ it.val }}</span>
<div class="modal-body" style="max-height:400px;overflow-y:auto;"> <span v-if="it.unit" class="unit">{{ it.unit }}</span>
<table class="data-table"> </div>
<thead> </div>
<tr><th>时间</th><th>位置</th><th>事件类型</th><th>描述</th><th>实测厚度</th><th>速度</th><th>操作员</th></tr>
</thead>
<tbody>
<tr v-for="t in trackingData" :key="t.id">
<td class="td-muted">{{ fmtTime(t.event_time) }}</td>
<td>{{ t.position || '—' }}</td>
<td><span class="badge badge-blue">{{ t.event_type }}</span></td>
<td>{{ t.event_desc || '—' }}</td>
<td class="td-num">{{ t.actual_thickness || '—' }}</td>
<td class="td-num">{{ t.speed || '—' }}</td>
<td class="td-muted">{{ t.operator || '—' }}</td>
</tr>
<tr v-if="!trackingData.length">
<td colspan="7" class="td-muted" style="text-align:center;padding:20px;">暂无跟踪记录</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="trackingVisible=false">关闭</button>
</div> </div>
</div> </div>
</div> </div>
@@ -156,116 +288,328 @@
</template> </template>
<script> <script>
import { getCoils, createCoil, updateCoil, getTracking } from '@/api' function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
function fix(v, n = 1) { return Number(v).toFixed(n) }
const STATUS_MAP = { const EQUIPMENTS = [
waiting: { label: '等待入线', badge: 'badge-gray' }, { k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1' },
on_line: { label: '在线处理', badge: 'badge-green' }, { k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9' },
finished: { label: '处理完成', badge: 'badge-blue' }, { k:'crop_shear', label:'切头剪', type:'shear', code:'CRP' },
abnormal: { label: '异常', badge: 'badge-red' }, { k:'acid1', label:'酸洗槽1', type:'acid', idx:0 },
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1 },
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2 },
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3 },
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4 },
{ k:'rinse', label:'漂洗段', type:'rinse' },
{ k:'dryer', label:'热风烘干段', type:'dryer' },
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1' },
{ k:'loop', label:'活套坑', type:'loop' },
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2' },
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3' },
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3' },
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM' },
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS' },
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL' },
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1' },
]
// 默认辊缝值 (mm)
const DEFAULT_GAP = {
straightener: 4.20,
br1: 3.80, br2: 3.80, br3: 3.80,
tension: 4.00,
leveler: 3.50,
} }
export default { export default {
name: 'Material', name: 'Material',
data() { data() {
return { return {
loading: false, saving: false, l1Online: false,
tableData: [], total: 0, current: { coil_no: '26053552', speed: 95.0 },
query: { page: 1, page_size: 20, coil_no: '', status: '' }, prev_coil_no: '26053551',
statusOptions: Object.entries(STATUS_MAP).map(([value, { label }]) => ({ value, label })), weld: { position: 0.08 },
dialogVisible: false, editRow: null, form: {},
trackingVisible: false, trackingCoil: '', trackingData: [], uncoiler: { tension: 18.5, speed: 92.0, current: 240, torque: 1.8, diameter: 1450 },
straightener: { speed: 92.0, current: 165, torque: 1.5, gap: 4.20 },
br1: { speed: 92.0, current: 145, torque: 1.3, gap: 3.80 },
br2: { speed: 92.0, current: 142, torque: 1.3, gap: 3.80 },
br3: { speed: 92.0, current: 140, torque: 1.3, gap: 3.80 },
tension_vfd: [
{ speed: 92.0, current: 158, torque: 1.6 },
{ speed: 92.0, current: 156, torque: 1.5 },
{ speed: 92.0, current: 154, torque: 1.5 },
],
tension_gap: 4.00,
leveler: { gap: 3.50, force: 280, elongation: 0.45 },
recoiler: { tension: 22.4, diameter: 980, speed: 95 },
acid: [
{ temp: 82, conc: 198, level: 0.97, cond: 215, tank_conc: 195, tank_cond: 210 },
{ temp: 81, conc: 188, level: 1.03, cond: 205, tank_conc: 185, tank_cond: 200 },
{ temp: 81, conc: 175, level: 0.94, cond: 192, tank_conc: 172, tank_cond: 188 },
{ temp: 80, conc: 162, level: 0.74, cond: 178, tank_conc: 158, tank_cond: 175 },
{ temp: 74, conc: 148, level: 0.71, cond: 162, tank_conc: 145, tank_cond: 160 },
],
acid_mist: { ph: 6.8, vfd_speed: 48.5, vfd_current: 32.6 },
acid_cond: { level: 1.85, temp: 42.5, cond: 12.5 },
rinse_tank_temp: [65, 62, 58, 54, 48],
rinse: [
{ conc: 0.5, cond: 18.5, level: 0.45, tank_conc: 0.4, tank_cond: 17.5 },
{ conc: 0.3, cond: 12.2, level: 0.54, tank_conc: 0.3, tank_cond: 11.8 },
{ conc: 0.2, cond: 6.8, level: 0.18, tank_conc: 0.2, tank_cond: 6.5 },
{ conc: 0.1, cond: 2.5, level: 0.77, tank_conc: 0.1, tank_cond: 2.4 },
{ conc: 0.0, cond: 0.8, level: 0.81, tank_conc: 0.0, tank_cond: 0.7 },
],
rinse_mist: { ph: 7.0, vfd_speed: 45.2, vfd_current: 28.4 },
rinse_cond: { level: 2.10, temp: 38.6, cond: 4.5 },
dryer: { t1: 145, t2: 168, t3: 152 },
_timer: null,
} }
}, },
created() { this.fetchData() }, computed: {
equipments() {
const n = EQUIPMENTS.length
const xStart = 50, xEnd = 1850
const step = (xEnd - xStart) / (n - 1)
return EQUIPMENTS.map((e, i) => ({ ...e, x: xStart + step * i }))
},
weldX() {
const p = Math.max(0, Math.min(1, this.weld.position))
return 50 + (1850 - 50) * p
},
currentEquipment() {
const n = this.equipments.length
const idx = Math.max(0, Math.min(n - 1, Math.floor(this.weld.position * n)))
return { ...this.equipments[idx], idx }
},
rtItems() {
const items = []
const push = (k, label, val, unit) => items.push({ k, label, val, unit })
push('u_t', '开卷机 开卷张力', fix(this.uncoiler.tension, 1), 'kN')
push('u_s', '开卷机 速度反馈', fix(this.uncoiler.speed, 1), 'm/min')
push('u_c', '开卷机 电流反馈', fix(this.uncoiler.current, 0), 'A')
push('u_q', '开卷机 扭矩反馈', fix(this.uncoiler.torque, 2), 'kN·m')
push('st_s', '九辊矫直机 速度反馈',fix(this.straightener.speed, 1), 'm/min')
push('st_c', '九辊矫直机 电流反馈',fix(this.straightener.current, 0), 'A')
push('st_q', '九辊矫直机 扭矩反馈',fix(this.straightener.torque, 2), 'kN·m')
for (const [k, name] of [['br1','1号夹送辊'], ['br2','2号夹送辊'], ['br3','3号夹送辊']]) {
push(k+'_s', `${name} 速度反馈`, fix(this[k].speed, 1), 'm/min')
push(k+'_c', `${name} 电流反馈`, fix(this[k].current, 0), 'A')
push(k+'_q', `${name} 扭矩反馈`, fix(this[k].torque, 2), 'kN·m')
}
this.tension_vfd.forEach((v, i) => {
push(`tv${i}s`, `三辊张力 变频器${i+1} 速度反馈`, fix(v.speed, 1), 'm/min')
push(`tv${i}c`, `三辊张力 变频器${i+1} 电流反馈`, fix(v.current, 0), 'A')
push(`tv${i}q`, `三辊张力 变频器${i+1} 扭矩反馈`, fix(v.torque, 2), 'kN·m')
})
push('r_t', '收卷机 收卷张力', fix(this.recoiler.tension, 1), 'kN')
this.acid.forEach((a, i) => {
push(`at${i}`, `酸洗${i+1}# 槽/罐温度(公用)`, fix(a.temp, 1), '°C')
push(`al${i}`, `酸洗${i+1}# 罐液位`, fix(a.level, 2), 'm')
push(`ac${i}`, `酸洗${i+1}# 槽浓度`, fix(a.conc, 1), 'g/L')
push(`ae${i}`, `酸洗${i+1}# 槽电导率`, fix(a.cond, 1), 'mS/cm')
push(`atc${i}`,`酸洗${i+1}# 罐浓度`, fix(a.tank_conc, 1), 'g/L')
push(`ate${i}`,`酸洗${i+1}# 罐电导率`, fix(a.tank_cond, 1), 'mS/cm')
})
push('amp', '酸雾塔 PH', fix(this.acid_mist.ph, 2), '')
push('ams', '酸雾塔 变频器频率', fix(this.acid_mist.vfd_speed, 1), 'Hz')
push('amc', '酸雾塔 变频器电流', fix(this.acid_mist.vfd_current,1),'A')
push('acl', '酸侧冷凝水罐 液位', fix(this.acid_cond.level, 2), 'm')
push('act', '酸侧冷凝水罐 温度', fix(this.acid_cond.temp, 1), '°C')
push('acc', '酸侧冷凝水罐 电导率', fix(this.acid_cond.cond, 1), 'μS/cm')
this.rinse.forEach((r, i) => {
const t = this.rinse_tank_temp[i]
push(`rt${i}`, `漂洗${i+1}# 槽/罐温度(公用)`, fix(t, 1), '°C')
push(`rl${i}`, `漂洗${i+1}# 罐液位`, fix(r.level, 2), 'm')
push(`rc${i}`, `漂洗${i+1}# 槽浓度`, fix(r.conc, 2), 'g/L')
push(`re${i}`, `漂洗${i+1}# 槽电导率`, fix(r.cond, 2), 'μS/cm')
push(`rtc${i}`,`漂洗${i+1}# 罐浓度`, fix(r.tank_conc, 2), 'g/L')
push(`rte${i}`,`漂洗${i+1}# 罐电导率`, fix(r.tank_cond, 2), 'μS/cm')
})
push('rmp', '漂洗酸雾塔 PH', fix(this.rinse_mist.ph, 2), '')
push('rms', '漂洗酸雾塔 变频器频率', fix(this.rinse_mist.vfd_speed, 1), 'Hz')
push('rmc', '漂洗酸雾塔 变频器电流', fix(this.rinse_mist.vfd_current,1),'A')
push('rcl', '漂洗冷凝水罐 液位', fix(this.rinse_cond.level, 2), 'm')
push('rct', '漂洗冷凝水罐 温度', fix(this.rinse_cond.temp, 1), '°C')
push('rcc', '漂洗冷凝水罐 电导率', fix(this.rinse_cond.cond, 2), 'μS/cm')
push('lvg', '平整机 辊缝', fix(this.leveler.gap, 2), 'mm')
push('lvf', '平整机 轧制力', fix(this.leveler.force, 0), 'kN')
push('lve', '平整机 延伸率', fix(this.leveler.elongation,2), '%')
push('dt1','烘干1段温度', fix(this.dryer.t1, 0), '°C')
push('dt2','烘干2段温度', fix(this.dryer.t2, 0), '°C')
push('dt3','烘干3段温度', fix(this.dryer.t3, 0), '°C')
return items
},
},
methods: { methods: {
async fetchData() { // 一行的展示数据:根据设备状态决定卷号/速度/辊缝/辅助列
this.loading = true rowOf(eq, i) {
try { const curIdx = this.currentEquipment.idx
const res = await getCoils(this.query) const isHere = i === curIdx
this.tableData = res.data.items const passed = i < curIdx
this.total = res.data.total const cur = this.current.coil_no || '—'
} finally { this.loading = false } const prev = this.prev_coil_no || '—'
let coil = '—'
if (isHere) coil = cur
else if (passed) coil = cur // 已被本卷穿过
else coil = prev // 还在上一卷尾部
const speed = (isHere || passed) ? this.current.speed.toFixed(1) : prev !== '—' ? '0.0' : '—'
let gap = '—'
let aux = '—'
switch (eq.type) {
case 'coiler':
gap = '—'
aux = this.uncoiler.tension.toFixed(1) + ' kN'
break
case 'recoiler':
gap = '—'
aux = this.recoiler.tension.toFixed(1) + ' kN'
break
case 'rolls9':
gap = this.straightener.gap.toFixed(2)
aux = this.straightener.torque.toFixed(2) + ' kN·m'
break
case 'pinch':
gap = this[eq.k].gap.toFixed(2)
aux = this[eq.k].torque.toFixed(2) + ' kN·m'
break
case 'tension3':
gap = this.tension_gap.toFixed(2)
aux = this.tension_vfd[0].torque.toFixed(2) + ' kN·m'
break
case 'leveler':
gap = this.leveler.gap.toFixed(2)
aux = this.leveler.force.toFixed(0) + ' kN'
break
case 'acid':
gap = '—'
aux = this.acid[eq.idx].temp.toFixed(1) + ' °C'
break
case 'rinse':
gap = '—'
aux = this.rinse_tank_temp[0].toFixed(1) + ' °C'
break
case 'dryer':
gap = '—'
aux = this.dryer.t2.toFixed(0) + ' °C'
break
case 'shear':
case 'oiler':
case 'loop':
gap = '—'
aux = '—'
break
}
return { coil, gap, speed, aux }
}, },
statusLabel(s) { return STATUS_MAP[s]?.label || s }, tick() {
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' }, this.weld.position = (this.weld.position + 0.012) % 1
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' }, // 新一卷开始时滚动卷号
openDialog(row = null) { if (this.weld.position < 0.012) {
this.editRow = row this.prev_coil_no = this.current.coil_no
this.form = row ? { ...row } : {} const n = parseInt(this.current.coil_no || '26053552', 10) + 1
this.dialogVisible = true this.current.coil_no = String(n)
}
this.current.speed = Math.max(0, rnd(this.current.speed, 4))
const wig = (o, key, amp) => { o[key] = rnd(o[key], amp) }
wig(this.uncoiler, 'tension', 0.4); wig(this.uncoiler, 'speed', 2)
wig(this.uncoiler, 'current', 6); wig(this.uncoiler, 'torque', 0.1)
wig(this.straightener, 'speed', 2); wig(this.straightener, 'current', 5)
wig(this.straightener, 'torque', 0.1); wig(this.straightener, 'gap', 0.01)
;['br1','br2','br3'].forEach(k => {
wig(this[k], 'speed', 2); wig(this[k], 'current', 5)
wig(this[k], 'torque', 0.1); wig(this[k], 'gap', 0.01)
})
this.tension_vfd.forEach(v => { wig(v, 'speed', 2); wig(v, 'current', 5); wig(v, 'torque', 0.1) })
this.tension_gap = rnd(this.tension_gap, 0.01)
wig(this.leveler, 'gap', 0.005); wig(this.leveler, 'force', 8); wig(this.leveler, 'elongation', 0.02)
wig(this.recoiler, 'tension', 0.4)
this.acid.forEach(a => {
wig(a, 'temp', 0.3); wig(a, 'conc', 1); wig(a, 'cond', 0.8); wig(a, 'level', 0.02)
wig(a, 'tank_conc', 1); wig(a, 'tank_cond', 0.8)
})
wig(this.acid_mist, 'ph', 0.05); wig(this.acid_mist, 'vfd_speed', 0.6); wig(this.acid_mist, 'vfd_current', 0.4)
wig(this.acid_cond, 'level', 0.02); wig(this.acid_cond, 'temp', 0.3); wig(this.acid_cond, 'cond', 0.2)
this.rinse.forEach(r => {
wig(r, 'conc', 0.05); wig(r, 'cond', 0.3); wig(r, 'level', 0.02)
wig(r, 'tank_conc', 0.05); wig(r, 'tank_cond', 0.3)
})
for (let i = 0; i < this.rinse_tank_temp.length; i++) this.rinse_tank_temp[i] = rnd(this.rinse_tank_temp[i], 0.4)
wig(this.rinse_mist, 'ph', 0.05); wig(this.rinse_mist, 'vfd_speed', 0.6); wig(this.rinse_mist, 'vfd_current', 0.4)
wig(this.rinse_cond, 'level', 0.02); wig(this.rinse_cond, 'temp', 0.3); wig(this.rinse_cond, 'cond', 0.1)
wig(this.dryer, 't1', 2); wig(this.dryer, 't2', 2); wig(this.dryer, 't3', 2)
}, },
async saveCoil() { },
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return } created() {
this.saving = true this.tick()
try { this._timer = setInterval(this.tick, 2000)
if (this.editRow) await updateCoil(this.form.coil_no, this.form) },
else await createCoil(this.form) beforeDestroy() {
this.$message.success('保存成功') if (this._timer) clearInterval(this._timer)
this.dialogVisible = false },
this.fetchData()
} finally { this.saving = false }
},
async viewTracking(row) {
this.trackingCoil = row.coil_no
const res = await getTracking({ coil_no: row.coil_no, page_size: 100 })
this.trackingData = res.data.items
this.trackingVisible = true
}
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/styles/variables'; @import '@/assets/styles/variables';
.action-link { .mat-page { display: flex; flex-direction: column; gap: 10px; }
color: $sms-highlight;
cursor: pointer;
font-size: 12px;
margin-right: 12px;
font-family: $font-main;
&:hover { text-decoration: underline; }
}
.form-field { display: flex; flex-direction: column; gap: 5px; } .status-bar {
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
padding: 8px 16px;
background: $bg-card; border: 1px solid $border; border-radius: 6px;
}
.status-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.status-item .kv-label { color: $text-muted; font-size: 11px; }
.status-item .kv-value { color: $sms-highlight; font-weight: 600; }
.status-item .kv-unit { color: $text-muted; font-size: 10px; margin-left: 2px; }
// Modal .line-wrap { padding: 0; }
.modal-mask { .line-body { padding: 6px 10px 10px; background: #0a1218; }
position: fixed; inset: 0; .line-svg { width: 100%; height: 280px; display: block; }
background: rgba(0,0,0,.6);
display: flex; align-items: center; justify-content: center; .split-row { display: grid; grid-template-columns: 1.05fr 1fr; gap: 10px; align-items: stretch; }
z-index: 9999; .split-left, .split-right { display: flex; flex-direction: column; min-height: 540px; }
} .split-right .card-body { flex: 1; overflow-y: auto; }
.modal-box {
background: $bg-card; .track-scroll { max-height: 640px; overflow-y: auto; }
border: 1px solid $border;
border-radius: 6px; .hd-cnt { font-size: 11px; color: #6b7c8d; margin-left: 8px; font-weight: 400; }
width: 640px;
max-width: 95vw; .sec-body { padding: 10px 14px; background: #161d24; }
max-height: 90vh;
display: flex; .dg { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 4px 18px; }
flex-direction: column; .dg-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #c8d4e0; padding: 2px 0; }
} .dg-item .lbl { color: #8b9aab; flex: 1; min-width: 140px; }
.modal-header { .dg-item .vbox {
display: flex; background: #0e1418; border: 1px solid #2a3540; padding: 1px 8px;
align-items: center; min-width: 70px; text-align: right; font-family: monospace;
justify-content: space-between; color: #00c8ff; border-radius: 2px;
padding: 12px 16px;
background: $bg-panel;
border-bottom: 1px solid $border;
font-size: 13px;
font-weight: 600;
color: $sms-highlight;
.modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } }
}
.modal-body { padding: 16px; overflow-y: auto; }
.modal-footer {
padding: 10px 16px;
background: $bg-panel;
border-top: 1px solid $border;
display: flex;
justify-content: flex-end;
gap: 10px;
} }
.dg-item .unit { color: #6b7c8d; font-size: 11px; min-width: 44px; }
.data-table.compact th, .data-table.compact td { padding: 5px 8px; font-size: 11.5px; }
.tracking-table tr.row-active { background: rgba(255, 221, 68, 0.10); }
.tracking-table tr.row-active td { color: #ffdd44 !important; font-weight: 600; }
.tracking-table tr.row-passed td { color: #6b8aaa; }
.tracking-table tr.row-pending td { color: #5a6a78; }
</style> </style>

View File

@@ -10,6 +10,10 @@
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option> <option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option>
</select> </select>
</div> </div>
<div class="flex-row">
<span class="kv-label">冷卷号</span>
<input v-model="query.cold_coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
</div>
<div class="flex-row"> <div class="flex-row">
<span class="kv-label">日期</span> <span class="kv-label">日期</span>
<input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" /> <input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" />
@@ -33,42 +37,50 @@
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
<th>计划号</th><th>计划日期</th><th>班次</th> <th style="width:48px;">序号</th>
<th>计划()</th><th>计划重量(kg)</th> <th>冷卷号</th>
<th>实际()</th><th>实际重量(kg)</th> <th>热卷号</th>
<th>完成率</th><th>优先级</th><th>状态</th><th>创建人</th><th>操作</th> <th>钢种</th>
<th>来料厚度</th>
<th>产品厚度</th>
<th>偏差上限</th>
<th>偏差下限</th>
<th>来料宽度</th>
<th>产品宽度</th>
<th>包装要求</th>
<th>卷径</th>
<th>分卷数</th>
<th>下工序</th>
<th>计划时间</th>
<th>状态</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in tableData" :key="row.id"> <tr v-for="(row, idx) in tableData" :key="row.id">
<td class="td-num">{{ row.plan_no }}</td> <td class="td-num">{{ idx + 1 }}</td>
<td class="td-muted">{{ fmtDate(row.plan_date) }}</td> <td class="td-num">{{ row.cold_coil_no || row.plan_no || '—' }}</td>
<td>{{ row.shift ? row.shift + '班' : '—' }}</td> <td class="td-num">{{ row.hot_coil_no || '—' }}</td>
<td class="td-num">{{ row.plan_quantity }}</td> <td>{{ row.steel_grade || '—' }}</td>
<td class="td-num">{{ row.plan_weight }}</td> <td class="td-num">{{ fmtNum(row.incoming_thickness) }}</td>
<td class="td-num">{{ row.actual_quantity }}</td> <td class="td-num">{{ fmtNum(row.product_thickness) }}</td>
<td class="td-num">{{ row.actual_weight }}</td> <td class="td-num">{{ fmtNum(row.deviation_upper, 3) }}</td>
<td> <td class="td-num">{{ fmtNum(row.deviation_lower, 3) }}</td>
<div v-if="row.plan_quantity > 0"> <td class="td-num">{{ fmtNum(row.incoming_width, 0) }}</td>
<div class="prog-bar-wrap" style="width:70px;display:inline-block;vertical-align:middle;margin-right:6px;"> <td class="td-num">{{ fmtNum(row.product_width, 0) }}</td>
<div class="prog-bar-fill" :style="{ width: completionRate(row) + '%', background: rateColor(row) }"></div> <td>{{ row.packaging_req || '—' }}</td>
</div> <td class="td-num">{{ fmtNum(row.coil_diameter, 0) }}</td>
<span :style="{ color: rateColor(row) }">{{ completionRate(row) }}%</span> <td class="td-num">{{ row.split_count != null ? row.split_count : 1 }}</td>
</div> <td class="td-num">{{ row.next_process != null ? row.next_process : '—' }}</td>
<span v-else class="td-muted"></span> <td class="td-muted">{{ fmtTime(row.plan_date) }}</td>
</td>
<td>
<span :class="['badge', row.priority >= 8 ? 'badge-red' : row.priority >= 5 ? 'badge-yellow' : 'badge-gray']">P{{ row.priority }}</span>
</td>
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td> <td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td class="td-muted">{{ row.created_by || '—' }}</td>
<td> <td>
<span class="action-link" @click="openDialog(row)">编辑</span> <span class="action-link" @click="openDialog(row)">编辑</span>
<span v-if="row.status === 'draft'" class="action-link" style="color:var(--accent-green)" @click="confirmPlan(row)">确认</span> <span v-if="row.status === 'ready'" class="action-link" style="color:var(--accent-green)" @click="confirmPlan(row)">上线</span>
</td> </td>
</tr> </tr>
<tr v-if="!tableData.length && !loading"> <tr v-if="!tableData.length && !loading">
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td> <td colspan="17" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -76,47 +88,82 @@
</div> </div>
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false"> <div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:640px;"> <div class="modal-box" style="width:780px;">
<div class="modal-header"> <div class="modal-header">
{{ editRow ? '编辑计划' : '新增计划' }} {{ editRow ? '编辑计划' : '新增计划' }}
<span class="modal-close" @click="dialogVisible=false"></span> <span class="modal-close" @click="dialogVisible=false"></span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="grid-2" style="gap:12px;"> <div class="grid-3" style="gap:12px;">
<div class="form-field"> <div class="form-field">
<div class="kv-label">计划号 *</div> <div class="kv-label">计划号 *</div>
<input v-model="form.plan_no" class="kv-input" :disabled="!!editRow" /> <input v-model="form.plan_no" class="kv-input" :disabled="!!editRow" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">计划日期</div> <div class="kv-label">冷卷号</div>
<input v-model="form.plan_date" type="date" class="kv-input" /> <input v-model="form.cold_coil_no" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">班次</div> <div class="kv-label">热卷号</div>
<select v-model="form.shift" class="kv-input"> <input v-model="form.hot_coil_no" class="kv-input" />
<option value="">不限</option> </div>
<option v-for="s in ['甲','乙','丙','丁']" :key="s" :value="s">{{ s }}</option> <div class="form-field">
<div class="kv-label">钢种</div>
<input v-model="form.steel_grade" class="kv-input" placeholder="QTGLG-2019" />
</div>
<div class="form-field">
<div class="kv-label">来料厚度 (mm)</div>
<input v-model.number="form.incoming_thickness" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">产品厚度 (mm)</div>
<input v-model.number="form.product_thickness" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">偏差上限</div>
<input v-model.number="form.deviation_upper" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">偏差下限</div>
<input v-model.number="form.deviation_lower" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">来料宽度 (mm)</div>
<input v-model.number="form.incoming_width" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">产品宽度 (mm)</div>
<input v-model.number="form.product_width" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">包装要求</div>
<select v-model="form.packaging_req" class="kv-input">
<option value=""></option>
<option value="裸包">裸包</option>
<option value="筒包">筒包</option>
</select> </select>
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">优先级 (1-10)</div> <div class="kv-label">卷径 (mm)</div>
<input v-model.number="form.priority" type="number" min="1" max="10" class="kv-input" /> <input v-model.number="form.coil_diameter" type="number" step="1" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">计划数量 ()</div> <div class="kv-label">分卷数</div>
<input v-model.number="form.plan_quantity" type="number" class="kv-input" /> <input v-model.number="form.split_count" type="number" min="1" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">计划重量 (kg)</div> <div class="kv-label">下工序</div>
<input v-model.number="form.plan_weight" type="number" class="kv-input" /> <input v-model="form.next_process" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">主要钢种</div> <div class="kv-label">计划时间</div>
<input v-model="form.steel_grade" class="kv-input" /> <input v-model="form.plan_date" type="datetime-local" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">规格范围</div> <div class="kv-label">状态</div>
<input v-model="form.spec_range" class="kv-input" /> <select v-model="form.status" class="kv-input">
<option v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option>
</select>
</div> </div>
</div> </div>
</div> </div>
@@ -133,11 +180,10 @@
import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm } from '@/api' import { getPlans, createPlan, updatePlan, confirmPlan as apiConfirm } from '@/api'
const STATUS_MAP = { const STATUS_MAP = {
draft: { label: '草稿', badge: 'badge-gray' }, ready: { label: '准备好', badge: 'badge-green' },
confirmed: { label: '已确认', badge: 'badge-blue' }, online: { label: '在线', badge: 'badge-yellow' },
in_progress: { label: '执行中', badge: 'badge-green' }, producing: { label: '生产中', badge: 'badge-yellow' },
completed: { label: '完成', badge: 'badge-gray' }, produced: { label: '产出', badge: 'badge-blue' },
cancelled: { label: '取消', badge: 'badge-red' },
} }
export default { export default {
@@ -146,41 +192,62 @@ export default {
return { return {
loading: false, saving: false, loading: false, saving: false,
tableData: [], total: 0, tableData: [], total: 0,
query: { page: 1, page_size: 20, status: '', start_date: '', end_date: '' }, query: { page: 1, page_size: 50, status: '', cold_coil_no: '', start_date: '', end_date: '' },
statusOptions: Object.entries(STATUS_MAP).map(([value, { label }]) => ({ value, label })), statusOptions: Object.entries(STATUS_MAP).map(([value, { label }]) => ({ value, label })),
dialogVisible: false, editRow: null, form: { priority: 5 }, dialogVisible: false, editRow: null, form: { split_count: 1, status: 'ready' },
} }
}, },
created() { this.fetchData() }, created() { this.fetchData() },
methods: { methods: {
async fetchData() { async fetchData() {
this.loading = true this.loading = true
const params = { ...this.query } const params = { page: this.query.page, page_size: this.query.page_size }
if (params.start_date) params.start_date += 'T00:00:00' if (this.query.status) params.status = this.query.status
if (params.end_date) params.end_date += 'T23:59:59' if (this.query.start_date) params.start_date = this.query.start_date + 'T00:00:00'
try { const res = await getPlans(params); this.tableData = res.data.items; this.total = res.data.total } finally { this.loading = false } if (this.query.end_date) params.end_date = this.query.end_date + 'T23:59:59'
try {
const res = await getPlans(params)
let items = res.data.items
if (this.query.cold_coil_no) {
items = items.filter(x => (x.cold_coil_no || '').includes(this.query.cold_coil_no))
}
this.tableData = items
this.total = res.data.total
} finally { this.loading = false }
}, },
fmtDate(t) { return t ? t.slice(0, 10) : '—' }, fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
statusLabel(s) { return STATUS_MAP[s]?.label || s }, fmtNum(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' },
statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' },
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' }, statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
completionRate(row) { return row.plan_quantity > 0 ? Math.min(100, Math.round(row.actual_quantity / row.plan_quantity * 100)) : 0 }, openDialog(row = null) {
rateColor(row) { this.editRow = row
const r = this.completionRate(row) if (row) {
return r >= 90 ? 'var(--accent-green)' : r >= 70 ? 'var(--accent-yellow)' : 'var(--accent-red)' const r = { ...row }
if (r.plan_date) r.plan_date = r.plan_date.slice(0, 16)
this.form = r
} else {
this.form = { plan_no: '', split_count: 1, status: 'ready', plan_date: this.nowDT() }
}
this.dialogVisible = true
},
nowDT() {
const d = new Date(); const p = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`
}, },
openDialog(row = null) { this.editRow = row; this.form = row ? { ...row } : { priority: 5, plan_quantity: 0, plan_weight: 0 }; this.dialogVisible = true },
async confirmPlan(row) { async confirmPlan(row) {
if (!confirm(`确认计划 ${row.plan_no}`)) return if (!confirm(`计划 ${row.plan_no} 上线`)) return
await apiConfirm(row.id) await apiConfirm(row.id)
this.$message.success('已确认') this.$message.success('已上线')
this.fetchData() this.fetchData()
}, },
async save() { async save() {
if (!this.form.plan_no) { this.$message.error('计划号不能为空'); return } if (!this.form.plan_no) { this.$message.error('计划号不能为空'); return }
if (!this.form.plan_date) { this.$message.error('计划时间不能为空'); return }
this.saving = true this.saving = true
try { try {
const d = { ...this.form } const d = { ...this.form }
if (d.plan_date && !d.plan_date.includes('T')) d.plan_date += 'T00:00:00' if (d.plan_date && !d.plan_date.includes(':')) d.plan_date += 'T00:00:00'
else if (d.plan_date && d.plan_date.length === 16) d.plan_date += ':00'
if (this.editRow) await updatePlan(this.editRow.id, d) if (this.editRow) await updatePlan(this.editRow.id, d)
else await createPlan(d) else await createPlan(d)
this.$message.success('保存成功') this.$message.success('保存成功')
@@ -195,6 +262,7 @@ export default {
@import '@/assets/styles/variables'; @import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 12px; &:hover { text-decoration: underline; } } .action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 12px; &:hover { text-decoration: underline; } }
.form-field { display: flex; flex-direction: column; gap: 5px; } .form-field { display: flex; flex-direction: column; gap: 5px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; } .modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }

View File

@@ -43,10 +43,10 @@
</div> </div>
</div> </div>
<!-- 酸槽 4-6 --> <!-- 酸槽 4-5 -->
<div class="sec-title mt8">酸槽 4#6#</div> <div class="sec-title mt8">酸槽 4#5#</div>
<div class="grid-3"> <div class="grid-3">
<div v-for="i in [3,4,5]" :key="i" class="card"> <div v-for="i in [3,4]" :key="i" class="card">
<div class="card-header"> <div class="card-header">
{{ i+1 }}# 酸槽 {{ i+1 }}# 酸槽
<span :class="['badge', tankBadge(tanks[i])]" style="margin-left:auto;">{{ tankStatus(tanks[i]) }}</span> <span :class="['badge', tankBadge(tanks[i])]" style="margin-left:auto;">{{ tankStatus(tanks[i]) }}</span>
@@ -140,13 +140,13 @@
</span> </span>
</div> </div>
<div class="grid-3" style="gap:8px;margin-bottom:8px;"> <div class="grid-3" style="gap:8px;margin-bottom:8px;">
<div v-for="i in 6" :key="i" class="form-field"> <div v-for="i in 5" :key="i" class="form-field">
<div class="kv-label">{{ i }}# 槽浓度 (g/L)</div> <div class="kv-label">{{ i }}# 槽浓度 (g/L)</div>
<input v-model.number="calc.acid_conc_list[i-1]" type="number" class="kv-input" step="5" @change="doCalc" /> <input v-model.number="calc.acid_conc_list[i-1]" type="number" class="kv-input" step="5" @change="doCalc" />
</div> </div>
</div> </div>
<div class="grid-3" style="gap:8px;margin-bottom:14px;"> <div class="grid-3" style="gap:8px;margin-bottom:14px;">
<div v-for="i in 6" :key="'t'+i" class="form-field"> <div v-for="i in 5" :key="'t'+i" class="form-field">
<div class="kv-label">{{ i }}# 槽温度 (°C)</div> <div class="kv-label">{{ i }}# 槽温度 (°C)</div>
<input v-model.number="calc.acid_temp_list[i-1]" type="number" class="kv-input" step="1" @change="doCalc" /> <input v-model.number="calc.acid_temp_list[i-1]" type="number" class="kv-input" step="1" @change="doCalc" />
</div> </div>
@@ -326,7 +326,7 @@ export default {
return { return {
lastRefresh: '--:--:--', lastRefresh: '--:--:--',
l1Online: false, l1Online: false,
tanks: Array.from({ length: 6 }, () => ({ conc: null, temp: null, fe2: null, rt: null, eff: null })), tanks: Array.from({ length: 5 }, () => ({ conc: null, temp: null, fe2: null, rt: null, eff: null })),
rinse: Array.from({ length: 5 }, () => ({ ph: null, temp: null, flow: null, conductivity: null })), rinse: Array.from({ length: 5 }, () => ({ ph: null, temp: null, flow: null, conductivity: null })),
current: { speed: null, tension_inlet: null, tension_outlet: null, acid_temp: null, coil_no: null }, current: { speed: null, tension_inlet: null, tension_outlet: null, acid_temp: null, coil_no: null },
steelGrades: STEEL_GRADES, steelGrades: STEEL_GRADES,
@@ -336,8 +336,8 @@ export default {
steel_grade: 'Q235', steel_grade: 'Q235',
target_pi: 95, target_pi: 95,
scale_weight: 8.5, scale_weight: 8.5,
acid_conc_list: [200, 188, 175, 162, 148, 135], acid_conc_list: [200, 185, 170, 155, 140],
acid_temp_list: [80, 78, 76, 75, 74, 72], acid_temp_list: [80, 78, 76, 75, 74],
}, },
calculating: false, calculating: false,
calcResult: null, calcResult: null,

View File

@@ -4,25 +4,33 @@
<div class="card-body" style="padding:10px 14px;"> <div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;"> <div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row"> <div class="flex-row">
<span class="kv-label">卷号</span> <span class="kv-label">卷号</span>
<input v-model="query.coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" /> <input v-model="query.coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
</div> </div>
<div class="flex-row"> <div class="flex-row">
<span class="kv-label"></span> <span class="kv-label"></span>
<select v-model="query.shift" class="kv-input" style="width:90px;"> <select v-model="query.shift" class="kv-input" style="width:80px;">
<option value="">全部</option> <option value="">全部</option>
<option v-for="s in ['','','','']" :key="s" :value="s">{{ s }}</option> <option v-for="s in ['A','B','C','D']" :key="s" :value="s">{{ s }}</option>
</select> </select>
</div> </div>
<div class="flex-row"> <div class="flex-row">
<span class="kv-label">开始日期</span> <span class="kv-label">状态</span>
<input v-model="query.start_date" type="date" class="kv-input" style="width:140px;" /> <select v-model="query.status" class="kv-input" style="width:110px;">
<option value="">全部</option>
<option value="UNWEIGH">未称重</option>
<option value="PRODUCT">已产出</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">日期</span>
<input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" />
<span class="kv-label">~</span> <span class="kv-label">~</span>
<input v-model="query.end_date" type="date" class="kv-input" style="width:140px;" /> <input v-model="query.end_date" type="date" class="kv-input" style="width:130px;" />
</div> </div>
<div class="flex-row"> <div class="flex-row">
<button class="btn btn-primary" @click="fetchData">查询</button> <button class="btn btn-primary" @click="fetchData">查询</button>
<button class="btn btn-outline" @click="openDialog()"> 新增</button> <button class="btn btn-outline" @click="openDialog()"> 新增实绩</button>
</div> </div>
</div> </div>
</div> </div>
@@ -34,99 +42,158 @@
<span class="ch-badge"> {{ total }} </span> <span class="ch-badge"> {{ total }} </span>
</div> </div>
<div class="table-scroll" v-loading="loading"> <div class="table-scroll" v-loading="loading">
<table class="data-table"> <table class="data-table compact">
<thead> <thead>
<tr> <tr>
<th>卷号</th><th></th><th>开始时间</th><th>结束时间</th> <th>子卷号</th><th>卷号</th><th></th><th></th><th>钢种</th>
<th>处理重量(kg)</th><th>平均速度</th><th>最大速度</th> <th>来料厚度</th><th>出口厚度</th><th>偏差上限</th><th>偏差下限</th>
<th>酸耗(L)</th><th>入口厚</th><th>出口</th><th>质量等级</th><th>操作</th> <th>来料宽</th><th>出口</th>
<th>来料重量</th><th>称重重量</th>
<th>包装要求</th>
<th>表面质量</th><th>表面版型</th>
<th>成品质量</th><th>成品长度</th><th>吨钢长度</th>
<th>下线时间</th><th>状态</th><th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in tableData" :key="row.id"> <tr v-for="row in tableData" :key="row.id">
<td class="td-num">{{ row.coil_no }}</td> <td class="td-num">{{ row.sub_coil_no || row.coil_no }}</td>
<td class="td-num">{{ row.hot_coil_no || '—' }}</td>
<td>{{ row.shift || '—' }}</td> <td>{{ row.shift || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.start_time) }}</td> <td>{{ row.team != null ? row.team : '—' }}</td>
<td class="td-muted">{{ fmtTime(row.end_time) }}</td> <td>{{ row.steel_grade || '—' }}</td>
<td class="td-num">{{ row.process_weight || '—' }}</td> <td class="td-num">{{ fmt(row.incoming_thickness) }}</td>
<td class="td-num">{{ row.avg_speed ? row.avg_speed + ' m/min' : '—' }}</td> <td class="td-num">{{ fmt(row.outlet_thickness) }}</td>
<td class="td-num">{{ row.max_speed ? row.max_speed + ' m/min' : '—' }}</td> <td class="td-num">{{ fmt(row.deviation_upper, 3) }}</td>
<td class="td-num">{{ row.acid_consumption || '—' }}</td> <td class="td-num">{{ fmt(row.deviation_lower, 3) }}</td>
<td class="td-num">{{ row.inlet_thickness || '—' }}</td> <td class="td-num">{{ fmt(row.incoming_width, 0) }}</td>
<td class="td-num">{{ row.outlet_thickness || '—' }}</td> <td class="td-num">{{ fmt(row.outlet_width, 0) }}</td>
<td class="td-num">{{ fmt(row.incoming_weight, 3) }}</td>
<td class="td-num">{{ fmt(row.weighed_weight, 3) }}</td>
<td>{{ row.packaging_req || '—' }}</td>
<td>{{ row.surface_quality || '—' }}</td>
<td>--</td>
<td class="td-num">{{ fmt(row.product_quality) }}</td>
<td class="td-num">{{ fmt(row.product_length, 0) }}</td>
<td class="td-num">{{ fmt(row.length_per_ton) }}</td>
<td class="td-muted">{{ fmtTime(row.offline_time) }}</td>
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td> <td>
<span v-if="row.quality_grade" :class="['badge', gradeClass(row.quality_grade)]">{{ row.quality_grade }}</span> <span class="action-link" @click="openDialog(row)">编辑</span>
<span v-else class="td-muted"></span> <span class="action-link" style="color:#1d8eff" @click="viewCert(row)">质保书</span>
</td> </td>
<td><span class="action-link" @click="openDialog(row)">编辑</span></td>
</tr> </tr>
<tr v-if="!tableData.length && !loading"> <tr v-if="!tableData.length && !loading">
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td> <td colspan="22" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card-body" style="padding:8px 14px;" v-if="total > query.page_size">
<div class="flex-row">
<button class="btn btn-outline" :disabled="query.page<=1" @click="query.page--;fetchData()">上一页</button>
<span class="kv-label"> {{ query.page }} / {{ Math.ceil(total/query.page_size) }} </span>
<button class="btn btn-outline" :disabled="query.page>=Math.ceil(total/query.page_size)" @click="query.page++;fetchData()">下一页</button>
</div>
</div>
</div> </div>
<!-- Modal --> <!-- 新增/编辑弹窗 -->
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false"> <div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:680px;"> <div class="modal-box" style="width:820px;">
<div class="modal-header"> <div class="modal-header">
{{ editRow ? '编辑实绩' : '新增实绩' }} {{ editRow ? '编辑实绩' : '新增实绩' }}
<span class="modal-close" @click="dialogVisible=false"></span> <span class="modal-close" @click="dialogVisible=false"></span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="grid-2" style="gap:12px;"> <div class="grid-3" style="gap:12px;">
<div class="form-field"> <div class="form-field">
<div class="kv-label">卷号 *</div> <div class="kv-label">卷号 *</div>
<input v-model="form.coil_no" class="kv-input" /> <input v-model="form.coil_no" class="kv-input" :disabled="!!editRow" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">班次</div> <div class="kv-label">热卷号</div>
<input v-model="form.hot_coil_no" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">钢种</div>
<input v-model="form.steel_grade" class="kv-input" placeholder="QTGLG-2019" />
</div>
<div class="form-field">
<div class="kv-label"></div>
<select v-model="form.shift" class="kv-input"> <select v-model="form.shift" class="kv-input">
<option v-for="s in ['甲','乙','丙','丁']" :key="s" :value="s">{{ s }}</option> <option value=""></option>
<option v-for="s in ['A','B','C','D']" :key="s" :value="s">{{ s }}</option>
</select> </select>
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">开始时间</div> <div class="kv-label"></div>
<input v-model="form.start_time" type="datetime-local" class="kv-input" /> <input v-model="form.team" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">结束时间</div> <div class="kv-label">来料厚度 (mm)</div>
<input v-model="form.end_time" type="datetime-local" class="kv-input" /> <input v-model.number="form.incoming_thickness" type="number" step="0.01" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">处理重量 (kg)</div> <div class="kv-label">出口厚度 (mm)</div>
<input v-model.number="form.process_weight" type="number" class="kv-input" /> <input v-model.number="form.outlet_thickness" type="number" step="0.01" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">平均速度 (m/min)</div> <div class="kv-label">偏差上限</div>
<input v-model.number="form.avg_speed" type="number" class="kv-input" /> <input v-model.number="form.deviation_upper" type="number" step="0.001" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">酸耗 (L)</div> <div class="kv-label">偏差下限</div>
<input v-model.number="form.acid_consumption" type="number" class="kv-input" /> <input v-model.number="form.deviation_lower" type="number" step="0.001" class="kv-input" />
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">质量等级</div> <div class="kv-label">来料宽度 (mm)</div>
<select v-model="form.quality_grade" class="kv-input"> <input v-model.number="form.incoming_width" type="number" class="kv-input" />
<option value="A1">A1</option> </div>
<option value="A2">A2</option> <div class="form-field">
<option value="B1">B1</option> <div class="kv-label">出口宽度 (mm)</div>
<option value="B2">B2</option> <input v-model.number="form.outlet_width" type="number" class="kv-input" />
<option value="C">C</option> </div>
<div class="form-field">
<div class="kv-label">来料重量 (t)</div>
<input v-model.number="form.incoming_weight" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">称重重量 (t)</div>
<input v-model.number="form.weighed_weight" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">包装要求</div>
<select v-model="form.packaging_req" class="kv-input">
<option value=""></option>
<option value="简包">简包</option>
<option value="筒包">筒包</option>
<option value="裸包">裸包</option>
</select> </select>
</div> </div>
<div class="form-field"> <div class="form-field">
<div class="kv-label">操作员</div> <div class="kv-label">表面质量</div>
<input v-model="form.operator" class="kv-input" /> <input v-model="form.surface_quality" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">表面版型</div>
<input class="kv-input" value="--" disabled />
</div>
<div class="form-field">
<div class="kv-label">成品质量 (%)</div>
<input v-model.number="form.product_quality" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">成品长度 (m)</div>
<input v-model.number="form.product_length" type="number" step="1" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">吨钢长度 (m/t)</div>
<input v-model.number="form.length_per_ton" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">下线时间</div>
<input v-model="form.offline_time" type="datetime-local" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">状态</div>
<select v-model="form.status" class="kv-input">
<option value="UNWEIGH">未称重</option>
<option value="PRODUCT">已产出</option>
</select>
</div> </div>
</div> </div>
</div> </div>
@@ -136,65 +203,132 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 质保书弹窗 -->
<div v-if="certVisible" class="modal-mask" @click.self="certVisible=false">
<div class="modal-box" style="width:680px;">
<div class="modal-header">
质保书 {{ certRow && (certRow.sub_coil_no || certRow.coil_no) }}
<span class="modal-close" @click="certVisible=false"></span>
</div>
<div class="modal-body" style="background:#fff;color:#000;padding:24px;">
<div style="text-align:center;font-size:18px;font-weight:600;margin-bottom:8px;">钢卷质量保证书</div>
<div style="text-align:center;font-size:11px;color:#666;margin-bottom:18px;">Quality Certificate</div>
<table class="cert-table">
<tr><th>子卷号</th><td>{{ certRow.sub_coil_no || certRow.coil_no }}</td><th>热卷号</th><td>{{ certRow.hot_coil_no || '—' }}</td></tr>
<tr><th>钢种</th><td>{{ certRow.steel_grade || '—' }}</td><th>/</th><td>{{ (certRow.shift || '—') + ' / ' + (certRow.team != null ? certRow.team : '—') }}</td></tr>
<tr><th>来料厚度</th><td>{{ fmt(certRow.incoming_thickness) }} mm</td><th>出口厚度</th><td>{{ fmt(certRow.outlet_thickness) }} mm</td></tr>
<tr><th>偏差上限</th><td>{{ fmt(certRow.deviation_upper, 3) }}</td><th>偏差下限</th><td>{{ fmt(certRow.deviation_lower, 3) }}</td></tr>
<tr><th>来料宽度</th><td>{{ fmt(certRow.incoming_width, 0) }} mm</td><th>出口宽度</th><td>{{ fmt(certRow.outlet_width, 0) }} mm</td></tr>
<tr><th>来料重量</th><td>{{ fmt(certRow.incoming_weight, 3) }} t</td><th>称重重量</th><td>{{ fmt(certRow.weighed_weight, 3) }} t</td></tr>
<tr><th>包装要求</th><td>{{ certRow.packaging_req || '—' }}</td><th>表面质量</th><td>{{ certRow.surface_quality || '—' }}</td></tr>
<tr><th>表面版型</th><td colspan="3">--</td></tr>
<tr><th>成品质量</th><td>{{ fmt(certRow.product_quality) }} %</td><th>成品长度</th><td>{{ fmt(certRow.product_length, 0) }} m</td></tr>
<tr><th>吨钢长度</th><td>{{ fmt(certRow.length_per_ton) }} m/t</td><th>下线时间</th><td>{{ fmtTime(certRow.offline_time) }}</td></tr>
<tr><th>备注</th><td colspan="3">{{ certRow.remark || '—' }}</td></tr>
</table>
<div style="margin-top:18px;display:flex;justify-content:space-between;font-size:12px;color:#666;">
<div>检验员________________</div>
<div>签发日期{{ today }}</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="certVisible=false">关闭</button>
<button class="btn btn-primary" @click="printCert">打印</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { getProductionRecords, createProductionRecord, updateProductionRecord } from '@/api' import { getProductionRecords, createProductionRecord, updateProductionRecord } from '@/api'
const STATUS_MAP = {
UNWEIGH: { label: '未称重', badge: 'badge-yellow' },
PRODUCT: { label: '已产出', badge: 'badge-blue' },
}
export default { export default {
name: 'Production', name: 'Production',
data() { data() {
return { return {
loading: false, saving: false, loading: false, saving: false,
tableData: [], total: 0, tableData: [], total: 0,
query: { page: 1, page_size: 20, coil_no: '', shift: '', start_date: '', end_date: '' }, query: { page: 1, page_size: 50, coil_no: '', shift: '', status: '', start_date: '', end_date: '' },
dialogVisible: false, editRow: null, form: {}, dialogVisible: false, editRow: null, form: { status: 'UNWEIGH' },
certVisible: false, certRow: {},
} }
}, },
computed: {
today() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
},
},
created() { this.fetchData() }, created() { this.fetchData() },
methods: { methods: {
async fetchData() { async fetchData() {
this.loading = true this.loading = true
const params = { ...this.query } const params = { page: this.query.page, page_size: this.query.page_size }
if (params.start_date) params.start_date = params.start_date + 'T00:00:00' if (this.query.coil_no) params.coil_no = this.query.coil_no
if (params.end_date) params.end_date = params.end_date + 'T23:59:59' if (this.query.shift) params.shift = this.query.shift
if (this.query.start_date) params.start_date = this.query.start_date
if (this.query.end_date) params.end_date = this.query.end_date
try { try {
const res = await getProductionRecords(params) const res = await getProductionRecords(params)
this.tableData = res.data.items let items = res.data.items || []
if (this.query.status) items = items.filter(x => x.status === this.query.status)
this.tableData = items
this.total = res.data.total this.total = res.data.total
} finally { this.loading = false } } finally { this.loading = false }
}, },
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' }, fmt(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' },
gradeClass(g) { fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
if (g?.startsWith('A')) return 'badge-green' statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' },
if (g?.startsWith('B')) return 'badge-blue' statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
return 'badge-yellow'
},
openDialog(row = null) { openDialog(row = null) {
this.editRow = row; this.form = row ? { ...row } : {}; this.dialogVisible = true this.editRow = row
if (row) {
const r = { ...row }
if (r.offline_time) r.offline_time = r.offline_time.slice(0, 16)
this.form = r
} else {
this.form = { coil_no: '', status: 'UNWEIGH' }
}
this.dialogVisible = true
}, },
async save() { async save() {
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return } if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return }
this.saving = true this.saving = true
try { try {
if (this.editRow) await updateProductionRecord(this.editRow.id, this.form) const d = { ...this.form }
else await createProductionRecord(this.form) if (d.offline_time && d.offline_time.length === 16) d.offline_time += ':00'
if (this.editRow) await updateProductionRecord(this.editRow.id, d)
else await createProductionRecord(d)
this.$message.success('保存成功') this.$message.success('保存成功')
this.dialogVisible = false; this.fetchData() this.dialogVisible = false; this.fetchData()
} finally { this.saving = false } } finally { this.saving = false }
} },
viewCert(row) { this.certRow = row; this.certVisible = true },
printCert() { window.print() },
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/styles/variables'; @import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 12px; &:hover { text-decoration: underline; } } .action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
.form-field { display: flex; flex-direction: column; gap: 5px; } .form-field { display: flex; flex-direction: column; gap: 5px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.data-table.compact th, .data-table.compact td { padding: 4px 6px; font-size: 11.5px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; width: 640px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; } .modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
.modal-body { padding: 16px; overflow-y: auto; } .modal-body { padding: 16px; overflow-y: auto; }
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; } .modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
.cert-table { width: 100%; border-collapse: collapse; font-size: 12px;
th, td { border: 1px solid #888; padding: 6px 10px; }
th { background: #eee; width: 110px; text-align: left; }
}
</style> </style>

View File

@@ -3,7 +3,7 @@
<!-- 标签页 --> <!-- 标签页 -->
<div class="tab-bar"> <div class="tab-bar">
<span :class="['tab-item', { active: activeTab === 'tasks' }]" @click="activeTab = 'tasks'">检验任务</span> <span :class="['tab-item', { active: activeTab === 'tasks' }]" @click="activeTab = 'tasks'">检验任务</span>
<span :class="['tab-item', { active: activeTab === 'defects' }]" @click="activeTab = 'defects'; fetchDefects()">缺陷记录</span> <span :class="['tab-item', { active: activeTab === 'abnormal' }]" @click="switchToAbnormal">异常管理</span>
</div> </div>
<!-- 检验任务 Tab --> <!-- 检验任务 Tab -->
@@ -38,12 +38,6 @@
<option value="unqualified">不合格</option> <option value="unqualified">不合格</option>
</select> </select>
</div> </div>
<div class="flex-row">
<span class="kv-label">日期</span>
<input v-model="taskQuery.start_date" type="date" class="kv-input" style="width:140px;" />
<span class="kv-label">~</span>
<input v-model="taskQuery.end_date" type="date" class="kv-input" style="width:140px;" />
</div>
<div class="flex-row"> <div class="flex-row">
<button class="btn btn-primary" @click="fetchTasks">查询</button> <button class="btn btn-primary" @click="fetchTasks">查询</button>
<button class="btn btn-outline" @click="openTaskDialog()"> 新增任务</button> <button class="btn btn-outline" @click="openTaskDialog()"> 新增任务</button>
@@ -52,223 +46,193 @@
</div> </div>
</div> </div>
<!-- 主体区域任务列表 + 检验项面板 -->
<div class="section-row">
<!-- 任务列表 -->
<div class="card" style="flex:3;min-width:0;">
<div class="card-header">
检验任务列表
<span class="ch-badge"> {{ taskTotal }} </span>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>任务编号</th>
<th>卷号</th>
<th>方案名称</th>
<th>检验人员</th>
<th>检验时间</th>
<th>状态</th>
<th>结果</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in taskData"
:key="row.id"
:class="{ 'row-selected': selectedTask && selectedTask.id === row.id }"
style="cursor:pointer;"
@click="selectTask(row)"
>
<td class="td-num">{{ row.task_code }}</td>
<td class="td-num">{{ row.coil_no || '—' }}</td>
<td>{{ row.scheme_name || '—' }}</td>
<td class="td-muted">{{ row.inspect_user || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
<td><span :class="['badge', taskStatusBadge(row.status)]">{{ taskStatusLabel(row.status) }}</span></td>
<td>
<span v-if="row.result" :class="['badge', row.result === 'qualified' ? 'badge-green' : 'badge-red']">
{{ row.result === 'qualified' ? '合格' : '不合格' }}
</span>
<span v-else class="td-muted"></span>
</td>
<td class="td-muted">{{ fmtTime(row.created_at) }}</td>
<td>
<span class="action-link" @click.stop="openTaskDialog(row)">编辑</span>
<span class="action-link" style="color:#da3633;" @click.stop="deleteTask(row)">删除</span>
</td>
</tr>
<tr v-if="!taskData.length">
<td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="card-body" style="padding:8px 14px;" v-if="taskTotal > taskQuery.page_size">
<div class="flex-row">
<button class="btn btn-outline" :disabled="taskQuery.page <= 1" @click="taskQuery.page--; fetchTasks()">上一页</button>
<span class="kv-label"> {{ taskQuery.page }} / {{ Math.ceil(taskTotal / taskQuery.page_size) }} </span>
<button class="btn btn-outline" :disabled="taskQuery.page >= Math.ceil(taskTotal / taskQuery.page_size)" @click="taskQuery.page++; fetchTasks()">下一页</button>
</div>
</div>
</div>
<!-- 检验项面板 -->
<div class="card" style="flex:2;min-width:0;">
<div class="card-header">
<span v-if="selectedTask">
检验项 &nbsp;<span class="td-muted" style="font-weight:400;font-size:11px;">{{ selectedTask.task_code }}</span>
</span>
<span v-else>检验项请点击任务行</span>
<button v-if="selectedTask" class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openItemDialog()"> 添加项目</button>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>检验项名称</th>
<th>类型</th>
<th>标准值</th>
<th>上限</th>
<th>下限</th>
<th>单位</th>
<th>实测值</th>
<th>是否合格</th>
<th>判定结果</th>
<th>检验人</th>
<th>检验时间</th>
</tr>
</thead>
<tbody>
<tr v-for="item in taskItems" :key="item.id">
<td>{{ item.item_name }}</td>
<td class="td-muted">{{ item.item_type || '—' }}</td>
<td class="td-num">{{ item.standard_value != null ? item.standard_value : '—' }}</td>
<td class="td-num">{{ item.upper_limit != null ? item.upper_limit : '—' }}</td>
<td class="td-num">{{ item.lower_limit != null ? item.lower_limit : '—' }}</td>
<td class="td-muted">{{ item.unit || '—' }}</td>
<td class="td-num">{{ item.inspect_value || '—' }}</td>
<td>
<span v-if="item.is_qualified != null" :class="['badge', item.is_qualified ? 'badge-green' : 'badge-red']">
{{ item.is_qualified ? '合格' : '不合格' }}
</span>
<span v-else class="td-muted"></span>
</td>
<td class="td-muted">{{ item.judge_result || '—' }}</td>
<td class="td-muted">{{ item.inspect_user || '—' }}</td>
<td class="td-muted">{{ fmtTime(item.inspect_time) }}</td>
</tr>
<tr v-if="!taskItems.length">
<td colspan="11" class="td-muted" style="text-align:center;padding:20px;">
{{ selectedTask ? '暂无检验项' : '请先选择任务' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<!-- 缺陷记录 Tab -->
<template v-if="activeTab === 'defects'">
<!-- 过滤栏 -->
<div class="card">
<div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row">
<span class="kv-label">卷号</span>
<input v-model="defectQuery.coil_no" class="kv-input" style="width:130px;" @keyup.enter="fetchDefects" />
</div>
<div class="flex-row">
<span class="kv-label">缺陷类型</span>
<input v-model="defectQuery.defect_type" class="kv-input" style="width:120px;" @keyup.enter="fetchDefects" />
</div>
<div class="flex-row">
<span class="kv-label">严重程度</span>
<select v-model="defectQuery.degree" class="kv-input" style="width:100px;">
<option value="">全部</option>
<option value="light">轻微</option>
<option value="normal">一般</option>
<option value="serious">严重</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">日期</span>
<input v-model="defectQuery.start_date" type="date" class="kv-input" style="width:140px;" />
<span class="kv-label">~</span>
<input v-model="defectQuery.end_date" type="date" class="kv-input" style="width:140px;" />
</div>
<div class="flex-row">
<button class="btn btn-primary" @click="fetchDefects">查询</button>
<button class="btn btn-outline" @click="openDefectDialog()"> 新增缺陷</button>
</div>
</div>
</div>
</div>
<!-- 缺陷列表 -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
缺陷记录列表 检验任务列表
<span class="ch-badge"> {{ defectTotal }} </span> <span class="ch-badge"> {{ taskTotal }} </span>
</div> </div>
<div class="table-scroll"> <div class="table-scroll">
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
<th>卷号</th> <th>任务编号</th><th>卷号</th><th>方案名称</th><th>检验人员</th>
<th>产线</th> <th>检验时间</th><th>状态</th><th>结果</th><th>创建时间</th><th>操作</th>
<th>位置</th>
<th>板面</th>
<th>缺陷代码</th>
<th>缺陷类型</th>
<th>缺陷率(%)</th>
<th>缺陷重量</th>
<th>严重程度</th>
<th>判定等级</th>
<th>判定人</th>
<th>判定时间</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in defectData" :key="row.id"> <tr v-for="row in taskData" :key="row.id">
<td class="td-num">{{ row.task_code }}</td>
<td class="td-num">{{ row.coil_no || '—' }}</td> <td class="td-num">{{ row.coil_no || '—' }}</td>
<td class="td-muted">{{ row.production_line || '—' }}</td> <td>{{ row.scheme_name || '—' }}</td>
<td class="td-muted">{{ row.position || '—' }}</td> <td class="td-muted">{{ row.inspect_user || '—' }}</td>
<td class="td-muted">{{ row.plate_surface || '—' }}</td> <td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
<td class="td-num">{{ row.defect_code || '—' }}</td> <td><span :class="['badge', taskStatusBadge(row.status)]">{{ taskStatusLabel(row.status) }}</span></td>
<td>{{ row.defect_type || '—' }}</td>
<td class="td-num">{{ row.defect_rate != null ? row.defect_rate : '—' }}</td>
<td class="td-num">{{ row.defect_weight != null ? row.defect_weight : '—' }}</td>
<td> <td>
<span v-if="row.degree" :class="['badge', degreeBadge(row.degree)]">{{ degreeLabel(row.degree) }}</span> <span v-if="row.result" :class="['badge', row.result === 'qualified' ? 'badge-green' : 'badge-red']">
{{ row.result === 'qualified' ? '合格' : '不合格' }}
</span>
<span v-else class="td-muted"></span> <span v-else class="td-muted"></span>
</td> </td>
<td class="td-muted">{{ row.judge_level || '—' }}</td> <td class="td-muted">{{ fmtTime(row.created_at) }}</td>
<td class="td-muted">{{ row.judge_by || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.judge_time) }}</td>
<td> <td>
<span class="action-link" @click="openDefectDialog(row)">编辑</span> <span class="action-link" @click="openTaskDialog(row)">编辑</span>
<span class="action-link" style="color:#da3633;" @click="deleteDefect(row)">删除</span> <span class="action-link" style="color:#da3633;" @click="deleteTask(row)">删除</span>
</td> </td>
</tr> </tr>
<tr v-if="!defectData.length"> <tr v-if="!taskData.length">
<td colspan="13" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td> <td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card-body" style="padding:8px 14px;" v-if="defectTotal > defectQuery.page_size"> </div>
<div class="flex-row"> </template>
<button class="btn btn-outline" :disabled="defectQuery.page <= 1" @click="defectQuery.page--; fetchDefects()">上一页</button>
<span class="kv-label"> {{ defectQuery.page }} / {{ Math.ceil(defectTotal / defectQuery.page_size) }} </span> <!-- 异常管理 Tab -->
<button class="btn btn-outline" :disabled="defectQuery.page >= Math.ceil(defectTotal / defectQuery.page_size)" @click="defectQuery.page++; fetchDefects()">下一页</button> <template v-if="activeTab === 'abnormal'">
<div class="abn-layout">
<!-- 左侧钢卷列表 -->
<div class="abn-sidebar">
<div class="sidebar-header">
钢卷列表
<span class="add-btn" title="刷新" @click="fetchCoils"></span>
</div> </div>
<div class="sidebar-search">
<input v-model="coilQuery.coil_no" class="kv-input" placeholder="搜索卷号..." style="width:100%;" @keyup.enter="fetchCoils" />
</div>
<div class="cl-list">
<div
v-for="c in coils"
:key="c.id"
:class="['cl-item', { active: selectedCoil && selectedCoil.id === c.id }]"
@click="selectCoil(c)"
>
<div class="cl-name">{{ c.coil_no }}</div>
<div class="cl-meta">
<span class="td-muted" style="font-size:10px;">{{ c.steel_grade || '—' }}</span>
<span class="td-muted" style="font-size:10px;">{{ c.spec_thickness ? c.spec_thickness + '×' + (c.spec_width || '?') : '' }}</span>
</div>
</div>
<div v-if="!coils.length" class="cl-empty">暂无钢卷</div>
</div>
</div>
<!-- 右侧异常管理面板 -->
<div class="abn-main">
<div v-if="!selectedCoil" class="empty-tip">请从左侧选择钢卷</div>
<template v-else>
<!-- 钢卷信息 -->
<div class="card">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<span>钢卷信息</span>
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="reloadCoil"> 刷新</button>
</div>
<div class="card-body">
<div class="kv-grid">
<div class="kv-cell"><span class="kv-label">入场卷号</span><span class="kv-value">{{ selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">当前卷号</span><span class="kv-value">{{ selectedCoil.coil_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">厂家原料号</span><span class="kv-value">{{ selectedCoil.order_no || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">逻辑库位</span><span class="kv-value">酸连轧原料库</span></div>
<div class="kv-cell"><span class="kv-label">实际库区</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">班组</span><span class="kv-value">{{ selectedCoil.shift || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">材料类型</span><span class="kv-value">原料</span></div>
<div class="kv-cell"><span class="kv-label">物料名</span><span class="kv-value">热轧卷板</span></div>
<div class="kv-cell"><span class="kv-label">规格</span><span class="kv-value">{{ specStr }}</span></div>
<div class="kv-cell"><span class="kv-label">材质</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">厂家</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">镀层质量</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">表面处理</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">质量状态</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">切边要求</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">原料材质</span><span class="kv-value">{{ selectedCoil.steel_grade || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">包装要求</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">实测厚度[mm]</span><span class="kv-value">{{ selectedCoil.target_thickness || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">实测宽度[mm]</span><span class="kv-value">{{ selectedCoil.target_width || '—' }}</span></div>
<div class="kv-cell"><span class="kv-label">长度[m]</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">毛重[t]</span><span class="kv-value">{{ weightT(selectedCoil.gross_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">净重[t]</span><span class="kv-value">{{ weightT(selectedCoil.net_weight) }}</span></div>
<div class="kv-cell"><span class="kv-label">生产开始</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">生产结束</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">调制度</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">镀层种类</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">钢卷表面处理</span><span class="kv-value"></span></div>
<div class="kv-cell"><span class="kv-label">备注</span><span class="kv-value">{{ selectedCoil.remark || '—' }}</span></div>
</div>
</div>
</div>
<!-- 异常记录 -->
<div class="card">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<span>异常记录 <span class="ch-badge">{{ defects.length }} </span></span>
<div class="flex-row" style="gap:8px;">
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="addDefectRow"> 新增行</button>
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="loadDefects"> 刷新</button>
<button class="btn btn-primary" style="padding:2px 14px;font-size:11px;" :disabled="saving" @click="saveDefects">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
<div class="table-scroll">
<table class="data-table abn-table">
<thead>
<tr>
<th style="width:48px;">序号</th>
<th style="min-width:140px;">缺陷描述</th>
<th style="width:90px;">开始位置</th>
<th style="width:90px;">结束位置</th>
<th style="width:70px;">长度</th>
<th style="width:140px;">上下版面</th>
<th style="width:180px;">断面位置</th>
<th style="width:160px;">缺陷代码</th>
<th style="width:110px;">程度</th>
<th style="width:60px;">主缺陷</th>
<th style="min-width:120px;">缺陷图片</th>
<th style="width:60px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(d, idx) in defects" :key="idx">
<td class="td-num">{{ idx + 1 }}</td>
<td><input v-model="d.defect_desc" class="kv-input" style="width:100%;" placeholder="请输入缺陷描述" /></td>
<td><input v-model.number="d.start_position" type="number" class="kv-input" style="width:100%;" /></td>
<td><input v-model.number="d.end_position" type="number" class="kv-input" style="width:100%;" /></td>
<td class="td-num">{{ computeLen(d) }}</td>
<td>
<label class="ck"><input type="checkbox" v-model="d.upper_surface" />上板面</label>
<label class="ck"><input type="checkbox" v-model="d.lower_surface" />下板面</label>
<a class="all-link" @click="setAll(d, ['upper_surface','lower_surface'])">全选</a>
</td>
<td>
<label class="ck"><input type="checkbox" v-model="d.side_op" />操作侧</label>
<label class="ck"><input type="checkbox" v-model="d.side_middle" />中间</label>
<label class="ck"><input type="checkbox" v-model="d.side_drive" />驱动侧</label>
<a class="all-link" @click="setAll(d, ['side_op','side_middle','side_drive'])">全选</a>
</td>
<td>
<label v-for="opt in defectCodeOptions" :key="opt.value" class="rd">
<input type="radio" :value="opt.value" v-model="d.defect_code" />{{ opt.label }}
</label>
</td>
<td>
<label v-for="opt in degreeOptions" :key="opt.value" class="rd">
<input type="radio" :value="opt.value" v-model="d.degree" />{{ opt.label }}
</label>
</td>
<td style="text-align:center;"><input type="checkbox" v-model="d.is_main" /></td>
<td><input v-model="d.image_url" class="kv-input" style="width:100%;" placeholder="图片URL" /></td>
<td>
<span class="action-link" @click="clearRow(d)">清空</span>
<span class="action-link" style="color:#da3633;" @click="removeRow(idx)">删除</span>
</td>
</tr>
<tr v-if="!defects.length">
<td colspan="12" class="td-muted" style="text-align:center;padding:18px;">暂无异常点击 新增行开始录入</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div> </div>
</div> </div>
</template> </template>
@@ -336,160 +300,14 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 添加检验项弹窗 -->
<div v-if="itemDialogVisible" class="modal-mask" @click.self="itemDialogVisible = false">
<div class="modal-box" style="width:500px;">
<div class="modal-header">
添加检验项
<span class="modal-close" @click="itemDialogVisible = false"></span>
</div>
<div class="modal-body">
<div class="grid-2" style="gap:12px;">
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">检验项名称 *</div>
<input v-model="itemForm.item_name" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">类型</div>
<select v-model="itemForm.item_type" class="kv-input">
<option value="">请选择</option>
<option value="quantitative">定量</option>
<option value="qualitative">定性</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">单位</div>
<input v-model="itemForm.unit" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">标准值</div>
<input v-model.number="itemForm.standard_value" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">上限</div>
<input v-model.number="itemForm.upper_limit" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">下限</div>
<input v-model.number="itemForm.lower_limit" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">实测值</div>
<input v-model="itemForm.inspect_value" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">是否合格</div>
<select v-model="itemForm.is_qualified" class="kv-input">
<option :value="null">待判定</option>
<option :value="1">合格</option>
<option :value="0">不合格</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">检验人</div>
<input v-model="itemForm.inspect_user" class="kv-input" />
</div>
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">判定结果说明</div>
<input v-model="itemForm.judge_result" class="kv-input" />
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="itemDialogVisible = false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="saveItem">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
<!-- 新增/编辑缺陷弹窗 -->
<div v-if="defectDialogVisible" class="modal-mask" @click.self="defectDialogVisible = false">
<div class="modal-box" style="width:580px;">
<div class="modal-header">
{{ editDefect ? '编辑缺陷记录 #' + editDefect.id : '新增缺陷记录' }}
<span class="modal-close" @click="defectDialogVisible = false"></span>
</div>
<div class="modal-body">
<div class="grid-2" style="gap:12px;">
<div class="form-field">
<div class="kv-label">卷号</div>
<input v-model="defectForm.coil_no" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">产线</div>
<input v-model="defectForm.production_line" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">位置</div>
<input v-model="defectForm.position" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">板面</div>
<select v-model="defectForm.plate_surface" class="kv-input">
<option value="">请选择</option>
<option value="上表面">上表面</option>
<option value="下表面">下表面</option>
<option value="双面">双面</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">缺陷代码</div>
<input v-model="defectForm.defect_code" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">缺陷类型</div>
<input v-model="defectForm.defect_type" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">缺陷率(%)</div>
<input v-model.number="defectForm.defect_rate" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">缺陷重量(kg)</div>
<input v-model.number="defectForm.defect_weight" type="number" step="0.1" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">严重程度</div>
<select v-model="defectForm.degree" class="kv-input">
<option value="">请选择</option>
<option value="light">轻微</option>
<option value="normal">一般</option>
<option value="serious">严重</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">判定等级</div>
<input v-model="defectForm.judge_level" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">判定人</div>
<input v-model="defectForm.judge_by" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">判定时间</div>
<input v-model="defectForm.judge_time" type="datetime-local" class="kv-input" />
</div>
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">备注</div>
<textarea v-model="defectForm.remark" class="kv-input" rows="2"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="defectDialogVisible = false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="saveDefect">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { import {
getQcTasks, createQcTask, updateQcTask, deleteQcTask, getQcTasks, createQcTask, updateQcTask, deleteQcTask,
getQcTaskItems, createQcTaskItem, getCoils, getCoil,
getQcDefects, createQcDefect, updateQcDefect, deleteQcDefect, getQcDefectsByCoil, bulkSaveQcDefects,
} from '@/api' } from '@/api'
const TASK_STATUS = { const TASK_STATUS = {
@@ -499,10 +317,35 @@ const TASK_STATUS = {
3: { label: '完成', badge: 'badge-green' }, 3: { label: '完成', badge: 'badge-green' },
} }
const DEGREE_MAP = { const DEFECT_CODES = [
light: { label: '轻微', badge: 'badge-blue' }, { value: 'S', label: '表面缺陷S' },
normal: { label: '一般', badge: 'badge-yellow' }, { value: 'E', label: '边部问题E' },
serious: { label: '严重', badge: 'badge-red' }, { value: 'M', label: '尺寸问题M' },
{ value: 'G', label: '收卷问题G' },
{ value: 'F', label: '版型问题F' },
]
const DEGREES = [
{ value: 'light', label: '轻微' },
{ value: 'medium', label: '中度' },
{ value: 'serious', label: '严重' },
]
function blankDefect() {
return {
defect_desc: '',
start_position: 0,
end_position: 0,
upper_surface: false,
lower_surface: false,
side_op: false,
side_middle: false,
side_drive: false,
defect_code: '',
degree: '',
is_main: false,
image_url: '',
}
} }
export default { export default {
@@ -514,46 +357,41 @@ export default {
// 任务 // 任务
taskData: [], taskTotal: 0, taskData: [], taskTotal: 0,
taskQuery: { page: 1, page_size: 20, task_code: '', coil_no: '', status: '', result: '', start_date: '', end_date: '' }, taskQuery: { page: 1, page_size: 20, task_code: '', coil_no: '', status: '', result: '' },
selectedTask: null,
taskItems: [],
taskDialogVisible: false, editTask: null, taskForm: {}, taskDialogVisible: false, editTask: null, taskForm: {},
itemDialogVisible: false, itemForm: {},
// 缺陷 // 异常管理
defectData: [], defectTotal: 0, coils: [],
defectQuery: { page: 1, page_size: 20, coil_no: '', defect_type: '', degree: '', start_date: '', end_date: '' }, coilQuery: { coil_no: '', page: 1, page_size: 50 },
defectDialogVisible: false, editDefect: null, defectForm: {}, selectedCoil: null,
defects: [],
defectCodeOptions: DEFECT_CODES,
degreeOptions: DEGREES,
} }
}, },
created() { computed: {
this.fetchTasks() specStr() {
const c = this.selectedCoil || {}
if (c.spec_thickness && c.spec_width) return `${c.spec_thickness}*${c.spec_width}`
return '—'
},
}, },
created() { this.fetchTasks() },
methods: { methods: {
// ── 任务 ────────────────────────────────────── // ── 任务 ──────────────────────────────────────
async fetchTasks() { async fetchTasks() {
const params = {} const params = { page: this.taskQuery.page, page_size: this.taskQuery.page_size }
if (this.taskQuery.task_code) params.task_code = this.taskQuery.task_code if (this.taskQuery.task_code) params.task_code = this.taskQuery.task_code
if (this.taskQuery.coil_no) params.coil_no = this.taskQuery.coil_no if (this.taskQuery.coil_no) params.coil_no = this.taskQuery.coil_no
if (this.taskQuery.status !== '') params.status = this.taskQuery.status if (this.taskQuery.status !== '') params.status = this.taskQuery.status
if (this.taskQuery.result) params.result = this.taskQuery.result if (this.taskQuery.result) params.result = this.taskQuery.result
if (this.taskQuery.start_date) params.start_date = this.taskQuery.start_date
if (this.taskQuery.end_date) params.end_date = this.taskQuery.end_date
params.page = this.taskQuery.page
params.page_size = this.taskQuery.page_size
try { try {
const res = await getQcTasks(params) const res = await getQcTasks(params)
this.taskData = res.data.items this.taskData = res.data.items
this.taskTotal = res.data.total this.taskTotal = res.data.total
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, },
async selectTask(row) {
this.selectedTask = row
try {
const res = await getQcTaskItems(row.id)
this.taskItems = res.data || []
} catch (e) { this.taskItems = [] }
},
openTaskDialog(row = null) { openTaskDialog(row = null) {
this.editTask = row this.editTask = row
this.taskForm = row ? { ...row } : { task_code: '', status: 0 } this.taskForm = row ? { ...row } : { task_code: '', status: 0 }
@@ -578,79 +416,101 @@ export default {
try { try {
await deleteQcTask(row.id) await deleteQcTask(row.id)
this.$message.success('已删除') this.$message.success('已删除')
if (this.selectedTask && this.selectedTask.id === row.id) {
this.selectedTask = null
this.taskItems = []
}
this.fetchTasks() this.fetchTasks()
} catch (e) { this.$message.error('删除失败') } } catch (e) { this.$message.error('删除失败') }
}, },
// ── 检验项 ──────────────────────────────────── // ── 异常管理 ──────────────────────────────────
openItemDialog() { async switchToAbnormal() {
this.itemForm = { task_id: this.selectedTask.id, item_name: '', is_qualified: null } this.activeTab = 'abnormal'
this.itemDialogVisible = true if (!this.coils.length) await this.fetchCoils()
}, },
async saveItem() { async fetchCoils() {
if (!this.itemForm.item_name) { this.$message.error('检验项名称不能为空'); return }
this.saving = true
try { try {
await createQcTaskItem({ ...this.itemForm, task_id: this.selectedTask.id }) const params = { page: 1, page_size: 50 }
this.$message.success('添加成功') if (this.coilQuery.coil_no) params.coil_no = this.coilQuery.coil_no
this.itemDialogVisible = false const res = await getCoils(params)
await this.selectTask(this.selectedTask) this.coils = res.data.items || []
} finally { this.saving = false } } catch (e) { this.coils = [] }
}, },
async selectCoil(c) {
// ── 缺陷 ────────────────────────────────────── this.selectedCoil = c
async fetchDefects() { await this.loadDefects()
const params = {} },
if (this.defectQuery.coil_no) params.coil_no = this.defectQuery.coil_no async reloadCoil() {
if (this.defectQuery.defect_type) params.defect_type = this.defectQuery.defect_type if (!this.selectedCoil) return
if (this.defectQuery.degree) params.degree = this.defectQuery.degree
if (this.defectQuery.start_date) params.start_date = this.defectQuery.start_date
if (this.defectQuery.end_date) params.end_date = this.defectQuery.end_date
params.page = this.defectQuery.page
params.page_size = this.defectQuery.page_size
try { try {
const res = await getQcDefects(params) const res = await getCoil(this.selectedCoil.coil_no)
this.defectData = res.data.items this.selectedCoil = res.data
this.defectTotal = res.data.total
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, },
openDefectDialog(row = null) { async loadDefects() {
this.editDefect = row if (!this.selectedCoil) return
this.defectForm = row ? { ...row } : {} try {
this.defectDialogVisible = true const res = await getQcDefectsByCoil(this.selectedCoil.coil_no)
this.defects = (res.data || []).map(d => ({
defect_desc: d.defect_desc || '',
start_position: d.start_position || 0,
end_position: d.end_position || 0,
upper_surface: !!d.upper_surface,
lower_surface: !!d.lower_surface,
side_op: !!d.side_op,
side_middle: !!d.side_middle,
side_drive: !!d.side_drive,
defect_code: d.defect_code || '',
degree: d.degree || '',
is_main: !!d.is_main,
image_url: d.image_url || '',
}))
if (!this.defects.length) this.addDefectRow()
} catch (e) { this.defects = [blankDefect()] }
}, },
async saveDefect() { addDefectRow() { this.defects.push(blankDefect()) },
removeRow(i) { this.defects.splice(i, 1) },
clearRow(d) { Object.assign(d, blankDefect()) },
setAll(d, keys) {
const allOn = keys.every(k => d[k])
keys.forEach(k => { d[k] = !allOn })
},
computeLen(d) {
const s = parseFloat(d.start_position); const e = parseFloat(d.end_position)
if (isNaN(s) || isNaN(e)) return 0
const v = +(e - s).toFixed(3)
return v
},
async saveDefects() {
if (!this.selectedCoil) return
this.saving = true this.saving = true
try { try {
if (this.editDefect) { const list = this.defects.map((d, i) => ({
await updateQcDefect(this.editDefect.id, this.defectForm) seq_no: i + 1,
} else { defect_desc: d.defect_desc || null,
await createQcDefect(this.defectForm) start_position: d.start_position != null ? Number(d.start_position) : null,
} end_position: d.end_position != null ? Number(d.end_position) : null,
length_val: this.computeLen(d),
upper_surface: !!d.upper_surface,
lower_surface: !!d.lower_surface,
side_op: !!d.side_op,
side_middle: !!d.side_middle,
side_drive: !!d.side_drive,
defect_code: d.defect_code || null,
degree: d.degree || null,
is_main: !!d.is_main,
image_url: d.image_url || null,
}))
await bulkSaveQcDefects({ coil_no: this.selectedCoil.coil_no, defects: list })
this.$message.success('保存成功') this.$message.success('保存成功')
this.defectDialogVisible = false await this.loadDefects()
this.fetchDefects() } catch (e) {
this.$message.error('保存失败')
} finally { this.saving = false } } finally { this.saving = false }
}, },
async deleteDefect(row) {
if (!confirm('确认删除该缺陷记录?')) return
try {
await deleteQcDefect(row.id)
this.$message.success('已删除')
this.fetchDefects()
} catch (e) { this.$message.error('删除失败') }
},
// ── 工具方法 ────────────────────────────────── // ── 工具 ─────────────────────────────────────
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' }, fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
taskStatusLabel(s) { return TASK_STATUS[s]?.label || s }, taskStatusLabel(s) { return TASK_STATUS[s]?.label || s },
taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' }, taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' },
degreeLabel(d) { return DEGREE_MAP[d]?.label || d }, weightT(kg) { return kg ? (kg / 1000).toFixed(3) : '—' },
degreeBadge(d) { return DEGREE_MAP[d]?.badge || 'badge-gray' },
}, },
} }
</script> </script>
@@ -660,7 +520,6 @@ export default {
.tab-bar { .tab-bar {
display: flex; display: flex;
gap: 0;
margin-bottom: 14px; margin-bottom: 14px;
border-bottom: 2px solid $border; border-bottom: 2px solid $border;
} }
@@ -675,11 +534,56 @@ export default {
&:hover { color: $text-primary; } &:hover { color: $text-primary; }
&.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; } &.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
} }
.section-row { display: flex; gap: 14px; align-items: flex-start; }
/* ─── 异常管理布局 ─── */
.abn-layout { display: flex; gap: 14px; align-items: flex-start; }
.abn-sidebar {
width: 220px; flex-shrink: 0;
background: $bg-card; border: 1px solid $border; border-radius: 6px;
display: flex; flex-direction: column; max-height: calc(100vh - 180px);
}
.sidebar-header {
padding: 10px 12px; font-size: 12px; font-weight: 600; color: $sms-highlight;
border-bottom: 1px solid $border; background: $bg-panel;
display: flex; align-items: center; justify-content: space-between;
}
.sidebar-search { padding: 8px 10px; border-bottom: 1px solid $border; }
.add-btn { cursor: pointer; color: $sms-highlight; font-size: 14px; &:hover { opacity: .7; } }
.cl-list { flex: 1; overflow-y: auto; }
.cl-item { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid rgba($border, .5);
&:hover { background: rgba(255,255,255,.03); }
&.active { background: rgba(0,200,255,.08); border-left: 3px solid $sms-highlight; }
}
.cl-name { font-size: 12px; color: $text-primary; margin-bottom: 3px; }
.cl-meta { display: flex; gap: 6px; }
.cl-empty { padding: 20px; text-align: center; font-size: 12px; color: $text-muted; }
.abn-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.empty-tip { padding: 60px; text-align: center; color: $text-muted; font-size: 13px;
background: $bg-card; border: 1px solid $border; border-radius: 6px; }
/* 钢卷信息网格5列 */
.kv-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px 14px;
}
.kv-cell { display: flex; align-items: center; gap: 6px; font-size: 12px; min-width: 0; }
.kv-cell .kv-label { color: $text-muted; flex-shrink: 0; }
.kv-cell .kv-value { color: $text-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 异常表格 */
.abn-table { table-layout: auto; font-size: 12px; }
.abn-table th, .abn-table td { padding: 6px 6px; vertical-align: middle; }
.abn-table .kv-input { font-size: 12px; padding: 3px 6px; }
.abn-table .ck { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; margin-right: 6px; cursor: pointer; }
.abn-table .rd { display: block; font-size: 11px; line-height: 1.6; cursor: pointer; }
.abn-table .all-link { display: block; font-size: 10px; color: $sms-highlight; cursor: pointer; margin-top: 2px;
&:hover { text-decoration: underline; } }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; } .grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
.row-selected { background: rgba(0,200,255,.08) !important; }
.form-field { display: flex; flex-direction: column; gap: 5px; } .form-field { display: flex; flex-direction: column; gap: 5px; }
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } } .action-link { color: $sms-highlight; cursor: pointer; font-size: 11px; margin-right: 8px; &:hover { text-decoration: underline; } }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; } .modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }