feat: 同步本地未提交的前后端更新(plan/quality/material/inspection/production 等模块)
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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 },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; } } }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
|
||||||
检验项 <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; } } }
|
||||||
|
|||||||
Reference in New Issue
Block a user