feat(qc): implement material library with CRUD, stock-in, usage tracking and task deduction

This commit is contained in:
zlwu committed 2026-05-20 06:36:50 +08:00
1 parent 4137c54588
commit e6072457fd
9 files changed
+1638 -22

No files matched your search

+177
View File
@@ -14,6 +14,8 @@ from app.crud import qc_task_result as result_svc
from app.crud import qc_task as task_svc
from app.crud import qc_third_party_company as tpc_svc
from app.crud import qc_third_party_item as tpi_svc
from app.crud import qc_material as mat_svc
from app.crud import qc_material_usage as mu_svc
from app.crud import qc_work_order as wo_svc
from app.crud.collection_protocol import total_pages
from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
@@ -373,6 +375,7 @@ class TaskCompleteWithResultBody(BaseModel):
results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
remark: Optional[str] = Field(None, alias="remark")
third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
material_usages: Optional[List[Dict[str, Any]]] = Field(None, alias="materialUsages")
@router.get("/qc-lab-test-items")
@@ -745,6 +748,18 @@ async def complete_task_with_result(
db, task_id, attachments=body.third_party_attachments,
tenant_no=tenant_no, user_id=user_id,
)
# 物料扣减(可选,仅当传了 materialUsages 时)
if body.material_usages:
task_obj_pre = task_svc.get_task(db, task_id, tenant_no=tenant_no)
task_no = task_obj_pre.task_no if task_obj_pre else ""
mu_svc.deduct_for_task(
db,
task_id,
task_no=task_no,
material_usages=body.material_usages,
tenant_no=tenant_no,
user_id=user_id,
)
obj = task_svc.apply_task_result(
db,
task_id,
@@ -788,3 +803,165 @@ async def update_qc_task_attachments(
return error_response(str(e))
except Exception as e:
return error_response(str(e))
# ---------- 质检物料库 ----------
class QcMaterialBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
material_code: str = Field(..., alias="materialCode")
material_name: str = Field(..., alias="materialName")
spec_model: Optional[str] = Field(None, alias="specModel")
unit: Optional[str] = None
warning_qty: Optional[float] = Field(0, alias="warningQty")
status: str = "1"
remark: Optional[str] = None
class QcMaterialUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
material_code: Optional[str] = Field(None, alias="materialCode")
material_name: Optional[str] = Field(None, alias="materialName")
spec_model: Optional[str] = Field(None, alias="specModel")
unit: Optional[str] = None
warning_qty: Optional[float] = Field(None, alias="warningQty")
status: Optional[str] = None
remark: Optional[str] = None
class StockInBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
qty: float = Field(..., gt=0)
remark: Optional[str] = None
@router.get("/qc-materials")
async def list_qc_materials(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
keyword: Optional[str] = None,
status: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = mat_svc.list_materials(
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
)
return success_response(_page_payload(items, total, page, pageSize))
except PermissionError as e:
return error_response(str(e), code=403)
except Exception as e:
return error_response(str(e))
@router.get("/qc-materials/options")
async def qc_material_options(
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
except Exception as e:
return error_response(str(e))
@router.post("/qc-materials")
async def create_qc_material(
body: QcMaterialBody,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = mat_svc.create_material(db, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id)
return success_response(obj, message="创建成功")
except ValueError as e:
return error_response(str(e))
except Exception as e:
return error_response(str(e))
@router.put("/qc-materials/{material_id}")
async def update_qc_material(
material_id: str,
body: QcMaterialUpdateBody,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = mat_svc.update_material(db, material_id, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id)
return success_response(obj, message="更新成功")
except ValueError as e:
return error_response(str(e))
except Exception as e:
return error_response(str(e))
@router.delete("/qc-materials/{material_id}")
async def delete_qc_material(
material_id: str,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no, user_id=user_id)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(str(e))
except Exception as e:
return error_response(str(e))
@router.post("/qc-materials/{material_id}/stock-in")
async def stock_in_qc_material(
material_id: str,
body: StockInBody,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = mat_svc.stock_in(db, material_id, qty=body.qty, remark=body.remark, tenant_no=tenant_no, user_id=user_id)
return success_response(obj, message="入库成功")
except ValueError as e:
return error_response(str(e))
except Exception as e:
return error_response(str(e))
# ---------- 物料变动记录 ----------
@router.get("/qc-material-usages")
async def list_qc_material_usages(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
materialId: Optional[str] = None,
changeType: Optional[str] = None,
startTime: Optional[str] = None,
endTime: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcMaterialUsage:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = mu_svc.list_usages(
db, tenant_no, page=page, page_size=pageSize,
material_id=materialId, change_type=changeType,
start_time=startTime, end_time=endTime,
)
return success_response(_page_payload(items, total, page, pageSize))
except PermissionError as e:
return error_response(str(e), code=403)
except Exception as e:
return error_response(str(e))
+313
View File
@@ -0,0 +1,313 @@
"""物料库 CRUD"""
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import BizQcMaterial, BizQcMaterialUsage
def _dec(v: Any) -> float:
if v is None:
return 0.0
return float(v)
def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _row_to_dict(r: BizQcMaterial) -> Dict[str, Any]:
return {
"id": r.id,
"materialCode": r.material_code,
"materialName": r.material_name,
"specModel": r.spec_model or "",
"unit": r.unit or "",
"stockQty": _dec(r.stock_qty),
"warningQty": _dec(r.warning_qty),
"status": r.status or "1",
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark or "",
"createTime": _fmt_dt(r.create_time),
"updateTime": _fmt_dt(r.update_time),
}
def code_exists(
db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None
) -> bool:
c = (code or "").strip()
if not c:
return False
q = db.query(BizQcMaterial).filter(
BizQcMaterial.material_code == c,
BizQcMaterial.tenant_no == tenant_no,
BizQcMaterial.del_flag == 0,
)
if exclude_id:
q = q.filter(BizQcMaterial.id != exclude_id)
return q.first() is not None
def total_pages(db: Session, tenant_no: str, page_size: int = 10) -> int:
cnt = (
db.query(func.count(BizQcMaterial.id))
.filter(
BizQcMaterial.tenant_no == tenant_no,
BizQcMaterial.del_flag == 0,
)
.scalar()
or 0
)
ps = max(1, page_size)
return (cnt + ps - 1) // ps
def list_materials(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
status: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizQcMaterial).filter(BizQcMaterial.del_flag == 0)
if tenant_no:
q0 = q0.filter(BizQcMaterial.tenant_no == tenant_no)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(
BizQcMaterial.material_code.like(like),
BizQcMaterial.material_name.like(like),
)
)
st = (status or "").strip()
if st in ("0", "1"):
q0 = q0.filter(BizQcMaterial.status == st)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizQcMaterial.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizQcMaterial.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [_row_to_dict(x) for x in rows], int(total)
def get_material(
db: Session, row_id: str, tenant_no: Optional[str] = None
) -> Optional[BizQcMaterial]:
q = db.query(BizQcMaterial).filter(
BizQcMaterial.id == row_id, BizQcMaterial.del_flag == 0
)
if tenant_no:
q = q.filter(BizQcMaterial.tenant_no == tenant_no)
return q.first()
def create_material(
db: Session,
*,
material_code: str,
material_name: str,
spec_model: Optional[str] = None,
unit: Optional[str] = None,
stock_qty: Optional[Decimal] = None,
warning_qty: Optional[Decimal] = None,
status: str = "1",
tenant_no: str,
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> BizQcMaterial:
code = (material_code or "").strip()
if not code:
raise ValueError("物料编码不能为空")
if code_exists(db, code, tenant_no):
raise ValueError("物料编码已存在")
name = (material_name or "").strip()
if not name:
raise ValueError("物料名称不能为空")
st = (status or "1").strip()
if st not in ("0", "1"):
raise ValueError("状态无效")
obj = BizQcMaterial(
id=generate_uuid(),
material_code=code,
material_name=name,
spec_model=(spec_model or "").strip() or None,
unit=(unit or "").strip() or None,
stock_qty=stock_qty if stock_qty is not None else 0,
warning_qty=warning_qty if warning_qty is not None else 0,
status=st,
tenant_no=tenant_no,
order_num=order_num if order_num is not None else 0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_material(
db: Session,
row_id: str,
*,
material_code: Optional[str] = None,
material_name: Optional[str] = None,
spec_model: Optional[str] = None,
unit: Optional[str] = None,
warning_qty: Optional[Decimal] = None,
status: Optional[str] = None,
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> BizQcMaterial:
obj = get_material(db, row_id, tenant_no=tenant_no)
if not obj:
raise ValueError("物料不存在")
if material_code is not None:
code = material_code.strip()
if not code:
raise ValueError("物料编码不能为空")
if code != obj.material_code and code_exists(db, code, obj.tenant_no, exclude_id=row_id):
raise ValueError("物料编码已存在")
obj.material_code = code
if material_name is not None:
name = (material_name or "").strip()
if not name:
raise ValueError("物料名称不能为空")
obj.material_name = name
if spec_model is not None:
obj.spec_model = (spec_model or "").strip() or None
if unit is not None:
obj.unit = (unit or "").strip() or None
if warning_qty is not None:
obj.warning_qty = warning_qty
if status is not None:
st = status.strip()
if st not in ("0", "1"):
raise ValueError("状态无效")
obj.status = st
if order_num is not None:
obj.order_num = order_num
if remark is not None:
obj.remark = (remark or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def soft_delete_material(db: Session, row_id: str, tenant_no: Optional[str] = None) -> None:
obj = get_material(db, row_id, tenant_no=tenant_no)
if not obj:
raise ValueError("物料不存在")
if obj.stock_qty is not None and _dec(obj.stock_qty) > 0:
raise ValueError("该物料仍有库存,无法删除")
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
db.commit()
def stock_in(
db: Session,
row_id: str,
*,
qty: Decimal,
remark: Optional[str] = None,
user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> BizQcMaterial:
if qty is None or float(qty) <= 0:
raise ValueError("入库数量必须大于0")
# 行锁锁定物料行
obj = (
db.query(BizQcMaterial)
.filter(BizQcMaterial.id == row_id, BizQcMaterial.del_flag == 0)
.with_for_update()
.first()
)
if not obj:
raise ValueError("物料不存在")
old_stock = obj.stock_qty or 0
new_stock = old_stock + qty
obj.stock_qty = new_stock
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
# 写入使用记录
usage = BizQcMaterialUsage(
id=generate_uuid(),
material_id=obj.id,
material_code=obj.material_code,
material_name=obj.material_name,
change_type="in",
change_qty=qty,
stock_after=new_stock,
source_type="manual_in",
tenant_no=obj.tenant_no,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id=obj.create_dept_id or "",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(usage)
db.commit()
db.refresh(obj)
return obj
def options_list(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcMaterial)
.filter(
BizQcMaterial.tenant_no == tenant_no,
BizQcMaterial.del_flag == 0,
BizQcMaterial.status == "1",
)
.order_by(BizQcMaterial.create_time.desc())
.all()
)
return [
{
"id": r.id,
"label": r.material_name,
"materialCode": r.material_code,
"materialName": r.material_name,
"specModel": r.spec_model or "",
"unit": r.unit or "",
"stockQty": _dec(r.stock_qty),
}
for r in rows
]
+140
View File
@@ -0,0 +1,140 @@
"""物料变动记录 CRUD"""
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from fastapi import HTTPException
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.qc_material import BizQcMaterial, BizQcMaterialUsage
def _row_to_dict(r: BizQcMaterialUsage) -> Dict[str, Any]:
return {
"id": r.id,
"materialId": r.material_id,
"materialCode": r.material_code or "",
"materialName": r.material_name or "",
"changeType": r.change_type,
"changeQty": float(r.change_qty) if r.change_qty is not None else 0,
"stockAfter": float(r.stock_after) if r.stock_after is not None else 0,
"qcTaskId": r.qc_task_id or "",
"qcTaskNo": r.qc_task_no or "",
"sourceType": r.source_type,
"remark": r.remark or "",
"createBy": r.create_by or "",
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
}
def list_usages(
db: Session,
tenant_no: str,
*,
page: int = 1,
page_size: int = 10,
material_id: Optional[str] = None,
change_type: Optional[str] = None,
start_time: Optional[str] = None,
end_time: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizQcMaterialUsage).filter(
BizQcMaterialUsage.tenant_no == tenant_no,
BizQcMaterialUsage.del_flag == 0,
)
if material_id:
q0 = q0.filter(BizQcMaterialUsage.material_id == material_id)
if change_type:
q0 = q0.filter(BizQcMaterialUsage.change_type == change_type)
if start_time:
q0 = q0.filter(BizQcMaterialUsage.create_time >= start_time)
if end_time:
q0 = q0.filter(BizQcMaterialUsage.create_time <= end_time + " 23:59:59")
return q0
total = _filtered().order_by(None).with_entities(func.count(BizQcMaterialUsage.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizQcMaterialUsage.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [_row_to_dict(x) for x in rows], int(total)
def deduct_for_task(
db: Session,
task_id: str,
task_no: str,
material_usages: List[Dict[str, Any]],
tenant_no: str,
user_id: Optional[str] = None,
dept_id: str = "",
) -> None:
"""扣减物料库存并生成变动记录。调用方控制事务,本函数不 commit。"""
for item in material_usages:
mid = item.get("materialId") or ""
qty = item.get("qty")
if not mid:
raise HTTPException(status_code=400, detail="物料ID不能为空")
if qty is None or Decimal(str(qty)) <= 0:
raise HTTPException(status_code=400, detail="物料使用数量必须大于0")
qty_dec = Decimal(str(qty))
material = (
db.query(BizQcMaterial)
.with_for_update()
.filter(
BizQcMaterial.id == mid,
BizQcMaterial.tenant_no == tenant_no,
BizQcMaterial.del_flag == 0,
BizQcMaterial.status == "1",
)
.first()
)
if not material:
raise HTTPException(status_code=400, detail=f"物料不存在或已停用(ID: {mid})")
stock_qty = material.stock_qty if material.stock_qty is not None else Decimal("0")
if stock_qty < qty_dec:
raise HTTPException(
status_code=400,
detail=f"物料「{material.material_name}」库存不足,当前库存 {stock_qty},需使用 {qty_dec}",
)
new_stock = stock_qty - qty_dec
material.stock_qty = new_stock
material.update_by = user_id
material.update_time = datetime.now()
usage = BizQcMaterialUsage(
id=generate_uuid(),
material_id=mid,
material_code=material.material_code,
material_name=material.material_name,
change_type="use",
change_qty=-qty_dec,
stock_after=new_stock,
qc_task_id=task_id,
qc_task_no=task_no,
source_type="task_complete",
tenant_no=tenant_no,
remark="",
create_by=user_id,
create_dept_id=dept_id,
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(usage)
+39
View File
@@ -0,0 +1,39 @@
import request from '@/utils/http'
type PageList<T> = {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export type QcMaterialUsage = {
id: string
materialId: string
materialCode: string
materialName: string
changeType: string
changeQty: number
stockAfter: number
qcTaskId: string
qcTaskNo: string
sourceType: string
remark: string
createBy: string
createTime: string
}
export function fetchQcMaterialUsageList(params?: {
page?: number
pageSize?: number
materialId?: string
changeType?: string
startTime?: string
endTime?: string
}) {
return request.get<PageList<QcMaterialUsage>>({
url: '/web/admin/qc-material-usages',
params
})
}
+81
View File
@@ -0,0 +1,81 @@
import request from '@/utils/http'
type PageList<T> = {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export type QcMaterial = {
id: string
materialCode: string
materialName: string
specModel: string
unit: string
stockQty: number
warningQty: number
status: string
tenantNo: string
orderNum: number
remark: string
createBy: string
createTime: string
updateTime: string
}
export type QcMaterialOption = {
id: string
label: string
materialCode: string
materialName: string
specModel: string
unit: string
stockQty: number
}
export function fetchQcMaterialList(params?: {
page?: number
pageSize?: number
keyword?: string
status?: string
}) {
return request.get<PageList<QcMaterial>>({
url: '/web/admin/qc-materials',
params
})
}
export function fetchQcMaterialOptions() {
return request.get<QcMaterialOption[]>({
url: '/web/admin/qc-materials/options'
})
}
export function fetchCreateQcMaterial(data: Record<string, any>) {
return request.post<QcMaterial>({
url: '/web/admin/qc-materials',
data
})
}
export function fetchUpdateQcMaterial(id: string, data: Record<string, any>) {
return request.put<QcMaterial>({
url: `/web/admin/qc-materials/${id}`,
data
})
}
export function fetchDeleteQcMaterial(id: string) {
return request.delete<void>({
url: `/web/admin/qc-materials/${id}`
})
}
export function fetchStockInQcMaterial(id: string, data: { qty: number; remark?: string }) {
return request.post<QcMaterial>({
url: `/web/admin/qc-materials/${id}/stock-in`,
data
})
}
+35
View File
@@ -191,6 +191,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'sample-code',
component: '/basic-info/sample-code/index',
meta: { title: '样本编码管理', keepAlive: true }
},
{
id: '40000000000000000000000000000007',
name: 'BasicInfoInsuranceCompany',
path: 'insurance-company',
component: '/basic-info/insurance-company/index',
meta: { title: '保险公司及项目价格', keepAlive: true }
}
]
},
@@ -240,6 +247,20 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'refund-confirm',
component: '/cell-storage/refund-confirm/index',
meta: { title: '退费确认(财务)', keepAlive: true }
},
{
id: '50000000000000000000000000000006',
name: 'CellStorageStorageCert',
path: 'storage-cert',
component: '/cell-storage/storage-cert/index',
meta: { title: '储存证管理', keepAlive: true }
},
{
id: '50000000000000000000000000000008',
name: 'CellStorageFullmoonTodo',
path: 'fullmoon-todo',
component: '/cell-storage/fullmoon-todo/index',
meta: { title: '满月待办管理', keepAlive: true }
}
]
},
@@ -344,6 +365,20 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
keepAlive: true,
menuCode: 'biz:qcTask:list'
}
},
{
id: '70000000000000000000000000000009',
name: 'QualityInspectionQcMaterial',
path: 'qc-material',
component: '/quality-inspection/qc-material/index',
meta: { title: '质检物料库', keepAlive: true, menuCode: 'biz:qcMaterial:list' }
},
{
id: '70000000000000000000000000000010',
name: 'QualityInspectionQcMaterialUsage',
path: 'qc-material-usage',
component: '/quality-inspection/qc-material-usage/index',
meta: { title: '物料变动记录', keepAlive: true, menuCode: 'biz:qcMaterialUsage:list' }
}
]
},
@@ -0,0 +1,259 @@
<!-- 物料变动记录 -->
<template>
<div class="page-container">
<header class="page-head">
<h1>物料变动记录</h1>
<p class="page-desc">查看检测物料的使用与入库变动流水,支持按物料、变动类型和时间范围筛选。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2>变动记录列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="物料筛选">
<el-select
v-model="searchForm.materialId"
placeholder="全部物料"
clearable
filterable
>
<el-option
v-for="m in materialOptions"
:key="m.id"
:label="m.label"
:value="m.id"
/>
</el-select>
</el-form-item>
<el-form-item label="变动类型">
<el-select v-model="searchForm.changeType" placeholder="全部" clearable>
<el-option label="使用" value="use" />
<el-option label="入库" value="in" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按物料、变动类型和时间范围查询变动记录。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-end">
<el-tooltip content="列设置" placement="top">
<el-button class="btn-ghost" circle @click="showColumnSettings = true">
<el-icon><Setting /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="刷新" placement="top">
<el-button class="btn-ghost" circle @click="loadData">
<el-icon><Refresh /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" />
<el-table-column prop="materialName" label="物料名称" min-width="140" />
<el-table-column prop="changeType" label="变动类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag
:type="row.changeType === 'use' ? 'warning' : 'success'"
size="small"
>
{{ row.changeType === 'use' ? '使用' : '入库' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="changeQty" label="变动数量" min-width="120" align="right">
<template #default="{ row }">
<span :class="row.changeQty < 0 ? 'qty-negative' : 'qty-positive'">
{{ row.changeQty < 0 ? row.changeQty : `+${row.changeQty}` }}
</span>
</template>
</el-table-column>
<el-table-column prop="stockAfter" label="变动后库存" min-width="120" align="right" />
<el-table-column prop="qcTaskNo" label="关联任务号" min-width="160">
<template #default="{ row }">
{{ row.qcTaskNo || '-' }}
</template>
</el-table-column>
<el-table-column prop="sourceType" label="来源类型" min-width="120" align="center">
<template #default="{ row }">
<el-tag
:type="getSourceTagType(row.sourceType)"
size="small"
>
{{ getSourceLabel(row.sourceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column prop="createBy" label="操作人" min-width="100" />
<el-table-column prop="createTime" label="变动时间" min-width="170" />
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
</section>
<!-- 列设置抽屉 -->
<el-drawer v-model="showColumnSettings" title="列设置" size="300px">
<p>列设置功能预留</p>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Search, RefreshLeft, Refresh, Setting } from '@element-plus/icons-vue'
import { fetchQcMaterialUsageList } from '@/api/qc-material-usage'
import type { QcMaterialUsage } from '@/api/qc-material-usage'
import { fetchQcMaterialOptions } from '@/api/qc-material'
import type { QcMaterialOption } from '@/api/qc-material'
defineOptions({ name: 'QcMaterialUsage' })
const route = useRoute()
const searchForm = reactive<{
materialId: string
changeType: string
}>({
materialId: '',
changeType: ''
})
const dateRange = ref<[string, string] | null>(null)
const tableData = ref<QcMaterialUsage[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const materialOptions = ref<QcMaterialOption[]>([])
const showColumnSettings = ref(false)
function getSourceTagType(sourceType: string) {
if (sourceType === 'task_complete') return 'primary'
if (sourceType === 'manual_in') return 'success'
return 'info'
}
function getSourceLabel(sourceType: string) {
if (sourceType === 'task_complete') return '任务完成'
if (sourceType === 'manual_in') return '手动入库'
return sourceType || '-'
}
async function loadMaterialOptions() {
try {
const res = await fetchQcMaterialOptions()
materialOptions.value = res || []
} catch {
materialOptions.value = []
}
}
async function loadData() {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
pageSize: pagination.pageSize
}
if (searchForm.materialId) params.materialId = searchForm.materialId
if (searchForm.changeType) params.changeType = searchForm.changeType
if (dateRange.value && dateRange.value[0]) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = await fetchQcMaterialUsageList(params)
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.materialId = ''
searchForm.changeType = ''
dateRange.value = null
pagination.page = 1
loadData()
}
onMounted(async () => {
await loadMaterialOptions()
if (route.query.materialId) {
searchForm.materialId = route.query.materialId as string
}
loadData()
})
</script>
<style scoped lang="scss">
.qty-positive {
color: var(--el-color-success);
font-weight: 500;
}
.qty-negative {
color: var(--el-color-danger);
font-weight: 500;
}
</style>
@@ -0,0 +1,472 @@
<!-- 质检物料库 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>质检物料库</h1>
<p class="page-desc">维护质检物料的编码、名称、规格、库存预警等信息,支持入库操作。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2>物料列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="物料名称/编码" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按物料名称、编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length === 0"
@click="handleStockInFromToolbar"
>
入库
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="right" header-align="center">
<template #default="{ row }">
{{ row.stockQty ?? 0 }}
</template>
</el-table-column>
<el-table-column label="预警库存" min-width="100" align="right" header-align="center">
<template #default="{ row }">
{{ row.warningQty ?? 0 }}
</template>
</el-table-column>
<el-table-column label="库存状态" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag
:type="row.stockQty <= row.warningQty ? 'danger' : 'success'"
size="small"
>
{{ row.stockQty <= row.warningQty ? '库存预警' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" min-width="80" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleStockIn(row)">入库</el-button>
<el-button class="btn-op-item" @click="handleViewUsage(row)">详情</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<!-- 新增/编辑 Dialog -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="material-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="formData.materialCode" placeholder="请输入物料编码" />
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="formData.materialName" placeholder="请输入物料名称" />
</el-form-item>
<el-form-item label="规格型号">
<el-input v-model="formData.specModel" placeholder="请输入规格型号" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="formData.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="预警库存">
<el-input-number
v-model="formData.warningQty"
:min="0"
:precision="0"
style="width: 100%"
placeholder="请输入预警库存"
/>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="formData.status">
<el-radio value="1">启用</el-radio>
<el-radio value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<!-- 入库 Dialog -->
<el-dialog
v-model="stockInVisible"
title="物料入库"
width="480px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="stockInFormRef"
:model="stockInForm"
:rules="stockInRules"
label-position="top"
class="material-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<el-form-item label="物料名称">
<el-input :model-value="stockInForm.materialName" disabled />
</el-form-item>
<el-form-item label="当前库存">
<el-input :model-value="stockInForm.currentStock" disabled />
</el-form-item>
<el-form-item label="入库数量" prop="qty">
<el-input-number
v-model="stockInForm.qty"
:min="0.0001"
:precision="4"
style="width: 100%"
placeholder="请输入入库数量"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="stockInVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleStockInSubmit" :loading="stockInLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check } from '@element-plus/icons-vue'
import {
fetchQcMaterialList,
fetchCreateQcMaterial,
fetchUpdateQcMaterial,
fetchDeleteQcMaterial,
fetchStockInQcMaterial,
} from '@/api/qc-material'
import type { QcMaterial } from '@/api/qc-material'
defineOptions({ name: 'QualityInspectionQcMaterial' })
const searchForm = reactive({ keyword: '', status: '' })
const tableData = ref<QcMaterial[]>([])
const selectedRows = ref<QcMaterial[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// 新增/编辑
const dialogVisible = ref(false)
const dialogTitle = ref('新增物料')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
const formData = reactive({
materialCode: '',
materialName: '',
specModel: '',
unit: '',
warningQty: 0,
status: '1',
remark: ''
})
const formRules = {
materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }]
}
// 入库
const stockInVisible = ref(false)
const stockInLoading = ref(false)
const stockInFormRef = ref()
const stockInTargetId = ref<string | null>(null)
const stockInForm = reactive({
materialName: '',
currentStock: 0,
qty: undefined as number | undefined,
remark: ''
})
const stockInRules = {
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }]
}
const handleSelectionChange = (rows: QcMaterial[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchQcMaterialList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
status: searchForm.status || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.status = ''
pagination.page = 1
loadData()
}
// 新增/编辑
function resetForm() {
formData.materialCode = ''
formData.materialName = ''
formData.specModel = ''
formData.unit = ''
formData.warningQty = 0
formData.status = '1'
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增物料'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: QcMaterial) {
isEdit.value = true
dialogTitle.value = '编辑物料'
editingId.value = row.id
formData.materialCode = row.materialCode || ''
formData.materialName = row.materialName || ''
formData.specModel = row.specModel || ''
formData.unit = row.unit || ''
formData.warningQty = row.warningQty ?? 0
formData.status = row.status || '1'
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
materialCode: formData.materialCode.trim(),
materialName: formData.materialName.trim(),
specModel: formData.specModel?.trim() || undefined,
unit: formData.unit?.trim() || undefined,
warningQty: formData.warningQty,
status: formData.status,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateQcMaterial(editingId.value, payload)
} else {
await fetchCreateQcMaterial(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
// 入库
function openStockInDialog(row: QcMaterial) {
stockInTargetId.value = row.id
stockInForm.materialName = row.materialName || ''
stockInForm.currentStock = row.stockQty ?? 0
stockInForm.qty = undefined
stockInForm.remark = ''
stockInVisible.value = true
stockInFormRef.value?.clearValidate?.()
}
function handleStockIn(row: QcMaterial) {
openStockInDialog(row)
}
function handleStockInFromToolbar() {
if (selectedRows.value.length === 0) return
openStockInDialog(selectedRows.value[0])
}
async function handleStockInSubmit() {
await stockInFormRef.value?.validate?.()
if (!stockInTargetId.value) return
stockInLoading.value = true
try {
await fetchStockInQcMaterial(stockInTargetId.value, {
qty: stockInForm.qty!,
remark: stockInForm.remark?.trim() || undefined
})
ElMessage.success('入库成功')
stockInVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
stockInLoading.value = false
}
}
// 查看变动记录
function handleViewUsage(row: QcMaterial) {
window.location.hash = `/sqxp/quality-inspection/qc-material-usage?materialId=${row.id}`
}
// 删除
async function handleDelete(row: QcMaterial) {
if ((row.stockQty ?? 0) > 0) {
ElMessage.warning('当前库存大于0,无法删除')
return
}
try {
await ElMessageBox.confirm('确定要删除该物料吗?', '提示', { type: 'warning' })
} catch {
return
}
await fetchDeleteQcMaterial(row.id)
ElMessage.success('删除成功')
await loadData()
}
onMounted(() => loadData())
</script>
<style scoped lang="scss">
.material-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">任务列表</h2>
<h2>任务列表</h2>
</div>
<div class="filter-block">
@@ -178,6 +178,43 @@
</el-form-item>
</div>
</div>
<div class="form-section material-usage-section">
<div class="section-title">物料使用(选填)</div>
<div v-for="(mu, idx) in materialUsages" :key="idx" class="material-usage-row">
<el-form-item label="物料" class="mu-field-material">
<el-select
v-model="mu.materialId"
filterable
placeholder="选择物料"
style="width: 100%"
@change="(v: string) => onMaterialSelect(mu, v)"
>
<el-option
v-for="opt in materialOptions"
:key="opt.id"
:label="opt.label"
:value="opt.id"
/>
</el-select>
</el-form-item>
<el-form-item label="使用数量" class="mu-field-qty">
<el-input-number
v-model="mu.qty"
:min="0.0001"
:precision="4"
:step="1"
style="width: 100%"
placeholder="数量"
/>
</el-form-item>
<el-button class="btn-ghost mu-remove-btn" @click="materialUsages.splice(idx, 1)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-button class="btn-ghost" size="small" @click="materialUsages.push({ materialId: '', materialName: '', qty: undefined })">
<el-icon><Plus /></el-icon>添加物料
</el-button>
</div>
<div class="form-section result-conclusion-section">
<div class="section-title">检测结论</div>
<el-form-item label="结论" required>
@@ -186,21 +223,18 @@
<el-radio value="btg">不通过</el-radio>
</el-radio-group>
</el-form-item>
</div>
<div class="form-section result-remark-section">
<div class="section-title">备注</div>
<el-form-item label="备注">
<el-input v-model="resultRemark" type="textarea" :rows="3" placeholder="选填" />
</el-form-item>
</div>
<div class="form-section result-attachment-section">
<ProtocolFinanceConfirmFields
v-model:finance-remark="resultRemark"
v-model:attachments="resultAttachments"
:show-remark="false"
:attachment-label="resultAttachmentLabel"
:upload-folder="resultUploadFolder"
/>
<div class="result-attachment-block">
<ProtocolFinanceConfirmFields
v-model:finance-remark="resultRemark"
v-model:attachments="resultAttachments"
:show-remark="false"
:attachment-label="resultAttachmentLabel"
:upload-folder="resultUploadFolder"
/>
</div>
</div>
</el-form>
<template #footer>
@@ -270,6 +304,7 @@
type QcTaskDetail
} from '@/api/qc-task'
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
import {
taskTypeLabel,
taskStatusLabel,
@@ -298,6 +333,8 @@
const resultRemark = ref('')
const resultConclusion = ref('')
const resultAttachments = ref<FinanceAttachmentItem[]>([])
const materialOptions = ref<QcMaterialOption[]>([])
const materialUsages = ref<Array<{ materialId: string; materialName: string; qty: number | undefined }>>([])
function canFillResult(row: QcTask) {
if (row.taskType === 'sysz') {
@@ -321,6 +358,11 @@
resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
)
function onMaterialSelect(mu: { materialId: string; materialName: string; qty: number | undefined }, id: string) {
const opt = materialOptions.value.find((o) => o.id === id)
mu.materialName = opt ? opt.materialName : ''
}
const viewVisible = ref(false)
const viewDetail = ref<QcTaskDetail | null>(null)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
@@ -580,6 +622,7 @@
resultRemark.value = row.remark || ''
resultConclusion.value = ''
resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
materialUsages.value = []
resultLoading.value = true
try {
const detail = await fetchQcTaskDetail(row.id)
@@ -594,6 +637,11 @@
} finally {
resultLoading.value = false
}
try {
materialOptions.value = await fetchQcMaterialOptions()
} catch {
materialOptions.value = []
}
resultVisible.value = true
}
@@ -612,11 +660,15 @@
}
resultLoading.value = true
try {
const validMaterialUsages = materialUsages.value.filter(
(mu) => mu.materialId && mu.qty && mu.qty > 0
)
const data: {
conclusion: string
results: QcTaskResultItem[]
remark?: string
thirdPartyAttachments?: string
materialUsages?: Array<{ materialId: string; qty: number }>
} = {
conclusion: resultConclusion.value,
remark: resultRemark.value,
@@ -628,6 +680,12 @@
}))
}
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
if (validMaterialUsages.length > 0) {
data.materialUsages = validMaterialUsages.map((mu) => ({
materialId: mu.materialId,
qty: mu.qty!,
}))
}
await fetchCompleteTaskWithResult(resultTaskId.value, data)
ElMessage.success(resultTaskStatus.value === 'ywc' ? '已保存' : '已完成')
resultVisible.value = false
@@ -658,19 +716,61 @@
border-left: 3px solid #10b981;
}
.material-usage-section {
margin-top: 12px;
.material-usage-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
.mu-field-material {
flex: 3;
}
.mu-field-qty {
flex: 1.2;
}
.mu-remove-btn {
margin-top: 28px;
padding: 8px;
}
}
}
.result-conclusion-section {
margin-top: 12px;
}
display: flex;
flex-direction: column;
gap: 16px;
.result-remark-section {
margin-top: 12px;
}
.section-title {
margin-bottom: 0;
}
.result-attachment-section {
margin-top: 12px;
background: #fafafa;
border-radius: 8px;
padding: 20px;
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__label) {
display: inline-flex;
width: 100%;
justify-content: flex-start;
text-align: left;
padding-left: 0 !important;
}
.result-attachment-block {
margin-top: 0;
padding: 0;
background: transparent;
:deep(.protocol-finance-confirm-fields .el-form-item) {
margin-bottom: 0;
}
}
}
.biz-dialog-form {