feat: 重构质量管理和设备巡检模块
质量管理: 由平铺记录改为任务制工作流(qc_task/qc_task_item/qc_defect三表) 设备巡检: 由点位+记录改为巡检模板制(eqp_checklist/item/record/detail四表) 前端: Quality.vue 支持任务列表+检验项详情+缺陷记录双Tab 前端: Inspection.vue 支持模板管理+项目维护+巡检记录+明细查看 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, func, desc
|
from sqlalchemy import select, func, desc
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.inspection import InspectionLocation, InspectionRecord
|
from app.models.inspection import EqpChecklist, EqpChecklistItem, EqpInspectionRecord, EqpInspectionDetail
|
||||||
from app.schemas.inspection import (
|
from app.schemas.inspection import (
|
||||||
InspectionLocationCreate, InspectionLocationOut,
|
EqpChecklistCreate, EqpChecklistUpdate, EqpChecklistOut,
|
||||||
InspectionRecordCreate, InspectionRecordOut,
|
EqpChecklistItemCreate, EqpChecklistItemOut,
|
||||||
|
EqpInspectionRecordCreate, EqpInspectionRecordOut,
|
||||||
|
EqpInspectionDetailOut,
|
||||||
)
|
)
|
||||||
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
|
||||||
@@ -15,61 +18,117 @@ from app.services.auth_service import get_current_user
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/locations", response_model=Response[list[InspectionLocationOut]])
|
# ─── 巡检模板 ────────────────────────────────────────────────────────────────
|
||||||
async def list_locations(
|
|
||||||
|
@router.get("/checklists", response_model=Response[list[EqpChecklistOut]])
|
||||||
|
async def list_checklists(
|
||||||
|
equipment_code: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_ = Depends(get_current_user),
|
_=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
result = await db.execute(
|
query = select(EqpChecklist).order_by(EqpChecklist.id)
|
||||||
select(InspectionLocation).order_by(InspectionLocation.sort_order, InspectionLocation.id)
|
if equipment_code:
|
||||||
)
|
query = query.where(EqpChecklist.equipment_code == equipment_code)
|
||||||
items = [InspectionLocationOut.model_validate(r) for r in result.scalars()]
|
r = await db.execute(query)
|
||||||
return Response.ok(items)
|
return Response.ok([EqpChecklistOut.model_validate(c) for c in r.scalars()])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/locations", response_model=Response[InspectionLocationOut])
|
@router.post("/checklists", response_model=Response[EqpChecklistOut])
|
||||||
async def create_location(
|
async def create_checklist(body: EqpChecklistCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
body: InspectionLocationCreate,
|
items_data = body.items or []
|
||||||
db: AsyncSession = Depends(get_db),
|
data = body.model_dump(exclude={"items"})
|
||||||
_ = Depends(get_current_user),
|
checklist = EqpChecklist(**data)
|
||||||
):
|
db.add(checklist)
|
||||||
loc = InspectionLocation(**body.model_dump())
|
|
||||||
db.add(loc)
|
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return Response.ok(InspectionLocationOut.model_validate(loc))
|
for item in items_data:
|
||||||
|
ci = EqpChecklistItem(checklist_id=checklist.id, **item.model_dump())
|
||||||
|
db.add(ci)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(EqpChecklistOut.model_validate(checklist))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/records", response_model=Response[PageResponse[InspectionRecordOut]])
|
@router.put("/checklists/{checklist_id}", response_model=Response[EqpChecklistOut])
|
||||||
|
async def update_checklist(checklist_id: int, body: EqpChecklistUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(EqpChecklist).where(EqpChecklist.id == checklist_id))
|
||||||
|
checklist = r.scalar_one_or_none()
|
||||||
|
if not checklist:
|
||||||
|
raise HTTPException(status_code=404, detail="模板不存在")
|
||||||
|
for k, v in body.model_dump(exclude_none=True).items():
|
||||||
|
setattr(checklist, k, v)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(EqpChecklistOut.model_validate(checklist))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/checklists/{checklist_id}/items", response_model=Response[list[EqpChecklistItemOut]])
|
||||||
|
async def list_checklist_items(checklist_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(
|
||||||
|
select(EqpChecklistItem).where(EqpChecklistItem.checklist_id == checklist_id)
|
||||||
|
.order_by(EqpChecklistItem.sort_order, EqpChecklistItem.id)
|
||||||
|
)
|
||||||
|
return Response.ok([EqpChecklistItemOut.model_validate(i) for i in r.scalars()])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/checklist-items", response_model=Response[EqpChecklistItemOut])
|
||||||
|
async def create_checklist_item(
|
||||||
|
body: EqpChecklistItemCreate,
|
||||||
|
checklist_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
item = EqpChecklistItem(checklist_id=checklist_id, **body.model_dump())
|
||||||
|
db.add(item)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(EqpChecklistItemOut.model_validate(item))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 巡检记录 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/records", response_model=Response[PageResponse[EqpInspectionRecordOut]])
|
||||||
async def list_records(
|
async def list_records(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 30,
|
page_size: int = 20,
|
||||||
location_id: Optional[int] = None,
|
checklist_id: Optional[int] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
start_date: Optional[str] = None,
|
||||||
|
end_date: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_ = Depends(get_current_user),
|
_=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
query = select(InspectionRecord).order_by(desc(InspectionRecord.created_at))
|
query = select(EqpInspectionRecord).order_by(desc(EqpInspectionRecord.created_at))
|
||||||
if location_id:
|
if checklist_id:
|
||||||
query = query.where(InspectionRecord.location_id == location_id)
|
query = query.where(EqpInspectionRecord.checklist_id == checklist_id)
|
||||||
|
if status:
|
||||||
|
query = query.where(EqpInspectionRecord.status == status)
|
||||||
|
if start_date:
|
||||||
|
query = query.where(EqpInspectionRecord.inspect_time >= datetime.fromisoformat(start_date))
|
||||||
|
if end_date:
|
||||||
|
query = query.where(EqpInspectionRecord.inspect_time <= datetime.fromisoformat(end_date + "T23:59:59"))
|
||||||
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
|
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
|
||||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||||
items = [InspectionRecordOut.model_validate(r) for r in result.scalars()]
|
items = [EqpInspectionRecordOut.model_validate(r) for r in rows.scalars()]
|
||||||
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
|
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/records", response_model=Response[InspectionRecordOut])
|
@router.post("/records", response_model=Response[EqpInspectionRecordOut])
|
||||||
async def create_record(
|
async def create_record(body: EqpInspectionRecordCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
body: InspectionRecordCreate,
|
details_data = body.details or []
|
||||||
db: AsyncSession = Depends(get_db),
|
# get checklist name
|
||||||
_ = Depends(get_current_user),
|
cl_r = await db.execute(select(EqpChecklist).where(EqpChecklist.id == body.checklist_id))
|
||||||
):
|
cl = cl_r.scalar_one_or_none()
|
||||||
loc_result = await db.execute(
|
data = body.model_dump(exclude={"details"})
|
||||||
select(InspectionLocation).where(InspectionLocation.id == body.location_id)
|
record = EqpInspectionRecord(**data, checklist_name=cl.name if cl else None)
|
||||||
)
|
|
||||||
loc = loc_result.scalar_one_or_none()
|
|
||||||
record = InspectionRecord(
|
|
||||||
**body.model_dump(),
|
|
||||||
location_name=loc.name if loc else None,
|
|
||||||
)
|
|
||||||
db.add(record)
|
db.add(record)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return Response.ok(InspectionRecordOut.model_validate(record))
|
for d in details_data:
|
||||||
|
detail = EqpInspectionDetail(record_id=record.id, **d.model_dump())
|
||||||
|
db.add(detail)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(EqpInspectionRecordOut.model_validate(record))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/records/{record_id}/details", response_model=Response[list[EqpInspectionDetailOut]])
|
||||||
|
async def get_record_details(record_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(
|
||||||
|
select(EqpInspectionDetail).where(EqpInspectionDetail.record_id == record_id).order_by(EqpInspectionDetail.id)
|
||||||
|
)
|
||||||
|
return Response.ok([EqpInspectionDetailOut.model_validate(d) for d in r.scalars()])
|
||||||
|
|||||||
@@ -5,128 +5,177 @@ 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.quality import QualityRecord
|
from app.models.quality import QcTask, QcTaskItem, QcDefect
|
||||||
from app.schemas.quality import QualityRecordCreate, QualityRecordUpdate, QualityRecordOut
|
from app.schemas.quality import (
|
||||||
|
QcTaskCreate, QcTaskUpdate, QcTaskOut,
|
||||||
|
QcTaskItemCreate, QcTaskItemUpdate, QcTaskItemOut,
|
||||||
|
QcDefectCreate, QcDefectUpdate, QcDefectOut,
|
||||||
|
)
|
||||||
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
|
||||||
from app.services.prediction import QualityPredictionModel
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/summary", response_model=Response[dict])
|
# ─── 检验任务 ───────────────────────────────────────────────────────────────
|
||||||
async def quality_summary(
|
|
||||||
|
@router.get("/tasks", response_model=Response[PageResponse[QcTaskOut]])
|
||||||
|
async def list_tasks(
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
task_code: Optional[str] = None,
|
||||||
|
coil_no: Optional[str] = None,
|
||||||
|
status: Optional[int] = None,
|
||||||
|
result: Optional[str] = None,
|
||||||
|
start_date: Optional[str] = None,
|
||||||
|
end_date: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_=Depends(get_current_user),
|
_=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""合格率、平均评分、班次分组统计"""
|
query = select(QcTask).where(QcTask.del_flag == 0).order_by(desc(QcTask.created_at))
|
||||||
total_q = await db.execute(select(func.count()).select_from(QualityRecord))
|
if task_code:
|
||||||
passed_q = await db.execute(
|
query = query.where(QcTask.task_code.ilike(f"%{task_code}%"))
|
||||||
select(func.count()).select_from(QualityRecord).where(QualityRecord.is_passed == True)
|
|
||||||
)
|
|
||||||
avg_pi_q = await db.execute(select(func.avg(QualityRecord.pi_score)).select_from(QualityRecord))
|
|
||||||
avg_suf_q = await db.execute(select(func.avg(QualityRecord.surface_score)).select_from(QualityRecord))
|
|
||||||
|
|
||||||
# Grade distribution
|
|
||||||
grade_counts: dict = {}
|
|
||||||
for grade in ["A1", "A2", "B1", "B2", "C"]:
|
|
||||||
cnt_q = await db.execute(
|
|
||||||
select(func.count()).select_from(QualityRecord).where(QualityRecord.overall_grade == grade)
|
|
||||||
)
|
|
||||||
grade_counts[grade] = cnt_q.scalar() or 0
|
|
||||||
|
|
||||||
total = total_q.scalar() or 0
|
|
||||||
passed = passed_q.scalar() or 0
|
|
||||||
pass_rate = round(passed / total * 100, 1) if total > 0 else 0.0
|
|
||||||
|
|
||||||
return Response.ok({
|
|
||||||
"total": total,
|
|
||||||
"passed": passed,
|
|
||||||
"pass_rate": pass_rate,
|
|
||||||
"avg_pi_score": round(avg_pi_q.scalar() or 0, 1),
|
|
||||||
"avg_surface_score": round(avg_suf_q.scalar() or 0, 1),
|
|
||||||
"grade_distribution": grade_counts,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Response[PageResponse[QualityRecordOut]])
|
|
||||||
async def list_quality(
|
|
||||||
page: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
coil_no: Optional[str] = None,
|
|
||||||
overall_grade: Optional[str] = None,
|
|
||||||
start_date: Optional[datetime] = None,
|
|
||||||
end_date: Optional[datetime] = None,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
_=Depends(get_current_user),
|
|
||||||
):
|
|
||||||
query = select(QualityRecord).order_by(desc(QualityRecord.created_at))
|
|
||||||
if coil_no:
|
if coil_no:
|
||||||
query = query.where(QualityRecord.coil_no.ilike(f"%{coil_no}%"))
|
query = query.where(QcTask.coil_no.ilike(f"%{coil_no}%"))
|
||||||
if overall_grade:
|
if status is not None:
|
||||||
query = query.where(QualityRecord.overall_grade == overall_grade)
|
query = query.where(QcTask.status == status)
|
||||||
|
if result:
|
||||||
|
query = query.where(QcTask.result == result)
|
||||||
if start_date:
|
if start_date:
|
||||||
query = query.where(QualityRecord.created_at >= start_date)
|
query = query.where(QcTask.created_at >= datetime.fromisoformat(start_date))
|
||||||
if end_date:
|
if end_date:
|
||||||
query = query.where(QualityRecord.created_at <= end_date)
|
query = query.where(QcTask.created_at <= datetime.fromisoformat(end_date + "T23:59:59"))
|
||||||
|
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
|
||||||
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
|
result_rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
items = [QcTaskOut.model_validate(r) for r in result_rows.scalars()]
|
||||||
items = [QualityRecordOut.model_validate(r) for r in result.scalars()]
|
|
||||||
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
|
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Response[QualityRecordOut])
|
@router.post("/tasks", response_model=Response[QcTaskOut])
|
||||||
async def create_quality(
|
async def create_task(body: QcTaskCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
body: QualityRecordCreate,
|
task = QcTask(**body.model_dump())
|
||||||
db: AsyncSession = Depends(get_db),
|
db.add(task)
|
||||||
_=Depends(get_current_user),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建质量记录。若未提供 pi_score / surface_score / overall_grade,
|
|
||||||
则尝试用 QualityPredictionModel 自动填充(需要thickness_actual和相关参数)。
|
|
||||||
"""
|
|
||||||
data = body.model_dump()
|
|
||||||
|
|
||||||
if (
|
|
||||||
data.get("pi_score") is None
|
|
||||||
and data.get("thickness_actual") is not None
|
|
||||||
):
|
|
||||||
# Use default avg values for auto-prediction when detailed params are absent
|
|
||||||
avg_speed = 100.0 # m/min default
|
|
||||||
acid_conc_avg = 160.0 # g/L default
|
|
||||||
acid_temp_avg = 75.0 # °C default
|
|
||||||
try:
|
|
||||||
pred = QualityPredictionModel(
|
|
||||||
thickness=data["thickness_actual"],
|
|
||||||
avg_speed=avg_speed,
|
|
||||||
acid_conc_avg=acid_conc_avg,
|
|
||||||
acid_temp_avg=acid_temp_avg,
|
|
||||||
).calculate()
|
|
||||||
data["pi_score"] = pred["pi_score"]
|
|
||||||
data["surface_score"] = pred["surface_score"]
|
|
||||||
data["overall_grade"] = pred["overall_grade"]
|
|
||||||
except Exception:
|
|
||||||
pass # best-effort, do not block creation
|
|
||||||
|
|
||||||
record = QualityRecord(**data)
|
|
||||||
db.add(record)
|
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return Response.ok(QualityRecordOut.model_validate(record))
|
return Response.ok(QcTaskOut.model_validate(task))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{quality_id}", response_model=Response[QualityRecordOut])
|
@router.get("/tasks/{task_id}", response_model=Response[QcTaskOut])
|
||||||
async def update_quality(
|
async def get_task(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
quality_id: int,
|
r = await db.execute(select(QcTask).where(QcTask.id == task_id, QcTask.del_flag == 0))
|
||||||
body: QualityRecordUpdate,
|
task = r.scalar_one_or_none()
|
||||||
db: AsyncSession = Depends(get_db),
|
if not task:
|
||||||
_=Depends(get_current_user),
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
):
|
return Response.ok(QcTaskOut.model_validate(task))
|
||||||
result = await db.execute(select(QualityRecord).where(QualityRecord.id == quality_id))
|
|
||||||
record = result.scalar_one_or_none()
|
|
||||||
if not record:
|
@router.put("/tasks/{task_id}", response_model=Response[QcTaskOut])
|
||||||
raise HTTPException(status_code=404, detail="质量记录不存在")
|
async def update_task(task_id: int, body: QcTaskUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(QcTask).where(QcTask.id == task_id, QcTask.del_flag == 0))
|
||||||
|
task = r.scalar_one_or_none()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
for k, v in body.model_dump(exclude_none=True).items():
|
for k, v in body.model_dump(exclude_none=True).items():
|
||||||
setattr(record, k, v)
|
setattr(task, k, v)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return Response.ok(QualityRecordOut.model_validate(record))
|
return Response.ok(QcTaskOut.model_validate(task))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/tasks/{task_id}", response_model=Response[dict])
|
||||||
|
async def delete_task(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(QcTask).where(QcTask.id == task_id))
|
||||||
|
task = r.scalar_one_or_none()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
|
task.del_flag = 1
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok({})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 检验项目 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/tasks/{task_id}/items", response_model=Response[list[QcTaskItemOut]])
|
||||||
|
async def list_task_items(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(QcTaskItem).where(QcTaskItem.task_id == task_id).order_by(QcTaskItem.id))
|
||||||
|
return Response.ok([QcTaskItemOut.model_validate(i) for i in r.scalars()])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/task-items", response_model=Response[QcTaskItemOut])
|
||||||
|
async def create_task_item(body: QcTaskItemCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
item = QcTaskItem(**body.model_dump())
|
||||||
|
db.add(item)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(QcTaskItemOut.model_validate(item))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/task-items/{item_id}", response_model=Response[QcTaskItemOut])
|
||||||
|
async def update_task_item(item_id: int, body: QcTaskItemUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(QcTaskItem).where(QcTaskItem.id == item_id))
|
||||||
|
item = r.scalar_one_or_none()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="检验项不存在")
|
||||||
|
for k, v in body.model_dump(exclude_none=True).items():
|
||||||
|
setattr(item, k, v)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(QcTaskItemOut.model_validate(item))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 缺陷记录 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/defects", response_model=Response[PageResponse[QcDefectOut]])
|
||||||
|
async def list_defects(
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
coil_no: Optional[str] = None,
|
||||||
|
defect_type: Optional[str] = None,
|
||||||
|
degree: Optional[str] = None,
|
||||||
|
start_date: Optional[str] = None,
|
||||||
|
end_date: Optional[str] = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
query = select(QcDefect).where(QcDefect.del_flag == 0).order_by(desc(QcDefect.created_at))
|
||||||
|
if coil_no:
|
||||||
|
query = query.where(QcDefect.coil_no.ilike(f"%{coil_no}%"))
|
||||||
|
if defect_type:
|
||||||
|
query = query.where(QcDefect.defect_type.ilike(f"%{defect_type}%"))
|
||||||
|
if degree:
|
||||||
|
query = query.where(QcDefect.degree == degree)
|
||||||
|
if start_date:
|
||||||
|
query = query.where(QcDefect.created_at >= datetime.fromisoformat(start_date))
|
||||||
|
if end_date:
|
||||||
|
query = query.where(QcDefect.created_at <= datetime.fromisoformat(end_date + "T23:59:59"))
|
||||||
|
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
|
||||||
|
rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||||
|
items = [QcDefectOut.model_validate(r) for r in rows.scalars()]
|
||||||
|
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/defects", response_model=Response[QcDefectOut])
|
||||||
|
async def create_defect(body: QcDefectCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
defect = QcDefect(**body.model_dump())
|
||||||
|
db.add(defect)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(QcDefectOut.model_validate(defect))
|
||||||
|
|
||||||
|
|
||||||
|
@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)):
|
||||||
|
r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id, QcDefect.del_flag == 0))
|
||||||
|
defect = r.scalar_one_or_none()
|
||||||
|
if not defect:
|
||||||
|
raise HTTPException(status_code=404, detail="缺陷记录不存在")
|
||||||
|
for k, v in body.model_dump(exclude_none=True).items():
|
||||||
|
setattr(defect, k, v)
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok(QcDefectOut.model_validate(defect))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/defects/{defect_id}", response_model=Response[dict])
|
||||||
|
async def delete_defect(defect_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
|
||||||
|
r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id))
|
||||||
|
defect = r.scalar_one_or_none()
|
||||||
|
if not defect:
|
||||||
|
raise HTTPException(status_code=404, detail="缺陷记录不存在")
|
||||||
|
defect.del_flag = 1
|
||||||
|
await db.flush()
|
||||||
|
return Response.ok({})
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from app.models.downtime import DowntimeRecord, DowntimeCategory
|
|||||||
from app.models.equipment import Equipment, EquipmentMaintenance
|
from app.models.equipment import Equipment, EquipmentMaintenance
|
||||||
from app.models.message import MessageLog
|
from app.models.message import MessageLog
|
||||||
from app.models.pdi import PDIRecord
|
from app.models.pdi import PDIRecord
|
||||||
from app.models.quality import QualityRecord
|
from app.models.quality import QcTask, QcTaskItem, QcDefect
|
||||||
from app.models.energy import EnergyRecord
|
from app.models.energy import EnergyRecord
|
||||||
from app.models.inspection import InspectionLocation, InspectionRecord
|
from app.models.inspection import EqpChecklist, EqpChecklistItem, EqpInspectionRecord, EqpInspectionDetail
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"User",
|
"User",
|
||||||
@@ -19,7 +19,7 @@ __all__ = [
|
|||||||
"Equipment", "EquipmentMaintenance",
|
"Equipment", "EquipmentMaintenance",
|
||||||
"MessageLog",
|
"MessageLog",
|
||||||
"PDIRecord",
|
"PDIRecord",
|
||||||
"QualityRecord",
|
"QcTask", "QcTaskItem", "QcDefect",
|
||||||
"EnergyRecord",
|
"EnergyRecord",
|
||||||
"InspectionLocation", "InspectionRecord",
|
"EqpChecklist", "EqpChecklistItem", "EqpInspectionRecord", "EqpInspectionDetail",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, func
|
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, ForeignKey, func
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
class InspectionLocation(Base):
|
class EqpChecklist(Base):
|
||||||
__tablename__ = "inspection_locations"
|
"""设备巡检模板"""
|
||||||
|
__tablename__ = "eqp_checklist"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
code = Column(String(30), unique=True, nullable=False, index=True)
|
|
||||||
name = Column(String(100), nullable=False)
|
name = Column(String(100), nullable=False)
|
||||||
description = Column(Text)
|
description = Column(Text, nullable=True)
|
||||||
|
equipment_code = Column(String(30), nullable=True, index=True)
|
||||||
|
equipment_name = Column(String(100), nullable=True)
|
||||||
|
period = Column(String(20), default="daily") # daily/weekly/monthly
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class EqpChecklistItem(Base):
|
||||||
|
"""巡检模板项目"""
|
||||||
|
__tablename__ = "eqp_checklist_item"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
checklist_id = Column(Integer, ForeignKey("eqp_checklist.id"), nullable=False, index=True)
|
||||||
|
item_name = Column(String(100), nullable=False)
|
||||||
|
item_standard = Column(String(200), nullable=True)
|
||||||
sort_order = Column(Integer, default=0)
|
sort_order = Column(Integer, default=0)
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
class InspectionRecord(Base):
|
class EqpInspectionRecord(Base):
|
||||||
__tablename__ = "inspection_records"
|
"""巡检记录"""
|
||||||
|
__tablename__ = "eqp_inspection_record"
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
location_id = Column(Integer, ForeignKey("inspection_locations.id"), nullable=False)
|
checklist_id = Column(Integer, ForeignKey("eqp_checklist.id"), nullable=False, index=True)
|
||||||
location_name = Column(String(100))
|
checklist_name = Column(String(100), nullable=True)
|
||||||
equipment_code = Column(String(30), index=True)
|
|
||||||
equipment_name = Column(String(100))
|
|
||||||
scan_code = Column(String(200))
|
|
||||||
inspector = Column(String(50), nullable=False)
|
inspector = Column(String(50), nullable=False)
|
||||||
result = Column(String(20), default="normal")
|
inspect_time = Column(DateTime, nullable=False)
|
||||||
notes = Column(Text)
|
status = Column(String(20), default="ok") # ok/issue/urgent
|
||||||
|
overall_result = Column(String(20), nullable=True) # pass/fail
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
class EqpInspectionDetail(Base):
|
||||||
|
"""巡检记录明细"""
|
||||||
|
__tablename__ = "eqp_inspection_detail"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
record_id = Column(Integer, ForeignKey("eqp_inspection_record.id"), nullable=False, index=True)
|
||||||
|
checklist_item_id = Column(Integer, nullable=True)
|
||||||
|
item_name = Column(String(100), nullable=False)
|
||||||
|
actual_value = Column(String(100), nullable=True)
|
||||||
|
is_ok = Column(Boolean, default=True)
|
||||||
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|||||||
@@ -2,37 +2,64 @@ from sqlalchemy import Column, Integer, String, Float, DateTime, Text, Boolean,
|
|||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
class QualityRecord(Base):
|
class QcTask(Base):
|
||||||
"""质量检验记录"""
|
"""检验任务"""
|
||||||
__tablename__ = "quality_records"
|
__tablename__ = "qc_task"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
task_code = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
|
coil_no = Column(String(30), nullable=True, index=True)
|
||||||
|
task_type = Column(String(30), nullable=True) # e.g. incoming/process/final
|
||||||
|
scheme_name = Column(String(100), nullable=True)
|
||||||
|
status = Column(Integer, default=0) # 0=待检验 1=检验中 2=待审核 3=完成
|
||||||
|
inspect_user = Column(String(50), nullable=True)
|
||||||
|
inspect_time = Column(DateTime, nullable=True)
|
||||||
|
audit_user = Column(String(50), nullable=True)
|
||||||
|
audit_time = Column(DateTime, nullable=True)
|
||||||
|
result = Column(String(20), nullable=True) # qualified/unqualified
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
del_flag = Column(Integer, default=0)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
coil_no = Column(String(30), nullable=False, index=True, comment="卷号")
|
|
||||||
production_record_id = Column(Integer, ForeignKey("production_records.id"), nullable=True, comment="关联生产实绩")
|
|
||||||
|
|
||||||
# 实测规格
|
class QcTaskItem(Base):
|
||||||
thickness_actual = Column(Float, nullable=True, comment="实测厚度 mm")
|
"""检验项目"""
|
||||||
width_actual = Column(Float, nullable=True, comment="实测宽度 mm")
|
__tablename__ = "qc_task_item"
|
||||||
flatness = Column(Float, nullable=True, comment="平直度 IU")
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
crown = Column(Float, nullable=True, comment="凸度 μm")
|
task_id = Column(Integer, ForeignKey("qc_task.id"), nullable=False, index=True)
|
||||||
|
item_name = Column(String(100), nullable=False)
|
||||||
|
item_type = Column(String(20), nullable=True) # quantitative/qualitative
|
||||||
|
standard_value = Column(Float, nullable=True)
|
||||||
|
upper_limit = Column(Float, nullable=True)
|
||||||
|
lower_limit = Column(Float, nullable=True)
|
||||||
|
unit = Column(String(20), nullable=True)
|
||||||
|
inspect_value = Column(String(50), nullable=True)
|
||||||
|
is_qualified = Column(Integer, nullable=True) # 1=qualified 0=unqualified
|
||||||
|
judge_result = Column(String(100), nullable=True)
|
||||||
|
inspect_user = Column(String(50), nullable=True)
|
||||||
|
inspect_time = Column(DateTime, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
# 表面缺陷
|
|
||||||
surface_defect_type = Column(String(50), nullable=True, comment="表面缺陷类型")
|
|
||||||
defect_length_m = Column(Float, nullable=True, comment="缺陷长度 m")
|
|
||||||
defect_position = Column(String(50), nullable=True, comment="缺陷位置")
|
|
||||||
|
|
||||||
# 质量模型评分
|
class QcDefect(Base):
|
||||||
pi_score = Column(Float, nullable=True, comment="酸洗指数评分 0-100")
|
"""钢卷缺陷记录"""
|
||||||
surface_score = Column(Float, nullable=True, comment="表面质量评分 0-100")
|
__tablename__ = "qc_defect"
|
||||||
overall_grade = Column(String(5), nullable=True, comment="综合等级 A1/A2/B1/B2/C")
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
coil_no = Column(String(30), nullable=True, index=True)
|
||||||
# 残酸 / 粗糙度
|
production_line = Column(String(50), nullable=True)
|
||||||
acid_residual = Column(Float, nullable=True, comment="残酸量 g/m²")
|
position = Column(String(50), nullable=True)
|
||||||
roughness_ra = Column(Float, nullable=True, comment="粗糙度 Ra μm")
|
plate_surface = Column(String(20), nullable=True)
|
||||||
|
defect_code = Column(String(30), nullable=True)
|
||||||
# 检验信息
|
defect_type = Column(String(50), nullable=True, index=True)
|
||||||
inspector = Column(String(50), nullable=True, comment="检验员")
|
defect_rate = Column(Float, nullable=True)
|
||||||
inspect_time = Column(DateTime, nullable=True, comment="检验时间")
|
defect_weight = Column(Float, nullable=True)
|
||||||
is_passed = Column(Boolean, default=True, comment="是否合格")
|
degree = Column(String(20), nullable=True) # light/normal/serious
|
||||||
|
judge_level = Column(String(20), nullable=True)
|
||||||
created_at = Column(DateTime, server_default=func.now())
|
judge_by = Column(String(50), nullable=True)
|
||||||
|
judge_time = Column(DateTime, nullable=True)
|
||||||
|
main_mark = Column(Integer, nullable=True)
|
||||||
|
whole_coil_mark = Column(Integer, nullable=True)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
del_flag = Column(Integer, default=0)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
|||||||
@@ -1,37 +1,88 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class InspectionLocationCreate(BaseModel):
|
class EqpChecklistItemCreate(BaseModel):
|
||||||
code: str
|
item_name: str
|
||||||
name: str
|
item_standard: Optional[str] = None
|
||||||
description: Optional[str] = None
|
|
||||||
sort_order: int = 0
|
sort_order: int = 0
|
||||||
|
|
||||||
|
|
||||||
class InspectionLocationOut(InspectionLocationCreate):
|
class EqpChecklistItemOut(EqpChecklistItemCreate):
|
||||||
id: int
|
id: int
|
||||||
|
checklist_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class InspectionRecordCreate(BaseModel):
|
class EqpChecklistCreate(BaseModel):
|
||||||
location_id: int
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
equipment_code: Optional[str] = None
|
equipment_code: Optional[str] = None
|
||||||
equipment_name: Optional[str] = None
|
equipment_name: Optional[str] = None
|
||||||
scan_code: Optional[str] = None
|
period: str = "daily"
|
||||||
inspector: str
|
items: Optional[List[EqpChecklistItemCreate]] = None
|
||||||
result: str = "normal"
|
|
||||||
|
|
||||||
|
class EqpChecklistUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
equipment_code: Optional[str] = None
|
||||||
|
equipment_name: Optional[str] = None
|
||||||
|
period: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EqpChecklistOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: Optional[str]
|
||||||
|
equipment_code: Optional[str]
|
||||||
|
equipment_name: Optional[str]
|
||||||
|
period: str
|
||||||
|
is_active: bool
|
||||||
|
created_at: datetime
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class EqpInspectionDetailCreate(BaseModel):
|
||||||
|
checklist_item_id: Optional[int] = None
|
||||||
|
item_name: str
|
||||||
|
actual_value: Optional[str] = None
|
||||||
|
is_ok: bool = True
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class InspectionRecordOut(InspectionRecordCreate):
|
class EqpInspectionDetailOut(EqpInspectionDetailCreate):
|
||||||
id: int
|
id: int
|
||||||
location_name: Optional[str] = None
|
record_id: int
|
||||||
|
created_at: datetime
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class EqpInspectionRecordCreate(BaseModel):
|
||||||
|
checklist_id: int
|
||||||
|
inspector: str
|
||||||
|
inspect_time: datetime
|
||||||
|
status: str = "ok"
|
||||||
|
overall_result: Optional[str] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
details: Optional[List[EqpInspectionDetailCreate]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EqpInspectionRecordOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
checklist_id: int
|
||||||
|
checklist_name: Optional[str]
|
||||||
|
inspector: str
|
||||||
|
inspect_time: datetime
|
||||||
|
status: str
|
||||||
|
overall_result: Optional[str]
|
||||||
|
remark: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -1,66 +1,136 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class QualityRecordCreate(BaseModel):
|
class QcTaskCreate(BaseModel):
|
||||||
coil_no: str
|
task_code: str
|
||||||
production_record_id: Optional[int] = None
|
coil_no: Optional[str] = None
|
||||||
thickness_actual: Optional[float] = None
|
task_type: Optional[str] = None
|
||||||
width_actual: Optional[float] = None
|
scheme_name: Optional[str] = None
|
||||||
flatness: Optional[float] = None
|
inspect_user: Optional[str] = None
|
||||||
crown: Optional[float] = None
|
remark: Optional[str] = None
|
||||||
surface_defect_type: Optional[str] = None
|
|
||||||
defect_length_m: Optional[float] = None
|
|
||||||
defect_position: Optional[str] = None
|
|
||||||
pi_score: Optional[float] = None
|
|
||||||
surface_score: Optional[float] = None
|
|
||||||
overall_grade: Optional[str] = None
|
|
||||||
acid_residual: Optional[float] = None
|
|
||||||
roughness_ra: Optional[float] = None
|
|
||||||
inspector: Optional[str] = None
|
|
||||||
inspect_time: Optional[datetime] = None
|
|
||||||
is_passed: Optional[bool] = True
|
|
||||||
|
|
||||||
|
|
||||||
class QualityRecordUpdate(BaseModel):
|
class QcTaskUpdate(BaseModel):
|
||||||
thickness_actual: Optional[float] = None
|
coil_no: Optional[str] = None
|
||||||
width_actual: Optional[float] = None
|
task_type: Optional[str] = None
|
||||||
flatness: Optional[float] = None
|
scheme_name: Optional[str] = None
|
||||||
crown: Optional[float] = None
|
status: Optional[int] = None
|
||||||
surface_defect_type: Optional[str] = None
|
inspect_user: Optional[str] = None
|
||||||
defect_length_m: Optional[float] = None
|
inspect_time: Optional[datetime] = None
|
||||||
defect_position: Optional[str] = None
|
audit_user: Optional[str] = None
|
||||||
pi_score: Optional[float] = None
|
audit_time: Optional[datetime] = None
|
||||||
surface_score: Optional[float] = None
|
result: Optional[str] = None
|
||||||
overall_grade: Optional[str] = None
|
remark: Optional[str] = None
|
||||||
acid_residual: Optional[float] = None
|
|
||||||
roughness_ra: Optional[float] = None
|
|
||||||
inspector: Optional[str] = None
|
|
||||||
inspect_time: Optional[datetime] = None
|
|
||||||
is_passed: Optional[bool] = None
|
|
||||||
|
|
||||||
|
|
||||||
class QualityRecordOut(BaseModel):
|
class QcTaskItemOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
coil_no: str
|
task_id: int
|
||||||
production_record_id: Optional[int]
|
item_name: str
|
||||||
thickness_actual: Optional[float]
|
item_type: Optional[str]
|
||||||
width_actual: Optional[float]
|
standard_value: Optional[float]
|
||||||
flatness: Optional[float]
|
upper_limit: Optional[float]
|
||||||
crown: Optional[float]
|
lower_limit: Optional[float]
|
||||||
surface_defect_type: Optional[str]
|
unit: Optional[str]
|
||||||
defect_length_m: Optional[float]
|
inspect_value: Optional[str]
|
||||||
defect_position: Optional[str]
|
is_qualified: Optional[int]
|
||||||
pi_score: Optional[float]
|
judge_result: Optional[str]
|
||||||
surface_score: Optional[float]
|
inspect_user: Optional[str]
|
||||||
overall_grade: Optional[str]
|
inspect_time: Optional[datetime]
|
||||||
acid_residual: Optional[float]
|
created_at: datetime
|
||||||
roughness_ra: Optional[float]
|
class Config:
|
||||||
inspector: Optional[str]
|
from_attributes = True
|
||||||
inspect_time: Optional[datetime]
|
|
||||||
is_passed: Optional[bool]
|
|
||||||
created_at: datetime
|
class QcTaskItemCreate(BaseModel):
|
||||||
|
task_id: int
|
||||||
|
item_name: str
|
||||||
|
item_type: Optional[str] = None
|
||||||
|
standard_value: Optional[float] = None
|
||||||
|
upper_limit: Optional[float] = None
|
||||||
|
lower_limit: Optional[float] = None
|
||||||
|
unit: Optional[str] = None
|
||||||
|
inspect_value: Optional[str] = None
|
||||||
|
is_qualified: Optional[int] = None
|
||||||
|
judge_result: Optional[str] = None
|
||||||
|
inspect_user: Optional[str] = None
|
||||||
|
inspect_time: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class QcTaskItemUpdate(BaseModel):
|
||||||
|
inspect_value: Optional[str] = None
|
||||||
|
is_qualified: Optional[int] = None
|
||||||
|
judge_result: Optional[str] = None
|
||||||
|
inspect_user: Optional[str] = None
|
||||||
|
inspect_time: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class QcTaskOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
task_code: str
|
||||||
|
coil_no: Optional[str]
|
||||||
|
task_type: Optional[str]
|
||||||
|
scheme_name: Optional[str]
|
||||||
|
status: int
|
||||||
|
inspect_user: Optional[str]
|
||||||
|
inspect_time: Optional[datetime]
|
||||||
|
audit_user: Optional[str]
|
||||||
|
audit_time: Optional[datetime]
|
||||||
|
result: Optional[str]
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class QcDefectCreate(BaseModel):
|
||||||
|
coil_no: Optional[str] = None
|
||||||
|
production_line: Optional[str] = None
|
||||||
|
position: Optional[str] = None
|
||||||
|
plate_surface: Optional[str] = None
|
||||||
|
defect_code: Optional[str] = None
|
||||||
|
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
|
||||||
|
main_mark: Optional[int] = None
|
||||||
|
whole_coil_mark: Optional[int] = 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
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -52,13 +52,24 @@ export const calibrateQuality = data => request.post('/prediction/calibr
|
|||||||
export const resetCalibration = model => request.post(`/prediction/calibration/reset/${model}`)
|
export const resetCalibration = model => request.post(`/prediction/calibration/reset/${model}`)
|
||||||
|
|
||||||
// 设备巡检
|
// 设备巡检
|
||||||
export const getInspectionLocations = () => request.get('/inspection/locations')
|
export const getChecklists = params => request.get('/inspection/checklists', { params })
|
||||||
export const createInspectionLocation = data => request.post('/inspection/locations', data)
|
export const createChecklist = data => request.post('/inspection/checklists', data)
|
||||||
|
export const updateChecklist = (id, data) => request.put(`/inspection/checklists/${id}`, data)
|
||||||
|
export const getChecklistItems = id => request.get(`/inspection/checklists/${id}/items`)
|
||||||
|
export const createChecklistItem = (checklistId, data) => request.post('/inspection/checklist-items', data, { params: { checklist_id: checklistId } })
|
||||||
export const getInspectionRecords = params => request.get('/inspection/records', { params })
|
export const getInspectionRecords = params => request.get('/inspection/records', { params })
|
||||||
export const createInspectionRecord = data => request.post('/inspection/records', data)
|
export const createInspectionRecord = data => request.post('/inspection/records', data)
|
||||||
|
export const getInspectionRecordDetails = id => request.get(`/inspection/records/${id}/details`)
|
||||||
|
|
||||||
// 质量管理
|
// 质量管理
|
||||||
export const getQualityList = params => request.get('/quality/', { params })
|
export const getQcTasks = params => request.get('/quality/tasks', { params })
|
||||||
export const createQuality = data => request.post('/quality/', data)
|
export const createQcTask = data => request.post('/quality/tasks', data)
|
||||||
export const updateQuality = (id, data) => request.put(`/quality/${id}`, data)
|
export const updateQcTask = (id, data) => request.put(`/quality/tasks/${id}`, data)
|
||||||
export const getQualitySummary = () => request.get('/quality/summary')
|
export const deleteQcTask = id => request.delete(`/quality/tasks/${id}`)
|
||||||
|
export const getQcTaskItems = taskId => request.get(`/quality/tasks/${taskId}/items`)
|
||||||
|
export const createQcTaskItem = data => request.post('/quality/task-items', data)
|
||||||
|
export const updateQcTaskItem = (id, data) => request.put(`/quality/task-items/${id}`, data)
|
||||||
|
export const getQcDefects = params => request.get('/quality/defects', { params })
|
||||||
|
export const createQcDefect = data => request.post('/quality/defects', data)
|
||||||
|
export const updateQcDefect = (id, data) => request.put(`/quality/defects/${id}`, data)
|
||||||
|
export const deleteQcDefect = id => request.delete(`/quality/defects/${id}`)
|
||||||
|
|||||||
@@ -1,168 +1,374 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="insp-layout">
|
<div class="insp-layout">
|
||||||
|
<!-- ─── 左侧:巡检模板列表 ─── -->
|
||||||
<div class="insp-sidebar">
|
<div class="insp-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
巡检点位
|
巡检模板
|
||||||
<span class="add-btn" @click="openLocDialog()">+</span>
|
<span class="add-btn" @click="openChecklistDialog()">+</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="loc-list">
|
<div class="cl-list">
|
||||||
<div
|
<div
|
||||||
v-for="loc in locations"
|
v-for="cl in checklists"
|
||||||
:key="loc.id"
|
:key="cl.id"
|
||||||
:class="['loc-item', { active: selectedLoc && selectedLoc.id === loc.id }]"
|
:class="['cl-item', { active: selectedCl && selectedCl.id === cl.id }]"
|
||||||
@click="selectLocation(loc)"
|
@click="selectChecklist(cl)"
|
||||||
>
|
>
|
||||||
<div class="loc-code">{{ loc.code }}</div>
|
<div class="cl-name">{{ cl.name }}</div>
|
||||||
<div class="loc-name">{{ loc.name }}</div>
|
<div class="cl-meta">
|
||||||
|
<span class="td-muted" style="font-size:10px;">{{ cl.equipment_name || cl.equipment_code || '通用' }}</span>
|
||||||
|
<span :class="['badge', periodBadge(cl.period)]" style="font-size:9px;padding:1px 5px;">{{ periodLabel(cl.period) }}</span>
|
||||||
|
<span :class="['badge', cl.is_active ? 'badge-green' : 'badge-gray']" style="font-size:9px;padding:1px 5px;">{{ cl.is_active ? '启用' : '停用' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!locations.length" class="loc-empty">暂无点位</div>
|
<div v-if="!checklists.length" class="cl-empty">暂无模板</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── 右侧主区域 ─── -->
|
||||||
<div class="insp-main">
|
<div class="insp-main">
|
||||||
<div v-if="!selectedLoc" class="empty-tip">请选择点位</div>
|
<div v-if="!selectedCl" class="empty-tip">请选择巡检模板</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<!-- 模板详情卡 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
<span>{{ selectedLoc.name }} <span class="td-muted" style="font-size:11px;">{{ selectedLoc.code }}</span></span>
|
<span>{{ selectedCl.name }}</span>
|
||||||
<button class="btn btn-primary" @click="openRecordDialog(null)">录入巡检</button>
|
<div class="flex-row" style="gap:8px;">
|
||||||
|
<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-primary" style="padding:2px 10px;font-size:11px;" @click="openInspectDialog()">开始巡检</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex-row" style="gap:24px;flex-wrap:wrap;">
|
||||||
|
<div class="flex-row"><span class="kv-label">设备编号</span><span class="kv-value">{{ selectedCl.equipment_code || '—' }}</span></div>
|
||||||
|
<div class="flex-row"><span class="kv-label">设备名称</span><span class="kv-value">{{ selectedCl.equipment_name || '—' }}</span></div>
|
||||||
|
<div class="flex-row"><span class="kv-label">巡检周期</span><span class="kv-value">{{ periodLabel(selectedCl.period) }}</span></div>
|
||||||
|
<div class="flex-row"><span class="kv-label">描述</span><span class="kv-value">{{ selectedCl.description || '—' }}</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 模板项目 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">关联设备</div>
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
|
巡检项目
|
||||||
|
<button class="btn btn-outline" style="padding:2px 10px;font-size:11px;" @click="openItemDialog()">+ 添加项目</button>
|
||||||
|
</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 style="width:50px;">序号</th>
|
||||||
|
<th>项目名称</th>
|
||||||
|
<th>检验标准</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="eq in locationEquipments" :key="eq.id">
|
<tr v-for="item in clItems" :key="item.id">
|
||||||
<td class="td-num">{{ eq.code }}</td>
|
<td class="td-num">{{ item.sort_order }}</td>
|
||||||
<td>{{ eq.name }}</td>
|
<td>{{ item.item_name }}</td>
|
||||||
<td>{{ eq.category || '—' }}</td>
|
<td class="td-muted">{{ item.item_standard || '—' }}</td>
|
||||||
<td><span :class="['badge', eqStatusBadge(eq.status)]">{{ eqStatusLabel(eq.status) }}</span></td>
|
|
||||||
<td><span class="action-link" @click="openRecordDialog(eq)">录入</span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!locationEquipments.length">
|
<tr v-if="!clItems.length">
|
||||||
<td colspan="5" class="td-muted" style="text-align:center;padding:16px;">该点位暂无关联设备</td>
|
<td colspan="3" class="td-muted" style="text-align:center;padding:16px;">暂无巡检项目</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 巡检记录 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
巡检历史
|
巡检记录
|
||||||
<span class="ch-badge">{{ records.length }} 条</span>
|
<span class="ch-badge">共 {{ recordTotal }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-scroll" v-loading="loadingRecords">
|
<!-- 记录过滤 -->
|
||||||
|
<div class="card-body" style="padding:8px 14px;border-bottom:1px solid var(--border, #30363d);">
|
||||||
|
<div class="flex-row" style="gap:10px;flex-wrap:wrap;">
|
||||||
|
<div class="flex-row">
|
||||||
|
<span class="kv-label">状态</span>
|
||||||
|
<select v-model="recordQuery.status" class="kv-input" style="width:90px;">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="ok">正常</option>
|
||||||
|
<option value="issue">有问题</option>
|
||||||
|
<option value="urgent">紧急</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<span class="kv-label">日期</span>
|
||||||
|
<input v-model="recordQuery.start_date" type="date" class="kv-input" style="width:130px;" />
|
||||||
|
<span class="kv-label">~</span>
|
||||||
|
<input v-model="recordQuery.end_date" type="date" class="kv-input" style="width:130px;" />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" @click="fetchRecords">查询</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="r in records" :key="r.id">
|
<tr v-for="r in records" :key="r.id">
|
||||||
<td class="td-muted">{{ fmtTime(r.created_at) }}</td>
|
<td class="td-muted">{{ fmtTime(r.inspect_time) }}</td>
|
||||||
<td>{{ r.equipment_name || r.equipment_code || '—' }}</td>
|
|
||||||
<td class="td-num">{{ r.scan_code || '—' }}</td>
|
|
||||||
<td><span :class="['badge', resultBadge(r.result)]">{{ resultLabel(r.result) }}</span></td>
|
|
||||||
<td>{{ r.inspector }}</td>
|
<td>{{ r.inspector }}</td>
|
||||||
<td>{{ r.notes || '—' }}</td>
|
<td><span :class="['badge', recordStatusBadge(r.status)]">{{ recordStatusLabel(r.status) }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span v-if="r.overall_result" :class="['badge', r.overall_result === 'pass' ? 'badge-green' : 'badge-red']">
|
||||||
|
{{ r.overall_result === 'pass' ? '通过' : '不通过' }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="td-muted">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="td-muted">{{ r.remark || '—' }}</td>
|
||||||
|
<td><span class="action-link" @click="viewDetails(r)">查看明细</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!records.length && !loadingRecords">
|
<tr v-if="!records.length">
|
||||||
<td colspan="6" class="td-muted" style="text-align:center;padding:16px;">暂无巡检记录</td>
|
<td colspan="6" class="td-muted" style="text-align:center;padding:16px;">暂无巡检记录</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body" style="padding:8px 14px;" v-if="recordTotal > recordQuery.page_size">
|
||||||
|
<div class="flex-row">
|
||||||
|
<button class="btn btn-outline" :disabled="recordQuery.page <= 1" @click="recordQuery.page--; fetchRecords()">上一页</button>
|
||||||
|
<span class="kv-label">第 {{ recordQuery.page }} / {{ Math.ceil(recordTotal / recordQuery.page_size) }} 页</span>
|
||||||
|
<button class="btn btn-outline" :disabled="recordQuery.page >= Math.ceil(recordTotal / recordQuery.page_size)" @click="recordQuery.page++; fetchRecords()">下一页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="recordDialogVisible" class="modal-mask" @click.self="recordDialogVisible=false">
|
<!-- ─── 新增模板弹窗 ─── -->
|
||||||
<div class="modal-box" style="width:460px;">
|
<div v-if="clDialogVisible" class="modal-mask" @click.self="clDialogVisible = false">
|
||||||
|
<div class="modal-box" style="width:480px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
录入巡检
|
新增巡检模板
|
||||||
<span class="modal-close" @click="recordDialogVisible=false">✕</span>
|
<span class="modal-close" @click="clDialogVisible = false">✕</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="kv-label">点位</div>
|
<div class="kv-label">模板名称 *</div>
|
||||||
<input class="kv-input" :value="selectedLoc.name" disabled />
|
<input v-model="clForm.name" 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="recordForm.equipment_code" class="kv-input" />
|
<input v-model="clForm.equipment_code" 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="recordForm.equipment_name" class="kv-input" />
|
<input v-model="clForm.equipment_name" 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="recordForm.scan_code" class="kv-input" ref="scanInput" />
|
<select v-model="clForm.period" class="kv-input">
|
||||||
</div>
|
<option value="daily">每日</option>
|
||||||
<div class="form-field">
|
<option value="weekly">每周</option>
|
||||||
<div class="kv-label">巡检结果 *</div>
|
<option value="monthly">每月</option>
|
||||||
<select v-model="recordForm.result" class="kv-input">
|
|
||||||
<option value="normal">正常</option>
|
|
||||||
<option value="abnormal">异常</option>
|
|
||||||
<option value="pending">待处理</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="recordForm.inspector" class="kv-input" />
|
<textarea v-model="clForm.description" class="kv-input" rows="2"></textarea>
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">备注</div>
|
|
||||||
<textarea v-model="recordForm.notes" class="kv-input" rows="3"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-outline" @click="recordDialogVisible=false">取消</button>
|
<button class="btn btn-outline" @click="clDialogVisible = false">取消</button>
|
||||||
<button class="btn btn-primary" :disabled="saving" @click="saveRecord">{{ saving ? '保存中...' : '保存' }}</button>
|
<button class="btn btn-primary" :disabled="saving" @click="saveChecklist">{{ saving ? '保存中...' : '保存' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="locDialogVisible" class="modal-mask" @click.self="locDialogVisible=false">
|
<!-- ─── 编辑模板弹窗 ─── -->
|
||||||
<div class="modal-box" style="width:380px;">
|
<div v-if="editClDialogVisible" class="modal-mask" @click.self="editClDialogVisible = false">
|
||||||
|
<div class="modal-box" style="width:480px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
新增点位
|
编辑模板
|
||||||
<span class="modal-close" @click="locDialogVisible=false">✕</span>
|
<span class="modal-close" @click="editClDialogVisible = false">✕</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="kv-label">点位编号 *</div>
|
<div class="kv-label">模板名称 *</div>
|
||||||
<input v-model="locForm.code" class="kv-input" />
|
<input v-model="editClForm.name" 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="locForm.name" class="kv-input" />
|
<input v-model="editClForm.equipment_code" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">设备名称</div>
|
||||||
|
<input v-model="editClForm.equipment_name" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">巡检周期</div>
|
||||||
|
<select v-model="editClForm.period" class="kv-input">
|
||||||
|
<option value="daily">每日</option>
|
||||||
|
<option value="weekly">每周</option>
|
||||||
|
<option value="monthly">每月</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">状态</div>
|
||||||
|
<select v-model="editClForm.is_active" class="kv-input">
|
||||||
|
<option :value="true">启用</option>
|
||||||
|
<option :value="false">停用</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="kv-label">描述</div>
|
<div class="kv-label">描述</div>
|
||||||
<input v-model="locForm.description" class="kv-input" />
|
<textarea v-model="editClForm.description" class="kv-input" rows="2"></textarea>
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">排序</div>
|
|
||||||
<input v-model.number="locForm.sort_order" type="number" class="kv-input" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-outline" @click="locDialogVisible=false">取消</button>
|
<button class="btn btn-outline" @click="editClDialogVisible = false">取消</button>
|
||||||
<button class="btn btn-primary" :disabled="saving" @click="saveLocation">{{ saving ? '保存中...' : '保存' }}</button>
|
<button class="btn btn-primary" :disabled="saving" @click="saveEditChecklist">{{ saving ? '保存中...' : '保存' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── 添加巡检项弹窗 ─── -->
|
||||||
|
<div v-if="itemDialogVisible" class="modal-mask" @click.self="itemDialogVisible = false">
|
||||||
|
<div class="modal-box" style="width:400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
添加巡检项目
|
||||||
|
<span class="modal-close" @click="itemDialogVisible = false">✕</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">项目名称 *</div>
|
||||||
|
<input v-model="itemForm.item_name" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">检验标准</div>
|
||||||
|
<input v-model="itemForm.item_standard" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">排序</div>
|
||||||
|
<input v-model.number="itemForm.sort_order" type="number" 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="inspectDialogVisible" class="modal-mask" @click.self="inspectDialogVisible = false">
|
||||||
|
<div class="modal-box" style="width:560px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
开始巡检 — {{ selectedCl && selectedCl.name }}
|
||||||
|
<span class="modal-close" @click="inspectDialogVisible = false">✕</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="grid-2" style="gap:12px;margin-bottom:16px;">
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">巡检人 *</div>
|
||||||
|
<input v-model="inspectForm.inspector" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">巡检时间 *</div>
|
||||||
|
<input v-model="inspectForm.inspect_time" type="datetime-local" class="kv-input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">状态</div>
|
||||||
|
<select v-model="inspectForm.status" class="kv-input">
|
||||||
|
<option value="ok">正常</option>
|
||||||
|
<option value="issue">有问题</option>
|
||||||
|
<option value="urgent">紧急</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<div class="kv-label">总体结果</div>
|
||||||
|
<select v-model="inspectForm.overall_result" class="kv-input">
|
||||||
|
<option value="">待定</option>
|
||||||
|
<option value="pass">通过</option>
|
||||||
|
<option value="fail">不通过</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1;">
|
||||||
|
<div class="kv-label">备注</div>
|
||||||
|
<textarea v-model="inspectForm.remark" class="kv-input" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 巡检项填写 -->
|
||||||
|
<div class="sec-title" style="font-size:12px;color:#8b949e;margin-bottom:8px;">巡检项目记录</div>
|
||||||
|
<div v-if="!clItems.length" class="td-muted" style="font-size:12px;padding:8px 0;">该模板暂无巡检项目</div>
|
||||||
|
<div v-for="(item, idx) in inspectDetails" :key="idx" class="inspect-item-row">
|
||||||
|
<div class="inspect-item-name">
|
||||||
|
<span class="td-muted" style="font-size:10px;">{{ item.sort_order }}</span>
|
||||||
|
{{ item.item_name }}
|
||||||
|
<span v-if="item.item_standard" class="td-muted" style="font-size:10px;">({{ item.item_standard }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="inspect-item-inputs">
|
||||||
|
<input v-model="item.actual_value" class="kv-input" style="width:120px;" placeholder="实测值" />
|
||||||
|
<label class="ok-label">
|
||||||
|
<input type="checkbox" v-model="item.is_ok" />
|
||||||
|
<span :style="{ color: item.is_ok ? '#28a745' : '#da3633' }">{{ item.is_ok ? '正常' : '异常' }}</span>
|
||||||
|
</label>
|
||||||
|
<input v-model="item.notes" class="kv-input" style="width:120px;" placeholder="备注" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline" @click="inspectDialogVisible = false">取消</button>
|
||||||
|
<button class="btn btn-primary" :disabled="saving" @click="saveInspect">{{ saving ? '提交中...' : '提交巡检' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── 巡检明细弹窗 ─── -->
|
||||||
|
<div v-if="detailDialogVisible" class="modal-mask" @click.self="detailDialogVisible = false">
|
||||||
|
<div class="modal-box" style="width:560px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
巡检明细
|
||||||
|
<span class="modal-close" @click="detailDialogVisible = false">✕</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>项目名称</th>
|
||||||
|
<th>实测值</th>
|
||||||
|
<th>是否正常</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="d in recordDetails" :key="d.id">
|
||||||
|
<td>{{ d.item_name }}</td>
|
||||||
|
<td class="td-num">{{ d.actual_value || '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<span :class="['badge', d.is_ok ? 'badge-green' : 'badge-red']">{{ d.is_ok ? '正常' : '异常' }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="td-muted">{{ d.notes || '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!recordDetails.length">
|
||||||
|
<td colspan="4" class="td-muted" style="text-align:center;padding:16px;">暂无明细</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline" @click="detailDialogVisible = false">关闭</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,118 +377,204 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
getInspectionLocations, createInspectionLocation,
|
getChecklists, createChecklist, updateChecklist,
|
||||||
getInspectionRecords, createInspectionRecord,
|
getChecklistItems, createChecklistItem,
|
||||||
getEquipments,
|
getInspectionRecords, createInspectionRecord, getInspectionRecordDetails,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
|
|
||||||
const EQ_STATUS = {
|
const PERIOD_MAP = {
|
||||||
normal: { label: '正常', badge: 'badge-green' },
|
daily: { label: '每日', badge: 'badge-blue' },
|
||||||
fault: { label: '故障', badge: 'badge-red' },
|
weekly: { label: '每周', badge: 'badge-yellow' },
|
||||||
maintenance: { label: '检修', badge: 'badge-yellow' },
|
monthly: { label: '每月', badge: 'badge' },
|
||||||
standby: { label: '备用', badge: 'badge-gray' },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESULT_MAP = {
|
const RECORD_STATUS = {
|
||||||
normal: { label: '正常', badge: 'badge-green' },
|
ok: { label: '正常', badge: 'badge-green' },
|
||||||
abnormal: { label: '异常', badge: 'badge-red' },
|
issue: { label: '有问题', badge: 'badge-yellow' },
|
||||||
pending: { label: '待处理', badge: 'badge-yellow' },
|
urgent: { label: '紧急', badge: 'badge-red' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Inspection',
|
name: 'Inspection',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
locations: [],
|
checklists: [],
|
||||||
selectedLoc: null,
|
selectedCl: null,
|
||||||
allEquipments: [],
|
clItems: [],
|
||||||
records: [],
|
records: [], recordTotal: 0,
|
||||||
loadingRecords: false,
|
recordQuery: { page: 1, page_size: 20, status: '', start_date: '', end_date: '' },
|
||||||
|
|
||||||
saving: false,
|
saving: false,
|
||||||
recordDialogVisible: false,
|
|
||||||
locDialogVisible: false,
|
// 新增模板
|
||||||
recordForm: { result: 'normal', inspector: '', equipment_code: '', equipment_name: '', scan_code: '', notes: '' },
|
clDialogVisible: false,
|
||||||
locForm: { code: '', name: '', description: '', sort_order: 0 },
|
clForm: { name: '', equipment_code: '', equipment_name: '', period: 'daily', description: '' },
|
||||||
|
|
||||||
|
// 编辑模板
|
||||||
|
editClDialogVisible: false,
|
||||||
|
editClForm: {},
|
||||||
|
|
||||||
|
// 添加巡检项
|
||||||
|
itemDialogVisible: false,
|
||||||
|
itemForm: { item_name: '', item_standard: '', sort_order: 0 },
|
||||||
|
|
||||||
|
// 开始巡检
|
||||||
|
inspectDialogVisible: false,
|
||||||
|
inspectForm: { inspector: '', inspect_time: '', status: 'ok', overall_result: '', remark: '' },
|
||||||
|
inspectDetails: [],
|
||||||
|
|
||||||
|
// 明细
|
||||||
|
detailDialogVisible: false,
|
||||||
|
recordDetails: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
locationEquipments() {
|
cl_is_active_badge() {
|
||||||
if (!this.selectedLoc) return []
|
return this.selectedCl?.is_active ? 'badge-green' : 'badge-gray'
|
||||||
return this.allEquipments.filter(e => e.location === this.selectedLoc.name)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.fetchLocations()
|
this.fetchChecklists()
|
||||||
this.fetchAllEquipments()
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchLocations() {
|
async fetchChecklists() {
|
||||||
const res = await getInspectionLocations()
|
try {
|
||||||
this.locations = res.data || []
|
const res = await getChecklists()
|
||||||
|
this.checklists = res.data || []
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
},
|
},
|
||||||
async fetchAllEquipments() {
|
async selectChecklist(cl) {
|
||||||
const res = await getEquipments({ page: 1, page_size: 200 })
|
this.selectedCl = cl
|
||||||
this.allEquipments = res.data?.items || []
|
await Promise.all([this.fetchClItems(), this.fetchRecords()])
|
||||||
},
|
},
|
||||||
async selectLocation(loc) {
|
async fetchClItems() {
|
||||||
this.selectedLoc = loc
|
if (!this.selectedCl) return
|
||||||
await this.fetchRecords()
|
try {
|
||||||
|
const res = await getChecklistItems(this.selectedCl.id)
|
||||||
|
this.clItems = res.data || []
|
||||||
|
} catch (e) { this.clItems = [] }
|
||||||
},
|
},
|
||||||
async fetchRecords() {
|
async fetchRecords() {
|
||||||
if (!this.selectedLoc) return
|
if (!this.selectedCl) return
|
||||||
this.loadingRecords = true
|
const params = { checklist_id: this.selectedCl.id, page: this.recordQuery.page, page_size: this.recordQuery.page_size }
|
||||||
|
if (this.recordQuery.status) params.status = this.recordQuery.status
|
||||||
|
if (this.recordQuery.start_date) params.start_date = this.recordQuery.start_date
|
||||||
|
if (this.recordQuery.end_date) params.end_date = this.recordQuery.end_date
|
||||||
try {
|
try {
|
||||||
const res = await getInspectionRecords({ location_id: this.selectedLoc.id, page_size: 50 })
|
const res = await getInspectionRecords(params)
|
||||||
this.records = res.data?.items || []
|
this.records = res.data.items || []
|
||||||
} finally {
|
this.recordTotal = res.data.total || 0
|
||||||
this.loadingRecords = false
|
} catch (e) { this.records = []; this.recordTotal = 0 }
|
||||||
}
|
|
||||||
},
|
},
|
||||||
openRecordDialog(eq) {
|
|
||||||
this.recordForm = {
|
// ── 模板 ──────────────────────────────────────
|
||||||
result: 'normal',
|
openChecklistDialog() {
|
||||||
inspector: '',
|
this.clForm = { name: '', equipment_code: '', equipment_name: '', period: 'daily', description: '' }
|
||||||
equipment_code: eq ? eq.code : '',
|
this.clDialogVisible = true
|
||||||
equipment_name: eq ? eq.name : '',
|
},
|
||||||
scan_code: '',
|
async saveChecklist() {
|
||||||
|
if (!this.clForm.name) { this.$message.error('模板名称不能为空'); return }
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await createChecklist(this.clForm)
|
||||||
|
this.$message.success('创建成功')
|
||||||
|
this.clDialogVisible = false
|
||||||
|
await this.fetchChecklists()
|
||||||
|
} finally { this.saving = false }
|
||||||
|
},
|
||||||
|
openEditChecklistDialog() {
|
||||||
|
this.editClForm = { ...this.selectedCl }
|
||||||
|
this.editClDialogVisible = true
|
||||||
|
},
|
||||||
|
async saveEditChecklist() {
|
||||||
|
if (!this.editClForm.name) { this.$message.error('模板名称不能为空'); return }
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await updateChecklist(this.selectedCl.id, this.editClForm)
|
||||||
|
this.$message.success('更新成功')
|
||||||
|
this.editClDialogVisible = false
|
||||||
|
await this.fetchChecklists()
|
||||||
|
// refresh selectedCl
|
||||||
|
const updated = this.checklists.find(c => c.id === this.selectedCl.id)
|
||||||
|
if (updated) this.selectedCl = updated
|
||||||
|
} finally { this.saving = false }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 巡检项 ────────────────────────────────────
|
||||||
|
openItemDialog() {
|
||||||
|
this.itemForm = { item_name: '', item_standard: '', sort_order: this.clItems.length }
|
||||||
|
this.itemDialogVisible = true
|
||||||
|
},
|
||||||
|
async saveItem() {
|
||||||
|
if (!this.itemForm.item_name) { this.$message.error('项目名称不能为空'); return }
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await createChecklistItem(this.selectedCl.id, this.itemForm)
|
||||||
|
this.$message.success('添加成功')
|
||||||
|
this.itemDialogVisible = false
|
||||||
|
await this.fetchClItems()
|
||||||
|
} finally { this.saving = false }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 开始巡检 ──────────────────────────────────
|
||||||
|
openInspectDialog() {
|
||||||
|
const now = new Date()
|
||||||
|
const pad = n => String(n).padStart(2, '0')
|
||||||
|
const localDT = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||||
|
this.inspectForm = { inspector: '', inspect_time: localDT, status: 'ok', overall_result: '', remark: '' }
|
||||||
|
this.inspectDetails = this.clItems.map(item => ({
|
||||||
|
checklist_item_id: item.id,
|
||||||
|
item_name: item.item_name,
|
||||||
|
item_standard: item.item_standard,
|
||||||
|
sort_order: item.sort_order,
|
||||||
|
actual_value: '',
|
||||||
|
is_ok: true,
|
||||||
notes: '',
|
notes: '',
|
||||||
}
|
}))
|
||||||
this.recordDialogVisible = true
|
this.inspectDialogVisible = true
|
||||||
this.$nextTick(() => { if (this.$refs.scanInput) this.$refs.scanInput.focus() })
|
|
||||||
},
|
},
|
||||||
async saveRecord() {
|
async saveInspect() {
|
||||||
if (!this.recordForm.inspector) { this.$message.error('巡检人不能为空'); return }
|
if (!this.inspectForm.inspector) { this.$message.error('巡检人不能为空'); return }
|
||||||
|
if (!this.inspectForm.inspect_time) { this.$message.error('巡检时间不能为空'); return }
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await createInspectionRecord({ ...this.recordForm, location_id: this.selectedLoc.id })
|
const details = this.inspectDetails.map(d => ({
|
||||||
this.$message.success('保存成功')
|
checklist_item_id: d.checklist_item_id,
|
||||||
this.recordDialogVisible = false
|
item_name: d.item_name,
|
||||||
|
actual_value: d.actual_value || null,
|
||||||
|
is_ok: d.is_ok,
|
||||||
|
notes: d.notes || null,
|
||||||
|
}))
|
||||||
|
await createInspectionRecord({
|
||||||
|
checklist_id: this.selectedCl.id,
|
||||||
|
inspector: this.inspectForm.inspector,
|
||||||
|
inspect_time: this.inspectForm.inspect_time,
|
||||||
|
status: this.inspectForm.status,
|
||||||
|
overall_result: this.inspectForm.overall_result || null,
|
||||||
|
remark: this.inspectForm.remark || null,
|
||||||
|
details,
|
||||||
|
})
|
||||||
|
this.$message.success('巡检提交成功')
|
||||||
|
this.inspectDialogVisible = false
|
||||||
await this.fetchRecords()
|
await this.fetchRecords()
|
||||||
} finally {
|
} finally { this.saving = false }
|
||||||
this.saving = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
openLocDialog() {
|
|
||||||
this.locForm = { code: '', name: '', description: '', sort_order: 0 }
|
// ── 查看明细 ──────────────────────────────────
|
||||||
this.locDialogVisible = true
|
async viewDetails(record) {
|
||||||
},
|
|
||||||
async saveLocation() {
|
|
||||||
if (!this.locForm.code || !this.locForm.name) { this.$message.error('编号和名称不能为空'); return }
|
|
||||||
this.saving = true
|
|
||||||
try {
|
try {
|
||||||
await createInspectionLocation(this.locForm)
|
const res = await getInspectionRecordDetails(record.id)
|
||||||
this.$message.success('保存成功')
|
this.recordDetails = res.data || []
|
||||||
this.locDialogVisible = false
|
this.detailDialogVisible = true
|
||||||
await this.fetchLocations()
|
} catch (e) { this.$message.error('获取明细失败') }
|
||||||
} finally {
|
|
||||||
this.saving = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
eqStatusLabel(s) { return EQ_STATUS[s]?.label || s },
|
|
||||||
eqStatusBadge(s) { return EQ_STATUS[s]?.badge || 'badge-gray' },
|
// ── 工具方法 ──────────────────────────────────
|
||||||
resultLabel(r) { return RESULT_MAP[r]?.label || r },
|
|
||||||
resultBadge(r) { return RESULT_MAP[r]?.badge || 'badge-gray' },
|
|
||||||
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
||||||
|
periodLabel(p) { return PERIOD_MAP[p]?.label || p },
|
||||||
|
periodBadge(p) { return PERIOD_MAP[p]?.badge || 'badge-gray' },
|
||||||
|
recordStatusLabel(s) { return RECORD_STATUS[s]?.label || s },
|
||||||
|
recordStatusBadge(s) { return RECORD_STATUS[s]?.badge || 'badge-gray' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -298,7 +590,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.insp-sidebar {
|
.insp-sidebar {
|
||||||
width: 220px;
|
width: 230px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: $bg-card;
|
background: $bg-card;
|
||||||
border: 1px solid $border;
|
border: 1px solid $border;
|
||||||
@@ -328,14 +620,14 @@ export default {
|
|||||||
&:hover { opacity: .7; }
|
&:hover { opacity: .7; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.loc-list {
|
.cl-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: $border transparent;
|
scrollbar-color: $border transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loc-item {
|
.cl-item {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid rgba($border, .5);
|
border-bottom: 1px solid rgba($border, .5);
|
||||||
@@ -344,9 +636,9 @@ export default {
|
|||||||
&.active { background: rgba(0,200,255,.08); border-left: 3px solid $sms-highlight; }
|
&.active { background: rgba(0,200,255,.08); border-left: 3px solid $sms-highlight; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.loc-code { font-size: 10px; font-family: $font-mono; color: $text-muted; }
|
.cl-name { font-size: 12px; color: $text-primary; margin-bottom: 4px; }
|
||||||
.loc-name { font-size: 12px; color: $text-primary; margin-top: 2px; }
|
.cl-meta { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
|
||||||
.loc-empty { padding: 20px; text-align: center; font-size: 12px; color: $text-muted; }
|
.cl-empty { padding: 20px; text-align: center; font-size: 12px; color: $text-muted; }
|
||||||
|
|
||||||
.insp-main {
|
.insp-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -366,10 +658,40 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; &:hover { text-decoration: underline; } }
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
|
||||||
|
|
||||||
|
.inspect-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba($border, .4);
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
.inspect-item-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
.inspect-item-inputs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ok-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-title { font-size: 11px; color: $text-muted; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; }
|
||||||
.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; } }
|
||||||
.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: 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; }
|
||||||
|
|||||||
@@ -1,278 +1,484 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- ─── 过滤栏 ─── -->
|
<!-- ─── 标签页 ─── -->
|
||||||
<div class="card">
|
<div class="tab-bar">
|
||||||
<div class="card-body" style="padding:10px 14px;">
|
<span :class="['tab-item', { active: activeTab === 'tasks' }]" @click="activeTab = 'tasks'">检验任务</span>
|
||||||
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
|
<span :class="['tab-item', { active: activeTab === 'defects' }]" @click="activeTab = 'defects'; fetchDefects()">缺陷记录</span>
|
||||||
<div class="flex-row">
|
</div>
|
||||||
<span class="kv-label">卷号</span>
|
|
||||||
<input v-model="query.coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
|
<!-- ═══════════════════════════════════════ 检验任务 Tab ═══════════════════════════════════════ -->
|
||||||
</div>
|
<template v-if="activeTab === 'tasks'">
|
||||||
<div class="flex-row">
|
<!-- 过滤栏 -->
|
||||||
<span class="kv-label">质量等级</span>
|
<div class="card">
|
||||||
<select v-model="query.overall_grade" class="kv-input" style="width:100px;">
|
<div class="card-body" style="padding:10px 14px;">
|
||||||
<option value="">全部</option>
|
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
|
||||||
<option v-for="g in ['A1','A2','B1','B2','C']" :key="g" :value="g">{{ g }}</option>
|
<div class="flex-row">
|
||||||
</select>
|
<span class="kv-label">任务编号</span>
|
||||||
</div>
|
<input v-model="taskQuery.task_code" class="kv-input" style="width:130px;" @keyup.enter="fetchTasks" />
|
||||||
<div class="flex-row">
|
</div>
|
||||||
<span class="kv-label">日期</span>
|
<div class="flex-row">
|
||||||
<input v-model="query.start_date" type="date" class="kv-input" style="width:140px;" />
|
<span class="kv-label">卷号</span>
|
||||||
<span class="kv-label">~</span>
|
<input v-model="taskQuery.coil_no" class="kv-input" style="width:120px;" @keyup.enter="fetchTasks" />
|
||||||
<input v-model="query.end_date" type="date" class="kv-input" style="width:140px;" />
|
</div>
|
||||||
</div>
|
<div class="flex-row">
|
||||||
<div class="flex-row">
|
<span class="kv-label">状态</span>
|
||||||
<button class="btn btn-primary" @click="fetchData">查询</button>
|
<select v-model="taskQuery.status" class="kv-input" style="width:100px;">
|
||||||
<button class="btn btn-outline" @click="openDialog()">+ 新增检验</button>
|
<option value="">全部</option>
|
||||||
|
<option value="0">待检验</option>
|
||||||
|
<option value="1">检验中</option>
|
||||||
|
<option value="2">待审核</option>
|
||||||
|
<option value="3">完成</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<span class="kv-label">结果</span>
|
||||||
|
<select v-model="taskQuery.result" class="kv-input" style="width:100px;">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="qualified">合格</option>
|
||||||
|
<option value="unqualified">不合格</option>
|
||||||
|
</select>
|
||||||
|
</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">
|
||||||
|
<button class="btn btn-primary" @click="fetchTasks">查询</button>
|
||||||
|
<button class="btn btn-outline" @click="openTaskDialog()">+ 新增任务</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ─── 统计卡片 ─── -->
|
<!-- 主体区域:任务列表 + 检验项面板 -->
|
||||||
<div class="grid-4">
|
<div class="section-row">
|
||||||
<div class="metric-box">
|
<!-- 任务列表 -->
|
||||||
<div class="mb-label">合格率</div>
|
<div class="card" style="flex:3;min-width:0;">
|
||||||
<div class="mb-value" :style="{ color: summary.pass_rate >= 95 ? '#28a745' : '#f0a500' }">
|
<div class="card-header">
|
||||||
{{ summary.pass_rate }}
|
检验任务列表
|
||||||
|
<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>
|
||||||
<div class="mb-unit">%</div>
|
|
||||||
</div>
|
|
||||||
<div class="metric-box">
|
|
||||||
<div class="mb-label">平均PI评分</div>
|
|
||||||
<div class="mb-value">{{ summary.avg_pi_score }}</div>
|
|
||||||
<div class="mb-unit">/ 100</div>
|
|
||||||
</div>
|
|
||||||
<div class="metric-box">
|
|
||||||
<div class="mb-label">平均表面评分</div>
|
|
||||||
<div class="mb-value">{{ summary.avg_surface_score }}</div>
|
|
||||||
<div class="mb-unit">/ 100</div>
|
|
||||||
</div>
|
|
||||||
<div class="metric-box">
|
|
||||||
<div class="mb-label">记录总数</div>
|
|
||||||
<div class="mb-value">{{ summary.total }}</div>
|
|
||||||
<div class="mb-unit">条</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ─── 主体区域 ─── -->
|
<!-- 检验项面板 -->
|
||||||
<div class="section-row">
|
<div class="card" style="flex:2;min-width:0;">
|
||||||
<!-- 质量记录表 -->
|
<div class="card-header">
|
||||||
<div class="card" style="flex:2;">
|
<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-header">
|
<div class="card-header">
|
||||||
质量检验记录
|
缺陷记录列表
|
||||||
<span class="ch-badge">共 {{ total }} 条</span>
|
<span class="ch-badge">共 {{ defectTotal }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-scroll" v-loading="loading">
|
<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>PI评分</th>
|
<th>板面</th>
|
||||||
<th>表面评分</th>
|
<th>缺陷代码</th>
|
||||||
<th>质量等级</th>
|
<th>缺陷类型</th>
|
||||||
<th>残酸(g/m²)</th>
|
<th>缺陷率(%)</th>
|
||||||
<th>粗糙度Ra</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 defectData" :key="row.id">
|
||||||
<td class="td-num">{{ row.coil_no }}</td>
|
<td class="td-num">{{ row.coil_no || '—' }}</td>
|
||||||
<td class="td-num">{{ row.thickness_actual || '—' }}</td>
|
<td class="td-muted">{{ row.production_line || '—' }}</td>
|
||||||
<td class="td-num">{{ row.width_actual || '—' }}</td>
|
<td class="td-muted">{{ row.position || '—' }}</td>
|
||||||
<td class="td-num">{{ row.pi_score != null ? row.pi_score.toFixed(1) : '—' }}</td>
|
<td class="td-muted">{{ row.plate_surface || '—' }}</td>
|
||||||
<td class="td-num">{{ row.surface_score != null ? row.surface_score.toFixed(1) : '—' }}</td>
|
<td class="td-num">{{ row.defect_code || '—' }}</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.overall_grade" :class="['badge', gradeBadge(row.overall_grade)]">
|
<span v-if="row.degree" :class="['badge', degreeBadge(row.degree)]">{{ degreeLabel(row.degree) }}</span>
|
||||||
{{ row.overall_grade }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="td-muted">—</span>
|
<span v-else class="td-muted">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="td-num">{{ row.acid_residual || '—' }}</td>
|
<td class="td-muted">{{ row.judge_level || '—' }}</td>
|
||||||
<td class="td-num">{{ row.roughness_ra || '—' }}</td>
|
<td class="td-muted">{{ row.judge_by || '—' }}</td>
|
||||||
|
<td class="td-muted">{{ fmtTime(row.judge_time) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span :class="['badge', row.is_passed ? 'badge-green' : 'badge-red']">
|
<span class="action-link" @click="openDefectDialog(row)">编辑</span>
|
||||||
{{ row.is_passed ? '合格' : '不合格' }}
|
<span class="action-link" style="color:#da3633;" @click="deleteDefect(row)">删除</span>
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="td-muted">{{ row.inspector || '—' }}</td>
|
|
||||||
<td class="td-muted">{{ fmtTime(row.inspect_time) }}</td>
|
|
||||||
<td><span class="action-link" @click="openDialog(row)">编辑</span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!tableData.length && !loading">
|
<tr v-if="!defectData.length">
|
||||||
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
|
<td colspan="13" 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="card-body" style="padding:8px 14px;" v-if="defectTotal > defectQuery.page_size">
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button class="btn btn-outline" :disabled="query.page<=1" @click="query.page--;fetchData()">上一页</button>
|
<button class="btn btn-outline" :disabled="defectQuery.page <= 1" @click="defectQuery.page--; fetchDefects()">上一页</button>
|
||||||
<span class="kv-label">第 {{ query.page }} / {{ Math.ceil(total/query.page_size) }} 页</span>
|
<span class="kv-label">第 {{ defectQuery.page }} / {{ Math.ceil(defectTotal / defectQuery.page_size) }} 页</span>
|
||||||
<button class="btn btn-outline" :disabled="query.page>=Math.ceil(total/query.page_size)" @click="query.page++;fetchData()">下一页</button>
|
<button class="btn btn-outline" :disabled="defectQuery.page >= Math.ceil(defectTotal / defectQuery.page_size)" @click="defectQuery.page++; fetchDefects()">下一页</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 右侧面板 -->
|
<!-- ─── 新增/编辑任务弹窗 ─── -->
|
||||||
<div style="flex:1;display:flex;flex-direction:column;gap:14px;min-width:0;">
|
<div v-if="taskDialogVisible" class="modal-mask" @click.self="taskDialogVisible = false">
|
||||||
<!-- 快速预测面板 -->
|
<div class="modal-box" style="width:560px;">
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">快速质量预测</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex-col">
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">厚度 (mm)</div>
|
|
||||||
<input v-model.number="pred.thickness" type="number" class="kv-input" step="0.1" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">平均速度 (m/min)</div>
|
|
||||||
<input v-model.number="pred.avg_speed" type="number" class="kv-input" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">平均HCl浓度 (g/L)</div>
|
|
||||||
<input v-model.number="pred.acid_conc_avg" type="number" class="kv-input" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">平均温度 (°C)</div>
|
|
||||||
<input v-model.number="pred.acid_temp_avg" type="number" class="kv-input" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">铁皮重量 (g/m²)</div>
|
|
||||||
<input v-model.number="pred.scale_weight" type="number" class="kv-input" step="0.5" />
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary fw" :disabled="predLoading" @click="doPred">
|
|
||||||
{{ predLoading ? '预测中...' : '质量预测' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 预测结果 -->
|
|
||||||
<div v-if="predResult" class="mt8">
|
|
||||||
<div class="kv-grid" style="margin-bottom:10px;">
|
|
||||||
<span class="kv-label">PI评分</span>
|
|
||||||
<span class="kv-value">{{ predResult.pi_score }}</span>
|
|
||||||
<span class="kv-label">表面评分</span>
|
|
||||||
<span class="kv-value">{{ predResult.surface_score }}</span>
|
|
||||||
<span class="kv-label">综合等级</span>
|
|
||||||
<span>
|
|
||||||
<span :class="['badge', gradeBadge(predResult.overall_grade)]" style="font-size:14px;padding:2px 12px;">
|
|
||||||
{{ predResult.overall_grade }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="sec-title">工艺建议</div>
|
|
||||||
<div v-for="(r, i) in predResult.recommendations" :key="i" class="rec-item">
|
|
||||||
💡 {{ r }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 等级分布图 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">等级分布</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<canvas ref="gradeChart" height="140"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ─── 新增/编辑弹窗 ─── -->
|
|
||||||
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
|
|
||||||
<div class="modal-box" style="width:680px;">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
{{ editRow ? '编辑质量记录 #' + editRow.id : '新增质量记录' }}
|
{{ editTask ? '编辑任务 #' + editTask.id : '新增检验任务' }}
|
||||||
<span class="modal-close" @click="dialogVisible=false">✕</span>
|
<span class="modal-close" @click="taskDialogVisible = false">✕</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="grid-3" style="gap:12px;margin-bottom:14px;">
|
<div class="grid-2" 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="taskForm.task_code" class="kv-input" :disabled="!!editTask" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="kv-label">实测厚度 (mm)</div>
|
<div class="kv-label">卷号</div>
|
||||||
<input v-model.number="form.thickness_actual" type="number" class="kv-input" step="0.01" />
|
<input v-model="taskForm.coil_no" class="kv-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="kv-label">实测宽度 (mm)</div>
|
<div class="kv-label">任务类型</div>
|
||||||
<input v-model.number="form.width_actual" type="number" class="kv-input" />
|
<select v-model="taskForm.task_type" class="kv-input">
|
||||||
</div>
|
<option value="">请选择</option>
|
||||||
<div class="form-field">
|
<option value="incoming">来料检验</option>
|
||||||
<div class="kv-label">平直度 (IU)</div>
|
<option value="process">过程检验</option>
|
||||||
<input v-model.number="form.flatness" type="number" class="kv-input" step="0.1" />
|
<option value="final">成品检验</option>
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">凸度 (μm)</div>
|
|
||||||
<input v-model.number="form.crown" type="number" class="kv-input" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">表面缺陷类型</div>
|
|
||||||
<select v-model="form.surface_defect_type" class="kv-input">
|
|
||||||
<option value="">无缺陷</option>
|
|
||||||
<option value="划伤">划伤</option>
|
|
||||||
<option value="压印">压印</option>
|
|
||||||
<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">缺陷长度 (m)</div>
|
<div class="kv-label">方案名称</div>
|
||||||
<input v-model.number="form.defect_length_m" type="number" class="kv-input" step="0.1" />
|
<input v-model="taskForm.scheme_name" 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.defect_position" class="kv-input" />
|
<input v-model="taskForm.inspect_user" class="kv-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div v-if="editTask" class="form-field">
|
||||||
<div class="kv-label">PI评分</div>
|
<div class="kv-label">状态</div>
|
||||||
<input v-model.number="form.pi_score" type="number" class="kv-input" step="0.1" min="0" max="100" />
|
<select v-model="taskForm.status" class="kv-input">
|
||||||
</div>
|
<option :value="0">待检验</option>
|
||||||
<div class="form-field">
|
<option :value="1">检验中</option>
|
||||||
<div class="kv-label">表面评分</div>
|
<option :value="2">待审核</option>
|
||||||
<input v-model.number="form.surface_score" type="number" class="kv-input" step="0.1" min="0" max="100" />
|
<option :value="3">完成</option>
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">综合等级</div>
|
|
||||||
<select v-model="form.overall_grade" class="kv-input">
|
|
||||||
<option v-for="g in ['A1','A2','B1','B2','C']" :key="g" :value="g">{{ g }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div v-if="editTask" class="form-field">
|
||||||
<div class="kv-label">残酸量 (g/m²)</div>
|
<div class="kv-label">检验结果</div>
|
||||||
<input v-model.number="form.acid_residual" type="number" class="kv-input" step="0.01" />
|
<select v-model="taskForm.result" class="kv-input">
|
||||||
</div>
|
<option value="">待定</option>
|
||||||
<div class="form-field">
|
<option value="qualified">合格</option>
|
||||||
<div class="kv-label">粗糙度 Ra (μm)</div>
|
<option value="unqualified">不合格</option>
|
||||||
<input v-model.number="form.roughness_ra" type="number" class="kv-input" step="0.01" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">检验员</div>
|
|
||||||
<input v-model="form.inspector" class="kv-input" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="kv-label">是否合格</div>
|
|
||||||
<select v-model="form.is_passed" class="kv-input">
|
|
||||||
<option :value="true">合格</option>
|
|
||||||
<option :value="false">不合格</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1;">
|
||||||
|
<div class="kv-label">备注</div>
|
||||||
|
<textarea v-model="taskForm.remark" class="kv-input" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
|
<button class="btn btn-outline" @click="taskDialogVisible = false">取消</button>
|
||||||
<button class="btn btn-primary" :disabled="saving" @click="save">
|
<button class="btn btn-primary" :disabled="saving" @click="saveTask">{{ saving ? '保存中...' : '保存' }}</button>
|
||||||
{{ saving ? '保存中...' : '保存' }}
|
</div>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
@@ -280,127 +486,202 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getQualityList, createQuality, getQualitySummary, predictQuality } from '@/api'
|
import {
|
||||||
|
getQcTasks, createQcTask, updateQcTask, deleteQcTask,
|
||||||
|
getQcTaskItems, createQcTaskItem,
|
||||||
|
getQcDefects, createQcDefect, updateQcDefect, deleteQcDefect,
|
||||||
|
} from '@/api'
|
||||||
|
|
||||||
|
const TASK_STATUS = {
|
||||||
|
0: { label: '待检验', badge: 'badge-gray' },
|
||||||
|
1: { label: '检验中', badge: 'badge-yellow' },
|
||||||
|
2: { label: '待审核', badge: 'badge' },
|
||||||
|
3: { label: '完成', badge: 'badge-green' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEGREE_MAP = {
|
||||||
|
light: { label: '轻微', badge: 'badge-blue' },
|
||||||
|
normal: { label: '一般', badge: 'badge-yellow' },
|
||||||
|
serious: { label: '严重', badge: 'badge-red' },
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Quality',
|
name: 'Quality',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false, saving: false, predLoading: false,
|
activeTab: 'tasks',
|
||||||
tableData: [], total: 0,
|
saving: false,
|
||||||
summary: { pass_rate: 0, avg_pi_score: 0, avg_surface_score: 0, total: 0, grade_distribution: {} },
|
|
||||||
query: { page: 1, page_size: 20, coil_no: '', overall_grade: '', start_date: '', end_date: '' },
|
// 任务
|
||||||
dialogVisible: false, editRow: null, form: {},
|
taskData: [], taskTotal: 0,
|
||||||
pred: { thickness: 3.0, avg_speed: 100, acid_conc_avg: 160, acid_temp_avg: 75, scale_weight: 8.5 },
|
taskQuery: { page: 1, page_size: 20, task_code: '', coil_no: '', status: '', result: '', start_date: '', end_date: '' },
|
||||||
predResult: null,
|
selectedTask: null,
|
||||||
|
taskItems: [],
|
||||||
|
taskDialogVisible: false, editTask: null, taskForm: {},
|
||||||
|
itemDialogVisible: false, itemForm: {},
|
||||||
|
|
||||||
|
// 缺陷
|
||||||
|
defectData: [], defectTotal: 0,
|
||||||
|
defectQuery: { page: 1, page_size: 20, coil_no: '', defect_type: '', degree: '', start_date: '', end_date: '' },
|
||||||
|
defectDialogVisible: false, editDefect: null, defectForm: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.fetchData()
|
this.fetchTasks()
|
||||||
this.fetchSummary()
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
// ── 任务 ──────────────────────────────────────
|
||||||
this.loading = true
|
async fetchTasks() {
|
||||||
const params = { ...this.query }
|
const params = {}
|
||||||
if (!params.coil_no) delete params.coil_no
|
if (this.taskQuery.task_code) params.task_code = this.taskQuery.task_code
|
||||||
if (!params.overall_grade) delete params.overall_grade
|
if (this.taskQuery.coil_no) params.coil_no = this.taskQuery.coil_no
|
||||||
if (params.start_date) params.start_date = params.start_date + 'T00:00:00'
|
if (this.taskQuery.status !== '') params.status = this.taskQuery.status
|
||||||
else delete params.start_date
|
if (this.taskQuery.result) params.result = this.taskQuery.result
|
||||||
if (params.end_date) params.end_date = params.end_date + 'T23:59:59'
|
if (this.taskQuery.start_date) params.start_date = this.taskQuery.start_date
|
||||||
else delete params.end_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 getQualityList(params)
|
const res = await getQcTasks(params)
|
||||||
this.tableData = res.data.items
|
this.taskData = res.data.items
|
||||||
this.total = res.data.total
|
this.taskTotal = res.data.total
|
||||||
} finally { this.loading = false }
|
|
||||||
},
|
|
||||||
async fetchSummary() {
|
|
||||||
try {
|
|
||||||
const res = await getQualitySummary()
|
|
||||||
this.summary = res.data
|
|
||||||
this.$nextTick(() => this.drawChart())
|
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
},
|
},
|
||||||
drawChart() {
|
async selectTask(row) {
|
||||||
const canvas = this.$refs.gradeChart
|
this.selectedTask = row
|
||||||
if (!canvas) return
|
try {
|
||||||
const ctx = canvas.getContext('2d')
|
const res = await getQcTaskItems(row.id)
|
||||||
const grades = ['A1','A2','B1','B2','C']
|
this.taskItems = res.data || []
|
||||||
const dist = this.summary.grade_distribution || {}
|
} catch (e) { this.taskItems = [] }
|
||||||
const counts = grades.map(g => dist[g] || 0)
|
|
||||||
const maxCount = Math.max(...counts, 1)
|
|
||||||
const colors = ['#28a745','#00c8ff','#0078d4','#f0a500','#da3633']
|
|
||||||
const W = canvas.offsetWidth || 300
|
|
||||||
const H = 140
|
|
||||||
canvas.width = W
|
|
||||||
canvas.height = H
|
|
||||||
ctx.clearRect(0, 0, W, H)
|
|
||||||
const barW = Math.floor(W / grades.length) - 8
|
|
||||||
const pad = 4
|
|
||||||
grades.forEach((g, i) => {
|
|
||||||
const barH = Math.floor((counts[i] / maxCount) * (H - 36))
|
|
||||||
const x = i * (barW + 8) + pad
|
|
||||||
const y = H - barH - 20
|
|
||||||
ctx.fillStyle = colors[i]
|
|
||||||
ctx.globalAlpha = 0.85
|
|
||||||
ctx.fillRect(x, y, barW, barH)
|
|
||||||
ctx.globalAlpha = 1
|
|
||||||
ctx.fillStyle = '#e6edf3'
|
|
||||||
ctx.font = '11px Consolas'
|
|
||||||
ctx.textAlign = 'center'
|
|
||||||
ctx.fillText(counts[i], x + barW / 2, y - 4)
|
|
||||||
ctx.fillStyle = '#8b949e'
|
|
||||||
ctx.font = '11px "Microsoft YaHei"'
|
|
||||||
ctx.fillText(g, x + barW / 2, H - 4)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' },
|
openTaskDialog(row = null) {
|
||||||
gradeBadge(g) {
|
this.editTask = row
|
||||||
if (g === 'A1') return 'badge-green'
|
this.taskForm = row ? { ...row } : { task_code: '', status: 0 }
|
||||||
if (g === 'A2') return 'badge-blue'
|
this.taskDialogVisible = true
|
||||||
if (g === 'B1') return 'badge-blue'
|
|
||||||
if (g === 'B2') return 'badge-yellow'
|
|
||||||
return 'badge-red'
|
|
||||||
},
|
},
|
||||||
openDialog(row = null) {
|
async saveTask() {
|
||||||
this.editRow = row
|
if (!this.taskForm.task_code) { this.$message.error('任务编号不能为空'); return }
|
||||||
this.form = row ? { ...row } : { is_passed: true }
|
|
||||||
this.dialogVisible = true
|
|
||||||
},
|
|
||||||
async save() {
|
|
||||||
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return }
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
if (this.editRow) await createQuality({ ...this.form }) // use PUT when available
|
if (this.editTask) {
|
||||||
else await createQuality(this.form)
|
await updateQcTask(this.editTask.id, this.taskForm)
|
||||||
|
} else {
|
||||||
|
await createQcTask(this.taskForm)
|
||||||
|
}
|
||||||
this.$message.success('保存成功')
|
this.$message.success('保存成功')
|
||||||
this.dialogVisible = false
|
this.taskDialogVisible = false
|
||||||
this.fetchData()
|
this.fetchTasks()
|
||||||
this.fetchSummary()
|
|
||||||
} finally { this.saving = false }
|
} finally { this.saving = false }
|
||||||
},
|
},
|
||||||
async doPred() {
|
async deleteTask(row) {
|
||||||
this.predLoading = true
|
if (!confirm(`确认删除任务 ${row.task_code}?`)) return
|
||||||
try {
|
try {
|
||||||
const res = await predictQuality(this.pred)
|
await deleteQcTask(row.id)
|
||||||
this.predResult = res.data
|
this.$message.success('已删除')
|
||||||
} catch (e) {
|
if (this.selectedTask && this.selectedTask.id === row.id) {
|
||||||
this.$message.error('预测失败:' + (e.response?.data?.detail || e.message))
|
this.selectedTask = null
|
||||||
} finally { this.predLoading = false }
|
this.taskItems = []
|
||||||
|
}
|
||||||
|
this.fetchTasks()
|
||||||
|
} catch (e) { this.$message.error('删除失败') }
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
// ── 检验项 ────────────────────────────────────
|
||||||
|
openItemDialog() {
|
||||||
|
this.itemForm = { task_id: this.selectedTask.id, item_name: '', is_qualified: null }
|
||||||
|
this.itemDialogVisible = true
|
||||||
|
},
|
||||||
|
async saveItem() {
|
||||||
|
if (!this.itemForm.item_name) { this.$message.error('检验项名称不能为空'); return }
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await createQcTaskItem({ ...this.itemForm, task_id: this.selectedTask.id })
|
||||||
|
this.$message.success('添加成功')
|
||||||
|
this.itemDialogVisible = false
|
||||||
|
await this.selectTask(this.selectedTask)
|
||||||
|
} finally { this.saving = false }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 缺陷 ──────────────────────────────────────
|
||||||
|
async fetchDefects() {
|
||||||
|
const params = {}
|
||||||
|
if (this.defectQuery.coil_no) params.coil_no = this.defectQuery.coil_no
|
||||||
|
if (this.defectQuery.defect_type) params.defect_type = this.defectQuery.defect_type
|
||||||
|
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 {
|
||||||
|
const res = await getQcDefects(params)
|
||||||
|
this.defectData = res.data.items
|
||||||
|
this.defectTotal = res.data.total
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
},
|
||||||
|
openDefectDialog(row = null) {
|
||||||
|
this.editDefect = row
|
||||||
|
this.defectForm = row ? { ...row } : {}
|
||||||
|
this.defectDialogVisible = true
|
||||||
|
},
|
||||||
|
async saveDefect() {
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
if (this.editDefect) {
|
||||||
|
await updateQcDefect(this.editDefect.id, this.defectForm)
|
||||||
|
} else {
|
||||||
|
await createQcDefect(this.defectForm)
|
||||||
|
}
|
||||||
|
this.$message.success('保存成功')
|
||||||
|
this.defectDialogVisible = false
|
||||||
|
this.fetchDefects()
|
||||||
|
} 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', ' ') : '—' },
|
||||||
|
taskStatusLabel(s) { return TASK_STATUS[s]?.label || s },
|
||||||
|
taskStatusBadge(s) { return TASK_STATUS[s]?.badge || 'badge-gray' },
|
||||||
|
degreeLabel(d) { return DEGREE_MAP[d]?.label || d },
|
||||||
|
degreeBadge(d) { return DEGREE_MAP[d]?.badge || 'badge-gray' },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</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: 10px; &:hover { text-decoration: underline; } }
|
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 2px solid $border;
|
||||||
|
}
|
||||||
|
.tab-item {
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $text-muted;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all .15s;
|
||||||
|
&:hover { color: $text-primary; }
|
||||||
|
&.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
|
||||||
|
}
|
||||||
|
.section-row { display: flex; gap: 14px; align-items: flex-start; }
|
||||||
|
.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; }
|
||||||
.rec-item { padding: 5px 8px; margin-top: 4px; background: rgba(0,200,255,.05); border-left: 2px solid $sms-highlight; font-size: 12px; color: $text-secondary; border-radius: 0 3px 3px 0; }
|
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
|
||||||
.mt8 { margin-top: 8px; }
|
|
||||||
.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: 680px; 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; } } }
|
||||||
.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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user