This commit is contained in:
zlwu committed 2026-05-19 23:12:56 +08:00
1 parent 93cd506a90
commit 3b875ec370
93 files changed
+11514 -2304

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ Full specs: `openspec/changes/zkxw-engineering-standards/specs/*/spec.md`. Below
- **Form field annotations**: Field hints (e.g. "数值越小越靠前") SHALL appear as small text after the label via `<template #label>字段名<span class="form-label-tip">注释</span></template>`, NOT beside the input control. Style class: `.form-label-tip` (12px, `--mint-text-muted`).
- **Attachment upload card layout**: Upload trigger SHALL be a card (120×96px, dashed border `#a7f3d0`) placed inline as the first item in the same flex row as uploaded attachment cards. Hint text below the row. See `openspec/.../ui-conventions/spec.md` "附件上传卡片化布局" for full spec + CSS reference.
- **Sort fields (sort_order/orderNum)**: web-admin 列表、新增弹窗、编辑弹窗中默认不展示排序字段。后续根据功能需要自行添加。
- **Status/conclusion/type fields as Tags**: All status, conclusion, and type fields in table columns and view dialogs SHALL render as `<el-tag>` with semantic colors. Tables: use `<el-tag :type="..." size="small">` in column templates. SystemViewDialog: use `tagType` property on `ViewField`. Semantic colors: `success`=正常/启用/已完成/通过, `warning`=进行中/待确认/已过期, `danger`=异常/已作废/拒绝/不通过, `info`=待执行/待生效/停用/默认, `primary`=特殊分类/来源类型. SHALL NOT use plain text or custom `<span class="tag">`. See `openspec/.../ui-conventions/spec.md` "状态、结论与类型字段使用 Tag 标签展示".
- **Template pages** — new pages SHALL match these exactly:
- Standard list: `src/views/system/unit/index.vue`
- Complex list: `src/views/system/order/index.vue`
+1 -1
View File
@@ -1 +1 @@
11478
7085
+1 -1
View File
@@ -254,7 +254,7 @@ STATIC_MENU_TREE = [
"name": "QualityInspectionQcWorkOrder",
"path": "qc-work-order",
"component": "/quality-inspection/qc-work-order/index",
"meta": {"title": "质检工单", "keepAlive": True, "menuCode": "biz:qcWorkOrder:list"},
"meta": {"title": "入库质检工单", "keepAlive": True, "menuCode": "biz:qcWorkOrder:list"},
},
{
"id": "70000000000000000000000000000008",
+269
View File
@@ -14,6 +14,20 @@ from app.crud import process_category as process_category_svc
from app.crud import output_type as output_type_svc
from app.crud import consumable_pack as consumable_pack_svc
from app.crud import process as process_svc
from app.crud import process_route as route_svc
from app.schemas.process_route import (
ProcessRouteCreateBody,
ProcessRouteUpdateBody,
SetCurrentBody,
StepAddBody,
StepCopyBody,
StepUpdateBody,
StepReorderBody,
StepReagentUpdateBody,
StepConsumableUpdateBody,
StepOutputUpdateBody,
StepDataCollectionUpdateBody,
)
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["生产管理"])
@@ -941,3 +955,258 @@ async def delete_process(
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 8. 工艺路线管理
# ---------------------------------------------------------------------------
@router.get("/process-routes")
async def list_process_routes(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
routeName: Optional[str] = None,
routeCode: Optional[str] = None,
productTypeId: Optional[str] = None,
user: dict = Depends(require_menu_code("production:process-route:list")),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = route_svc.list_routes(
db, page=page, page_size=pageSize,
route_name=routeName, route_code=routeCode,
product_type_id=productTypeId, tenant_no=tenant_no,
)
tp = route_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/process-routes/{route_id}")
async def get_process_route_detail(
route_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
detail = route_svc.get_route_detail(db, route_id)
if not detail:
return error_response(404, "路线不存在")
return success_response(detail)
@router.post("/process-routes")
async def create_process_route(
body: ProcessRouteCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = route_svc.create_route(
db,
route_code=body.route_code,
route_name=body.route_name,
product_type_id=body.product_type_id,
version=body.version,
remark=body.remark,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(route_svc._route_row_to_dict(obj), message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/{route_id}")
async def update_process_route(
route_id: str,
body: ProcessRouteUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
payload = body.model_dump(exclude_unset=True, by_alias=False)
obj = route_svc.update_route(
db, route_id,
user_id=user.get("user_id"),
route_code=payload.get("route_code"),
route_name=payload.get("route_name"),
product_type_id=payload.get("product_type_id"),
version=payload.get("version"),
remark=payload.get("remark"),
)
return success_response(route_svc._route_row_to_dict(obj), message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/set-current")
async def set_current_process_route(
body: SetCurrentBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = route_svc.set_current_route(db, body.route_id, tenant_no)
return success_response(route_svc._route_row_to_dict(obj), message="设置成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/process-routes/{route_id}")
async def delete_process_route(
route_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
route_svc.soft_delete_route(db, route_id)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ----- 步骤操作 -----
@router.post("/process-routes/step/add")
async def add_route_step(
body: StepAddBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
step_data = route_svc.add_step(
db,
route_id=body.route_id,
process_id=body.process_id,
step_order=body.step_order,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(step_data, message="添加成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/process-routes/step/copy")
async def copy_route_step(
body: StepCopyBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
step_data = route_svc.copy_step(
db,
step_id=body.step_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(step_data, message="复制成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/step/update")
async def update_route_step(
body: StepUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
payload = body.model_dump(exclude_unset=True, by_alias=False)
payload.pop("step_id", None)
obj = route_svc.update_step(db, body.step_id, user_id=user.get("user_id"), **payload)
return success_response(route_svc._step_row_to_dict(obj), message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/step/reorder")
async def reorder_route_steps(
body: StepReorderBody,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
route_svc.reorder_steps(db, body.route_id, body.step_ids)
return success_response(None, message="排序成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/process-routes/step/{step_id}")
async def delete_route_step(
step_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
route_svc.soft_delete_step(db, step_id)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ----- 步骤子表 -----
@router.put("/process-routes/step/reagent/update")
async def update_step_reagents(
body: StepReagentUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items_data = [it.model_dump(by_alias=False) for it in body.items]
route_svc.update_step_reagents(db, body.step_id, items_data, tenant_no, user.get("user_id"))
return success_response(None, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/step/consumable/update")
async def update_step_consumables(
body: StepConsumableUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items_data = [it.model_dump(by_alias=False) for it in body.items]
route_svc.update_step_consumables(db, body.step_id, items_data, tenant_no, user.get("user_id"))
return success_response(None, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/step/output/update")
async def update_step_outputs(
body: StepOutputUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items_data = [it.model_dump(by_alias=False) for it in body.items]
route_svc.update_step_outputs(db, body.step_id, items_data, tenant_no, user.get("user_id"))
return success_response(None, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/process-routes/step/data-collection/update")
async def update_step_data_collections(
body: StepDataCollectionUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items_data = [it.model_dump(by_alias=False) for it in body.items]
route_svc.update_step_data_collections(db, body.step_id, items_data, tenant_no, user.get("user_id"))
return success_response(None, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
+23 -2
View File
@@ -369,7 +369,9 @@ class TaskResultBody(BaseModel):
class TaskCompleteWithResultBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
conclusion: str = Field(..., alias="conclusion")
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")
@@ -624,6 +626,7 @@ async def list_qc_tasks(
taskType: Optional[str] = None,
taskStatus: Optional[str] = None,
workOrderId: Optional[str] = None,
qcBizType: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:qcTask:list")),
):
@@ -637,6 +640,7 @@ async def list_qc_tasks(
task_type=taskType,
task_status=taskStatus,
work_order_id=workOrderId,
qc_biz_type=qcBizType,
tenant_no=tenant_no,
)
return success_response(_page_payload(items, total, page, pageSize))
@@ -727,15 +731,32 @@ async def complete_task_with_result(
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
if not t:
return error_response("任务不存在")
was_done = (t.task_status or "").strip() == "ywc"
if body.results:
result_svc.save_results_for_task(db, task_id, body.results, tenant_no, user_id)
concl = (body.conclusion or "").strip()
if concl not in ("tg", "btg"):
return error_response("请选择检测结论(通过/不通过)")
if body.third_party_attachments is not None:
task_svc.update_task_attachments(
db, task_id, attachments=body.third_party_attachments,
tenant_no=tenant_no, user_id=user_id,
)
obj = task_svc.complete_task(db, task_id, tenant_no=tenant_no, user_id=user_id)
return success_response(task_svc._task_to_dict(obj), message="已完成")
obj = task_svc.apply_task_result(
db,
task_id,
tenant_no=tenant_no,
user_id=user_id,
conclusion=concl,
remark=body.remark,
)
return success_response(
task_svc._task_to_dict(obj, work_order_no="", db=db, tenant_no=tenant_no),
message="已保存" if was_done else "已完成",
)
except PermissionError as e:
return error_response(str(e), code=403)
except ValueError as e:
+13 -12
View File
@@ -200,13 +200,15 @@ async def list_pending_samples(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
keyword: Optional[str] = None,
sourceType: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:sampleReception:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = sr_svc.list_pending_protocol_samples(
db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword
db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
source_type=sourceType,
)
return success_response(_page_payload(items, total, page, pageSize))
except PermissionError as e:
@@ -220,22 +222,17 @@ async def list_received_samples(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
keyword: Optional[str] = None,
sourceType: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:sampleReception:list")),
):
try:
tenant_no = _get_tenant_no(db, user)
query = db.query(BizSampleReception).filter(
BizSampleReception.del_flag == 0,
BizSampleReception.tenant_no == tenant_no,
items, total = sr_svc.list_received_protocol_samples(
db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
source_type=sourceType,
)
if keyword:
query = query.filter(BizSampleReception.reception_no.like(f"%{keyword}%"))
query = apply_data_scope(query, BizSampleReception, user, db, tenant_no)
total = query.count()
items = query.order_by(BizSampleReception.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
result_list = [sr_svc.get_reception_by_id(db, r.id, tenant_no) for r in items]
return success_response(_page_payload(result_list, total, page, pageSize))
return success_response(_page_payload(items, total, page, pageSize))
except PermissionError as e:
return error_response(str(e), code=403)
except Exception as e:
@@ -281,6 +278,8 @@ class SampleReceptionCreateBody(BaseModel):
accept_conclusion: str = Field(..., alias="acceptConclusion")
receive_attachments: Optional[str] = Field(None, alias="receiveAttachments")
remark: Optional[str] = None
source_type: Optional[str] = Field("cell_storage", alias="sourceType")
donation_sample_id: Optional[str] = Field(None, alias="donationSampleId")
@router.post("/sample-receptions")
@@ -302,7 +301,9 @@ async def post_sample_reception(
]
obj = sr_svc.create_sample_reception(
db,
protocol_sample_id=body.protocol_sample_id,
reception_source=body.source_type or "cell_storage",
protocol_sample_id=body.protocol_sample_id if body.source_type != "research_donation" else None,
donation_sample_id=body.donation_sample_id if body.source_type == "research_donation" else None,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=dept_id,
+7
View File
@@ -752,11 +752,17 @@ def get_protocol_detail_bundle(
reception_by_sample = sample_reception_svc.get_receptions_map_for_samples(
db, sample_ids, tenant_no
)
from app.crud import qc_work_order as qc_work_order_svc
qc_by_sample = qc_work_order_svc.get_qc_info_map_for_samples(
db, sample_ids, tenant_no
)
samples_out: List[Dict[str, Any]] = []
for s in s_rows:
item = _sample_to_dict(s)
item["transport"] = transport_by_sample.get(s.id)
item["sampleReception"] = reception_by_sample.get(s.id)
item["qcInfo"] = qc_by_sample.get(s.id)
samples_out.append(item)
out["samples"] = samples_out
out["storageCert"] = _storage_cert_to_dict(sc) if sc else None
@@ -1849,6 +1855,7 @@ def save_sample_transports(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
sample_id=sample_obj.id,
sample_code=sample_obj.sample_code or None,
transport_type=transport_type,
express_company_id=(tnorm.get("express_company_id") or "").strip() or None,
waybill_no=(tnorm.get("waybill_no") or "").strip() or None,
+2 -1
View File
@@ -375,7 +375,8 @@ def update_process(
new_code = process_code.strip()
if not new_code:
raise ValueError("工艺编码不能为空")
if new_code != obj.process_code and process_code_exists(db, new_code, obj.tenant_no, exclude_id=process_id):
old_code = (obj.process_code or "").strip()
if new_code != old_code and process_code_exists(db, new_code, obj.tenant_no, exclude_id=process_id):
raise ValueError("工艺编码已存在")
obj.process_code = new_code
if process_name is not None:
+610
View File
@@ -0,0 +1,610 @@
"""工艺路线 CRUD(biz_process_route + biz_process_route_step + 4张步骤子表)"""
from __future__ import annotations
import math
from datetime import datetime
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 (
BizProcess,
BizProcessReagent,
BizProcessConsumable,
BizProcessOutput,
BizProcessDataCollection,
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepReagent,
BizProcessRouteStepConsumable,
BizProcessRouteStepOutput,
BizProcessRouteStepDataCollection,
)
# ---------------------------------------------------------------------------
# 序列化函数
# ---------------------------------------------------------------------------
def _route_row_to_dict(r: BizProcessRoute) -> Dict[str, Any]:
return {
"id": r.id,
"routeCode": r.route_code,
"routeName": r.route_name,
"productTypeId": r.product_type_id or "",
"version": r.version or "",
"isCurrent": r.is_current if r.is_current is not None else "0",
"tenantNo": r.tenant_no,
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark 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 _step_row_to_dict(s: BizProcessRouteStep) -> Dict[str, Any]:
return {
"id": s.id,
"routeId": s.route_id,
"processId": s.process_id or "",
"stepOrder": s.step_order if s.step_order is not None else 0,
"processCode": s.process_code or "",
"categoryId": s.category_id or "",
"processName": s.process_name or "",
"description": s.description or "",
"equipmentName": s.equipment_name or "",
"estimatedDuration": s.estimated_duration or "",
"sopIds": s.sop_ids or "",
"remark": s.remark or "",
}
def _step_reagent_row_to_dict(r: BizProcessRouteStepReagent) -> Dict[str, Any]:
return {
"id": r.id,
"stepId": r.step_id,
"reagentCode": r.reagent_code or "",
"reagentName": r.reagent_name or "",
"spec": r.spec or "",
"standardUsage": r.standard_usage or "",
"unit": r.unit or "",
"isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
"remark": r.remark or "",
}
def _step_consumable_row_to_dict(c: BizProcessRouteStepConsumable) -> Dict[str, Any]:
return {
"id": c.id,
"stepId": c.step_id,
"consumableCode": c.consumable_code or "",
"consumableName": c.consumable_name or "",
"specModel": c.spec_model or "",
"standardUsage": c.standard_usage or "",
"unit": c.unit or "",
"isRequired": c.is_required if c.is_required is not None else "1",
"remark": c.remark or "",
}
def _step_output_row_to_dict(o: BizProcessRouteStepOutput) -> Dict[str, Any]:
return {
"id": o.id,
"stepId": o.step_id,
"outputTypeId": o.output_type_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"remark": o.remark or "",
}
def _step_data_collection_row_to_dict(d: BizProcessRouteStepDataCollection) -> Dict[str, Any]:
return {
"id": d.id,
"stepId": d.step_id,
"collectionName": d.collection_name or "",
"unit": d.unit or "",
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
}
# ---------------------------------------------------------------------------
# 路线主表 CRUD
# ---------------------------------------------------------------------------
def list_routes(db: Session, *, page: int = 1, page_size: int = 10,
route_name: Optional[str] = None, route_code: Optional[str] = None,
product_type_id: Optional[str] = None,
tenant_no: Optional[str] = None) -> Tuple[List[Dict], int]:
def _filtered():
q = db.query(BizProcessRoute).filter(BizProcessRoute.del_flag == 0)
if tenant_no:
q = q.filter(BizProcessRoute.tenant_no == tenant_no)
if route_name and route_name.strip():
q = q.filter(BizProcessRoute.route_name.like(f"%{route_name.strip()}%"))
if route_code and route_code.strip():
q = q.filter(BizProcessRoute.route_code == route_code.strip())
if product_type_id and product_type_id.strip():
q = q.filter(BizProcessRoute.product_type_id == product_type_id.strip())
return q
total = _filtered().order_by(None).with_entities(func.count(BizProcessRoute.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(
func.coalesce(BizProcessRoute.order_num, 999999).asc(),
BizProcessRoute.create_time.desc(),
)
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [_route_row_to_dict(x) for x in rows], int(total)
def get_route(db: Session, route_id: str) -> Optional[BizProcessRoute]:
return db.query(BizProcessRoute).filter(
and_(BizProcessRoute.id == route_id, BizProcessRoute.del_flag == 0)
).first()
def get_route_detail(db: Session, route_id: str) -> Optional[Dict[str, Any]]:
route = get_route(db, route_id)
if not route:
return None
result = _route_row_to_dict(route)
steps = (
db.query(BizProcessRouteStep)
.filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
.order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
.all()
)
step_list = []
for s in steps:
sd = _step_row_to_dict(s)
reagents = db.query(BizProcessRouteStepReagent).filter(
and_(BizProcessRouteStepReagent.step_id == s.id, BizProcessRouteStepReagent.del_flag == 0)
).order_by(BizProcessRouteStepReagent.create_time.asc()).all()
consumables = db.query(BizProcessRouteStepConsumable).filter(
and_(BizProcessRouteStepConsumable.step_id == s.id, BizProcessRouteStepConsumable.del_flag == 0)
).order_by(BizProcessRouteStepConsumable.create_time.asc()).all()
outputs = db.query(BizProcessRouteStepOutput).filter(
and_(BizProcessRouteStepOutput.step_id == s.id, BizProcessRouteStepOutput.del_flag == 0)
).order_by(BizProcessRouteStepOutput.create_time.asc()).all()
data_collections = db.query(BizProcessRouteStepDataCollection).filter(
and_(BizProcessRouteStepDataCollection.step_id == s.id, BizProcessRouteStepDataCollection.del_flag == 0)
).order_by(BizProcessRouteStepDataCollection.order_num.asc()).all()
sd["reagents"] = [_step_reagent_row_to_dict(r) for r in reagents]
sd["consumables"] = [_step_consumable_row_to_dict(c) for c in consumables]
sd["outputs"] = [_step_output_row_to_dict(o) for o in outputs]
sd["dataCollections"] = [_step_data_collection_row_to_dict(d) for d in data_collections]
step_list.append(sd)
result["steps"] = step_list
return result
def create_route(db: Session, *, route_code: str, route_name: str,
product_type_id: Optional[str] = None, version: Optional[str] = None,
remark: Optional[str] = None, tenant_no: str = "zkxw",
user_id: Optional[str] = None) -> BizProcessRoute:
rc = (route_code or "").strip()
if not rc:
raise ValueError("路线编码不能为空")
now = datetime.now()
obj = BizProcessRoute(
id=generate_uuid(),
route_code=rc,
route_name=(route_name or "").strip() or rc,
product_type_id=(product_type_id or "").strip() or None,
version=(version or "").strip() or None,
is_current="0",
tenant_no=tenant_no,
order_num=0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_route(db: Session, route_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRoute:
obj = get_route(db, route_id)
if not obj:
raise ValueError("路线不存在")
for field in ["route_code", "route_name", "product_type_id", "version", "remark"]:
val = kwargs.get(field)
if val is not None:
setattr(obj, field, (val or "").strip() or None if field != "route_code" else (val or "").strip())
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def set_current_route(db: Session, route_id: str, tenant_no: str) -> BizProcessRoute:
obj = get_route(db, route_id)
if not obj:
raise ValueError("路线不存在")
now = datetime.now()
siblings = db.query(BizProcessRoute).filter(
and_(BizProcessRoute.route_code == obj.route_code,
BizProcessRoute.tenant_no == tenant_no,
BizProcessRoute.del_flag == 0)
).all()
for s in siblings:
s.is_current = "0"
s.update_time = now
db.add(s)
obj.is_current = "1"
obj.update_time = now
db.add(obj)
db.commit()
db.refresh(obj)
return obj
# ---------------------------------------------------------------------------
# 步骤子表辅助
# ---------------------------------------------------------------------------
def _soft_delete_sub_records(db: Session, model_class, step_id: str, now: datetime) -> None:
rows = db.query(model_class).filter(
and_(model_class.step_id == step_id, model_class.del_flag == 0)
).all()
for r in rows:
r.del_flag = 1
r.update_time = now
db.add(r)
def _create_step_reagent(db, step_id, tenant_no, user_id, now, data):
db.add(BizProcessRouteStepReagent(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
reagent_code=(data.get("reagent_code") or "").strip() or None,
reagent_name=(data.get("reagent_name") or "").strip() or None,
spec=(data.get("spec") or "").strip() or None,
standard_usage=(data.get("standard_usage") or "").strip() or None,
unit=(data.get("unit") or "").strip() or None,
is_key_material=data.get("is_key_material", "0"),
remark=(data.get("remark") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _create_step_consumable(db, step_id, tenant_no, user_id, now, data):
db.add(BizProcessRouteStepConsumable(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
consumable_code=(data.get("consumable_code") or "").strip() or None,
consumable_name=(data.get("consumable_name") or "").strip() or None,
spec_model=(data.get("spec_model") or "").strip() or None,
standard_usage=(data.get("standard_usage") or "").strip() or None,
unit=(data.get("unit") or "").strip() or None,
is_required=data.get("is_required", "1"),
remark=(data.get("remark") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _create_step_output(db, step_id, tenant_no, user_id, now, data):
db.add(BizProcessRouteStepOutput(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
output_type_id=(data.get("output_type_id") or "").strip() or None,
output_name=(data.get("output_name") or "").strip() or None,
output_barcode=(data.get("output_barcode") or "").strip() or None,
remark=(data.get("remark") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _create_step_data_collection(db, step_id, tenant_no, user_id, now, data):
db.add(BizProcessRouteStepDataCollection(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
collection_name=(data.get("collection_name") or "").strip() or None,
unit=(data.get("unit") or "").strip() or None,
order_num=data.get("order_num", 0) or 0,
remark=(data.get("remark") or "").strip() or None,
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# ---------------------------------------------------------------------------
# 步骤 CRUD
# ---------------------------------------------------------------------------
def add_step(db: Session, *, route_id: str, process_id: str, step_order: Optional[int] = None,
tenant_no: str = "zkxw", user_id: Optional[str] = None) -> Dict[str, Any]:
route = get_route(db, route_id)
if not route:
raise ValueError("路线不存在")
proc = db.query(BizProcess).filter(
and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
).first()
if not proc:
raise ValueError("工艺不存在")
now = datetime.now()
if step_order is None:
max_order = db.query(func.max(BizProcessRouteStep.step_order)).filter(
and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0)
).scalar()
step_order = (max_order or 0) + 1
step = BizProcessRouteStep(
id=generate_uuid(), route_id=route_id, process_id=process_id,
step_order=step_order,
process_code=proc.process_code, category_id=proc.category_id,
process_name=proc.process_name, description=proc.description,
equipment_name=proc.equipment_name, estimated_duration=proc.estimated_duration,
sop_ids=proc.sop_ids,
tenant_no=tenant_no, order_num=0,
remark=None, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(step)
db.flush()
src_reagents = db.query(BizProcessReagent).filter(
and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
).all()
for r in src_reagents:
_create_step_reagent(db, step.id, tenant_no, user_id, now, {
"reagent_code": r.reagent_code, "reagent_name": r.reagent_name,
"spec": r.spec, "standard_usage": r.standard_usage,
"unit": r.unit, "is_key_material": r.is_key_material,
})
src_consumables = db.query(BizProcessConsumable).filter(
and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
).all()
for c in src_consumables:
_create_step_consumable(db, step.id, tenant_no, user_id, now, {
"consumable_code": c.consumable_code, "consumable_name": c.consumable_name,
"spec_model": c.spec_model, "standard_usage": c.standard_usage,
"unit": c.unit, "is_required": c.is_required, "remark": c.remark,
})
src_outputs = db.query(BizProcessOutput).filter(
and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
).all()
for o in src_outputs:
_create_step_output(db, step.id, tenant_no, user_id, now, {
"output_type_id": o.output_type_id, "output_name": o.output_name,
"output_barcode": o.output_barcode,
})
src_collections = db.query(BizProcessDataCollection).filter(
and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
).all()
for d in src_collections:
_create_step_data_collection(db, step.id, tenant_no, user_id, now, {
"collection_name": d.collection_name, "unit": d.unit, "order_num": d.order_num,
})
db.commit()
detail = get_route_detail(db, route_id)
return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(step)
def update_step(db: Session, step_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRouteStep:
step = db.query(BizProcessRouteStep).filter(
and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
).first()
if not step:
raise ValueError("步骤不存在")
for field in ["process_code", "category_id", "process_name", "description",
"equipment_name", "estimated_duration", "sop_ids"]:
val = kwargs.get(field)
if val is not None:
setattr(step, field, (val or "").strip() or None)
step.update_by = user_id
step.update_time = datetime.now()
db.add(step)
db.commit()
db.refresh(step)
return step
def reorder_steps(db: Session, route_id: str, step_ids: List[str]) -> None:
now = datetime.now()
for idx, sid in enumerate(step_ids):
step = db.query(BizProcessRouteStep).filter(
and_(BizProcessRouteStep.id == sid, BizProcessRouteStep.route_id == route_id,
BizProcessRouteStep.del_flag == 0)
).first()
if step:
step.step_order = idx + 1
step.update_time = now
db.add(step)
db.commit()
def copy_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
user_id: Optional[str] = None) -> Dict[str, Any]:
"""复制步骤,插入到原步骤之后,名称追加(复制)"""
src = db.query(BizProcessRouteStep).filter(
and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
).first()
if not src:
raise ValueError("步骤不存在")
now = datetime.now()
new_order = src.step_order + 1
# 后续步骤 step_order +1
later_steps = db.query(BizProcessRouteStep).filter(
and_(
BizProcessRouteStep.route_id == src.route_id,
BizProcessRouteStep.del_flag == 0,
BizProcessRouteStep.step_order > src.step_order,
)
).order_by(BizProcessRouteStep.step_order.asc()).all()
for ls in later_steps:
ls.step_order += 1
ls.update_time = now
db.add(ls)
# 创建新步骤
new_name = (src.process_name or "").strip()
if not new_name.endswith("(复制)"):
new_name += "(复制)"
new_step = BizProcessRouteStep(
id=generate_uuid(), route_id=src.route_id, process_id=src.process_id,
step_order=new_order,
process_code=src.process_code, category_id=src.category_id,
process_name=new_name, description=src.description,
equipment_name=src.equipment_name, estimated_duration=src.estimated_duration,
sop_ids=src.sop_ids,
tenant_no=tenant_no, order_num=0,
remark=src.remark, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(new_step)
db.flush()
# 复制子表:试剂
for r in db.query(BizProcessRouteStepReagent).filter(
and_(BizProcessRouteStepReagent.step_id == step_id, BizProcessRouteStepReagent.del_flag == 0)
).all():
_create_step_reagent(db, new_step.id, tenant_no, user_id, now, {
"reagent_code": r.reagent_code, "reagent_name": r.reagent_name,
"spec": r.spec, "standard_usage": r.standard_usage,
"unit": r.unit, "is_key_material": r.is_key_material,
"remark": r.remark,
})
# 复制子表:耗材
for c in db.query(BizProcessRouteStepConsumable).filter(
and_(BizProcessRouteStepConsumable.step_id == step_id, BizProcessRouteStepConsumable.del_flag == 0)
).all():
_create_step_consumable(db, new_step.id, tenant_no, user_id, now, {
"consumable_code": c.consumable_code, "consumable_name": c.consumable_name,
"spec_model": c.spec_model, "standard_usage": c.standard_usage,
"unit": c.unit, "is_required": c.is_required,
"remark": c.remark,
})
# 复制子表:产出
for o in db.query(BizProcessRouteStepOutput).filter(
and_(BizProcessRouteStepOutput.step_id == step_id, BizProcessRouteStepOutput.del_flag == 0)
).all():
_create_step_output(db, new_step.id, tenant_no, user_id, now, {
"output_type_id": o.output_type_id, "output_name": o.output_name,
"output_barcode": o.output_barcode, "remark": o.remark,
})
# 复制子表:数据采集
for d in db.query(BizProcessRouteStepDataCollection).filter(
and_(BizProcessRouteStepDataCollection.step_id == step_id, BizProcessRouteStepDataCollection.del_flag == 0)
).all():
_create_step_data_collection(db, new_step.id, tenant_no, user_id, now, {
"collection_name": d.collection_name, "unit": d.unit,
"order_num": d.order_num, "remark": d.remark,
})
db.commit()
detail = get_route_detail(db, src.route_id)
return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(new_step)
def soft_delete_step(db: Session, step_id: str) -> None:
step = db.query(BizProcessRouteStep).filter(
and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
).first()
if not step:
raise ValueError("步骤不存在")
now = datetime.now()
step.del_flag = 1
step.update_time = now
db.add(step)
_soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now)
_soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now)
_soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now)
_soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now)
db.commit()
def soft_delete_route(db: Session, route_id: str) -> None:
route = get_route(db, route_id)
if not route:
raise ValueError("路线不存在")
now = datetime.now()
route.del_flag = 1
route.update_time = now
db.add(route)
steps = db.query(BizProcessRouteStep).filter(
and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0)
).all()
for s in steps:
s.del_flag = 1
s.update_time = now
db.add(s)
_soft_delete_sub_records(db, BizProcessRouteStepReagent, s.id, now)
_soft_delete_sub_records(db, BizProcessRouteStepConsumable, s.id, now)
_soft_delete_sub_records(db, BizProcessRouteStepOutput, s.id, now)
_soft_delete_sub_records(db, BizProcessRouteStepDataCollection, s.id, now)
db.commit()
# ---------------------------------------------------------------------------
# 子表完整替换更新
# ---------------------------------------------------------------------------
def update_step_reagents(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
now = datetime.now()
_soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now)
for it in items:
_create_step_reagent(db, step_id, tenant_no, user_id, now, it)
db.commit()
def update_step_consumables(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
now = datetime.now()
_soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now)
for it in items:
_create_step_consumable(db, step_id, tenant_no, user_id, now, it)
db.commit()
def update_step_outputs(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
now = datetime.now()
_soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now)
for it in items:
_create_step_output(db, step_id, tenant_no, user_id, now, it)
db.commit()
def update_step_data_collections(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
now = datetime.now()
_soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now)
for it in items:
_create_step_data_collection(db, step_id, tenant_no, user_id, now, it)
db.commit()
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))
+205 -17
View File
@@ -10,7 +10,21 @@ from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.crud.collection_protocol import urls_to_attachment_list
from app.models.models import BizQcTask, BizQcWorkOrder
from app.crud import sample_reception as sample_reception_svc
from app.crud.qc_work_order import (
_load_sample_type_name_map,
_qc_biz_type_label,
_reception_source_label,
_sample_type_name_for_reception,
_work_order_qc_biz_type,
)
from app.models.models import (
BizProtocolSample,
BizQcTask,
BizQcWorkOrder,
BizSampleReception,
)
from app.models.research_donation import BizResearchDonationSample
def _dec(v: Any) -> Optional[float]:
@@ -27,6 +41,14 @@ def _gen_task_no(db: Session, work_order: BizQcWorkOrder, seq: int) -> str:
return f"{work_order.work_order_no}-{seq:02d}"
_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
def _conclusion_label(v: Optional[str]) -> str:
key = (v or "").strip()
return _CONCLUSION_LABELS.get(key, key)
def _task_to_dict(
t: BizQcTask,
work_order_no: str = "",
@@ -47,6 +69,8 @@ def _task_to_dict(
"companyName": t.company_name or "",
"feeAmount": _dec(t.fee_amount),
"taskStatus": t.task_status,
"conclusion": t.conclusion or "",
"conclusionLabel": _conclusion_label(t.conclusion),
"thirdPartyAttachments": t.third_party_attachments or "",
"attachmentFiles": urls_to_attachment_list(t.third_party_attachments, db, tn) if db else [],
"completedTime": _fmt_dt(t.completed_time),
@@ -57,6 +81,102 @@ def _task_to_dict(
}
def _task_work_order_context(wo: BizQcWorkOrder, t: BizQcTask) -> Dict[str, Any]:
biz = _work_order_qc_biz_type(wo)
source = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
return {
"receptionId": wo.reception_id,
"protocolNo": wo.protocol_no or "",
"sampleCode": wo.sample_code or t.sample_code or "",
"qcBizType": biz,
"qcBizTypeLabel": _qc_biz_type_label(biz),
"receptionSource": source,
"receptionSourceLabel": _reception_source_label(source),
"receptionTime": "",
"handlerName": "",
"sampleTypeName": "",
"workOrderStatus": wo.work_order_status or "",
}
def _enrich_tasks_reception_fields(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if not items:
return items
rec_ids = [x.get("receptionId") for x in items if x.get("receptionId")]
if not rec_ids:
return items
rec_map = {
r.id: r
for r in db.query(BizSampleReception)
.filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
.all()
}
cell_sample_ids = [
r.protocol_sample_id
for r in rec_map.values()
if (r.reception_source or "cell_storage") != "research_donation" and r.protocol_sample_id
]
donation_sample_ids = [
r.donation_sample_id
for r in rec_map.values()
if (r.reception_source or "") == "research_donation" and r.donation_sample_id
]
protocol_samples: Dict[str, BizProtocolSample] = {}
if cell_sample_ids:
protocol_samples = {
s.id: s
for s in db.query(BizProtocolSample)
.filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
.all()
}
donation_samples: Dict[str, BizResearchDonationSample] = {}
if donation_sample_ids:
donation_samples = {
s.id: s
for s in db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.id.in_(donation_sample_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
}
type_ids = list(
{
s.sample_type_id
for s in list(protocol_samples.values()) + list(donation_samples.values())
if s.sample_type_id
}
)
type_map = _load_sample_type_name_map(db, type_ids)
for item in items:
rec = rec_map.get(item.get("receptionId") or "")
if not rec:
continue
item["receptionTime"] = _fmt_dt(rec.reception_time)
item["handlerName"] = rec.handler_name or ""
item["sampleTypeName"] = _sample_type_name_for_reception(
db,
rec,
type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
return items
def _task_row_to_dict(
t: BizQcTask,
wo: BizQcWorkOrder,
db: Session,
tenant_no: Optional[str],
) -> Dict[str, Any]:
out = _task_to_dict(t, wo.work_order_no or "", db, tenant_no)
out.update(_task_work_order_context(wo, t))
return out
def create_task_for_line(
db: Session,
*,
@@ -79,6 +199,7 @@ def create_task_for_line(
task_no=_gen_task_no(db, work_order, seq),
task_type=task_type,
source_line_id=source_line_id,
sample_code=work_order.sample_code or None,
item_code=item_code,
item_name=item_name,
company_id=company_id,
@@ -108,11 +229,12 @@ def list_tasks(
task_type: Optional[str] = None,
task_status: Optional[str] = None,
work_order_id: Optional[str] = None,
qc_biz_type: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = (
db.query(BizQcTask, BizQcWorkOrder.work_order_no)
db.query(BizQcTask, BizQcWorkOrder)
.join(
BizQcWorkOrder,
and_(
@@ -134,6 +256,8 @@ def list_tasks(
BizQcTask.task_no.like(like),
BizQcTask.item_name.like(like),
BizQcWorkOrder.work_order_no.like(like),
BizQcWorkOrder.sample_code.like(like),
BizQcWorkOrder.protocol_no.like(like),
)
)
tt = (task_type or "").strip()
@@ -142,6 +266,9 @@ def list_tasks(
st = (task_status or "").strip()
if st:
q0 = q0.filter(BizQcTask.task_status == st)
bt = (qc_biz_type or "").strip()
if bt:
q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0
@@ -158,22 +285,25 @@ def list_tasks(
.limit(page_size)
.all()
)
return [_task_to_dict(t, wno or "", db, tenant_no) for t, wno in rows], int(total)
items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows]
return _enrich_tasks_reception_fields(db, items), int(total)
def list_tasks_by_work_order(db: Session, work_order_id: str, tenant_no: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcTask, BizQcWorkOrder.work_order_no)
db.query(BizQcTask, BizQcWorkOrder)
.join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
BizQcWorkOrder.del_flag == 0,
)
.order_by(BizQcTask.order_num.asc())
.all()
)
return [_task_to_dict(t, wno or "", db, tenant_no) for t, wno in rows]
items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows]
return _enrich_tasks_reception_fields(db, items)
def get_task(
@@ -189,15 +319,22 @@ def get_task_detail(db: Session, row_id: str, tenant_no: str) -> Dict[str, Any]:
row = (
db.query(BizQcTask, BizQcWorkOrder)
.join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
.filter(BizQcTask.id == row_id, BizQcTask.tenant_no == tenant_no, BizQcTask.del_flag == 0)
.filter(
BizQcTask.id == row_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
BizQcWorkOrder.del_flag == 0,
)
.first()
)
if not row:
raise ValueError("任务不存在")
t, wo = row
out = _task_to_dict(t, wo.work_order_no or "", db, tenant_no)
out["protocolNo"] = wo.protocol_no or ""
out["sampleCode"] = wo.sample_code or ""
out = _task_row_to_dict(t, wo, db, tenant_no)
out = _enrich_tasks_reception_fields(db, [out])[0]
reception_detail = sample_reception_svc.get_reception_by_id(db, wo.reception_id, tenant_no)
if reception_detail:
out["reception"] = reception_detail
return out
@@ -218,21 +355,37 @@ def start_task(db: Session, row_id: str, *, tenant_no: str, user_id: Optional[st
return t
def complete_task(db: Session, row_id: str, *, tenant_no: str, user_id: Optional[str]) -> BizQcTask:
def _set_task_remark(t: BizQcTask, remark: Optional[str]) -> None:
t.remark = (remark or "").strip() or None
def complete_task(
db: Session,
row_id: str,
*,
tenant_no: str,
user_id: Optional[str],
conclusion: Optional[str] = None,
remark: Optional[str] = None,
) -> BizQcTask:
t = get_task(db, row_id, tenant_no=tenant_no)
if not t:
raise ValueError("任务不存在")
concl = (conclusion or "").strip() if conclusion is not None else ""
if concl and concl not in ("tg", "btg"):
raise ValueError("检测结论无效")
if t.task_type == "sysz":
if t.task_status not in ("dzx", "jxz"):
raise ValueError("当前状态不可完成")
elif t.task_type == "dsfjc":
if not (t.third_party_attachments or "").strip():
raise ValueError("请先上传第三方检测材料")
if t.task_status == "ywc":
raise ValueError("任务已完成")
else:
raise ValueError("任务类型无效")
t.task_status = "ywc"
if concl:
t.conclusion = concl
_set_task_remark(t, remark)
t.completed_time = datetime.now()
t.completed_by = user_id
t.update_by = user_id
@@ -246,6 +399,41 @@ def complete_task(db: Session, row_id: str, *, tenant_no: str, user_id: Optional
return t
def apply_task_result(
db: Session,
row_id: str,
*,
tenant_no: str,
user_id: Optional[str],
conclusion: str,
remark: Optional[str] = None,
) -> BizQcTask:
"""保存检测结果与结论;任务未完成时顺带标记为已完成。"""
t = get_task(db, row_id, tenant_no=tenant_no)
if not t:
raise ValueError("任务不存在")
concl = (conclusion or "").strip()
if concl not in ("tg", "btg"):
raise ValueError("请选择检测结论(通过/不通过)")
if (t.task_status or "").strip() == "ywc":
t.conclusion = concl
_set_task_remark(t, remark if remark is not None else "")
t.update_by = user_id
t.update_time = datetime.now()
db.add(t)
db.commit()
db.refresh(t)
return t
return complete_task(
db,
row_id,
tenant_no=tenant_no,
user_id=user_id,
conclusion=concl,
remark=remark,
)
def update_task_attachments(
db: Session,
row_id: str,
@@ -257,12 +445,12 @@ def update_task_attachments(
t = get_task(db, row_id, tenant_no=tenant_no)
if not t:
raise ValueError("任务不存在")
if t.task_type != "dsfjc":
raise ValueError("仅第三方检测任务可上传材料")
if t.task_status == "ywc":
raise ValueError("任务已完成,不可修改附件")
t.third_party_attachments = (attachments or "").strip() or None
if t.task_status == "dzx" and t.third_party_attachments:
if (
t.task_type == "dsfjc"
and t.task_status == "dzx"
and t.third_party_attachments
):
t.task_status = "jxz"
t.update_by = user_id
t.update_time = datetime.now()
+76 -2
View File
@@ -1,11 +1,14 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.crud import qc_item_param as param_svc
from app.crud.qc_task import get_task
from app.models.models import BizQcWorkOrderLabItem, BizQcWorkOrderThirdItem
from app.models.qc_param import BizQcTaskResult
@@ -21,7 +24,7 @@ def _row_to_dict(r: BizQcTaskResult) -> Dict[str, Any]:
}
def list_results_by_task(
def _list_saved_results_by_task(
db: Session, task_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
rows = (
@@ -37,6 +40,74 @@ def list_results_by_task(
return [_row_to_dict(r) for r in rows]
def _resolve_task_item_ref(db: Session, task) -> Tuple[Optional[str], Optional[str]]:
"""返回 (item_source, item_id) 供 biz_qc_item_param 查询。"""
if task.task_type == "sysz":
line = (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.id == task.source_line_id,
BizQcWorkOrderLabItem.del_flag == 0,
)
.first()
)
if line:
return "lab", line.lab_test_item_id
elif task.task_type == "dsfjc":
line = (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.id == task.source_line_id,
BizQcWorkOrderThirdItem.del_flag == 0,
)
.first()
)
if line:
return "third_party", line.third_party_item_id
return None, None
def _merge_saved_into_template(
templates: List[Dict[str, Any]], saved: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
by_id = {s["paramId"]: s for s in saved if s.get("paramId")}
by_code = {s["paramCode"]: s for s in saved if s.get("paramCode")}
out: List[Dict[str, Any]] = []
for p in templates:
pid = p.get("id") or ""
code = p.get("paramCode") or ""
hit = by_id.get(pid) or (by_code.get(code) if code else None)
out.append(
{
"paramId": pid,
"paramName": p.get("paramName", ""),
"paramCode": code,
"paramUnit": p.get("paramUnit", ""),
"paramType": p.get("paramType", "text"),
"paramValue": (hit.get("paramValue") if hit else "") or "",
"requiredFlag": p.get("requiredFlag", "0"),
}
)
return out
def list_results_by_task(
db: Session, task_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
"""返回任务结果填写表单:项目参数模板 + 已保存值(无模板时仅返回已保存记录)。"""
saved = _list_saved_results_by_task(db, task_id, tenant_no)
task = get_task(db, task_id, tenant_no=tenant_no)
if not task:
return saved
item_source, item_id = _resolve_task_item_ref(db, task)
if not item_source or not item_id:
return saved
templates = param_svc.list_params_by_item(db, item_source, item_id, tenant_no)
if not templates:
return saved
return _merge_saved_into_template(templates, saved)
def save_results_for_task(
db: Session,
task_id: str,
@@ -44,6 +115,9 @@ def save_results_for_task(
tenant_no: str,
user_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
task = get_task(db, task_id, tenant_no=tenant_no)
if not task:
raise ValueError("任务不存在")
now = datetime.now()
db.query(BizQcTaskResult).filter(
BizQcTaskResult.task_id == task_id,
+358 -57
View File
@@ -13,7 +13,11 @@ from app.crud import qc_lab_test_item as lab_item_svc
from app.crud import qc_task as task_svc
from app.crud import qc_third_party_item as third_item_svc
from app.crud.qc_third_party_company import get_company
from app.crud.collection_protocol import urls_to_attachment_list
from app.crud import sample_reception as sample_reception_svc
from app.crud import sample_reception_check_result as check_result_svc
from app.models.models import (
BizCellSampleType,
BizCollectionProtocol,
BizProtocolSample,
BizQcLabTestItem,
@@ -24,6 +28,22 @@ from app.models.models import (
BizQcWorkOrderThirdItem,
BizSampleReception,
)
from app.models.qc_param import BizQcTaskResult
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
_RECEPTION_SOURCE_LABELS = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
}
_QC_BIZ_TYPE_LABELS = {
"inbound": "入库质检",
"outbound": "出库质检",
}
_ACCEPT_CONCLUSION_LABELS = {"js": "接收", "bj": "拒收"}
_WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "ypz": "已配置", "ywc": "已完成"}
_TASK_TYPE_LABELS = {"sysz": "实验室自检", "dsfjc": "第三方检测"}
_TASK_STATUS_LABELS = {"dzx": "待执行", "jxz": "进行中", "ywc": "已完成"}
_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
def _dec(v: Any) -> Optional[float]:
@@ -36,6 +56,126 @@ def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _reception_source_label(source: Optional[str]) -> str:
key = (source or "").strip() or "cell_storage"
return _RECEPTION_SOURCE_LABELS.get(key, key)
def _qc_biz_type_label(biz_type: Optional[str]) -> str:
key = (biz_type or "").strip() or "inbound"
return _QC_BIZ_TYPE_LABELS.get(key, key)
def _work_order_qc_biz_type(wo: BizQcWorkOrder) -> str:
return getattr(wo, "qc_biz_type", None) or "inbound"
def _accept_conclusion_label(v: Optional[str]) -> str:
key = (v or "").strip()
return _ACCEPT_CONCLUSION_LABELS.get(key, key)
def _load_sample_type_name_map(db: Session, type_ids: List[str]) -> Dict[str, str]:
ids = [x for x in type_ids if x]
if not ids:
return {}
return {
t.id: t.type_name or ""
for t in db.query(BizCellSampleType)
.filter(BizCellSampleType.id.in_(ids), BizCellSampleType.del_flag == 0)
.all()
}
def _sample_type_name_for_reception(
db: Session,
rec: BizSampleReception,
type_map: Dict[str, str],
*,
protocol_samples: Dict[str, BizProtocolSample],
donation_samples: Dict[str, BizResearchDonationSample],
) -> str:
if (rec.reception_source or "cell_storage") == "research_donation":
sample = donation_samples.get(rec.donation_sample_id or "")
tid = sample.sample_type_id if sample else ""
else:
sample = protocol_samples.get(rec.protocol_sample_id or "")
tid = sample.sample_type_id if sample else ""
return type_map.get(tid or "", "")
def _enrich_work_order_list_items(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if not items:
return items
rec_ids = [x["receptionId"] for x in items if x.get("receptionId")]
if not rec_ids:
return items
rec_map = {
r.id: r
for r in db.query(BizSampleReception)
.filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
.all()
}
cell_sample_ids = [
r.protocol_sample_id
for r in rec_map.values()
if (r.reception_source or "cell_storage") != "research_donation" and r.protocol_sample_id
]
donation_sample_ids = [
r.donation_sample_id
for r in rec_map.values()
if (r.reception_source or "") == "research_donation" and r.donation_sample_id
]
protocol_samples: Dict[str, BizProtocolSample] = {}
if cell_sample_ids:
protocol_samples = {
s.id: s
for s in db.query(BizProtocolSample)
.filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
.all()
}
donation_samples: Dict[str, BizResearchDonationSample] = {}
if donation_sample_ids:
donation_samples = {
s.id: s
for s in db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.id.in_(donation_sample_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
}
type_ids = list(
{
s.sample_type_id
for s in list(protocol_samples.values()) + list(donation_samples.values())
if s.sample_type_id
}
)
type_map = _load_sample_type_name_map(db, type_ids)
for item in items:
rec = rec_map.get(item.get("receptionId") or "")
source = item.get("receptionSource") or (rec.reception_source if rec else "cell_storage")
item["receptionSourceLabel"] = _reception_source_label(source)
if not rec:
item.setdefault("receptionTime", "")
item.setdefault("handlerName", "")
item.setdefault("sampleTypeName", "")
continue
item["receptionTime"] = _fmt_dt(rec.reception_time)
item["handlerName"] = rec.handler_name or ""
item["sampleTypeName"] = _sample_type_name_for_reception(
db,
rec,
type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
return items
def _gen_daily_no(db: Session, tenant_no: str, prefix: str) -> str:
day = datetime.now().strftime("%Y%m%d")
like = f"{prefix}{day}%"
@@ -59,6 +199,15 @@ def _wo_to_dict(wo: BizQcWorkOrder) -> Dict[str, Any]:
"collectionProtocolId": wo.collection_protocol_id,
"protocolNo": wo.protocol_no or "",
"sampleCode": wo.sample_code or "",
"receptionSource": getattr(wo, "reception_source", "cell_storage") or "cell_storage",
"receptionSourceLabel": _reception_source_label(
getattr(wo, "reception_source", "cell_storage") or "cell_storage"
),
"qcBizType": _work_order_qc_biz_type(wo),
"qcBizTypeLabel": _qc_biz_type_label(_work_order_qc_biz_type(wo)),
"receptionTime": "",
"handlerName": "",
"sampleTypeName": "",
"workOrderStatus": wo.work_order_status,
"hasLab": wo.has_lab == "1",
"hasThirdParty": wo.has_third_party == "1",
@@ -74,7 +223,7 @@ def _wo_to_dict(wo: BizQcWorkOrder) -> Dict[str, Any]:
def create_work_order_from_reception(
db: Session,
*,
reception: BizSampleReception,
reception,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
@@ -93,44 +242,94 @@ def create_work_order_from_reception(
)
if exists:
return exists
protocol_no = ""
p = (
db.query(BizCollectionProtocol)
.filter(
BizCollectionProtocol.id == reception.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
source = getattr(reception, "reception_source", "cell_storage") or "cell_storage"
if source == "research_donation":
protocol_no = ""
d = (
db.query(BizResearchDonation)
.filter(
BizResearchDonation.id == (reception.research_donation_id or ""),
BizResearchDonation.del_flag == 0,
)
.first()
)
if d:
protocol_no = d.donation_no or ""
sample_code = ""
ds = (
db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.id == (reception.donation_sample_id or ""),
BizResearchDonationSample.del_flag == 0,
)
.first()
)
if ds:
sample_code = ds.sample_code or ""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
reception_id=reception.id,
protocol_sample_id=reception.donation_sample_id or "",
collection_protocol_id=reception.research_donation_id or "",
protocol_no=protocol_no or None,
sample_code=sample_code or None,
reception_source="research_donation",
qc_biz_type="inbound",
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
create_by=user_id,
create_dept_id=create_dept_id or "",
update_by=user_id,
create_time=datetime.now(),
update_time=datetime.now(),
del_flag=0,
)
else:
protocol_no = ""
p = (
db.query(BizCollectionProtocol)
.filter(
BizCollectionProtocol.id == reception.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
)
.first()
)
if p:
protocol_no = p.protocol_no or ""
sample_code = ""
s = (
db.query(BizProtocolSample)
.filter(BizProtocolSample.id == reception.protocol_sample_id, BizProtocolSample.del_flag == 0)
.first()
)
if s:
sample_code = s.sample_code or ""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
reception_id=reception.id,
protocol_sample_id=reception.protocol_sample_id,
collection_protocol_id=reception.collection_protocol_id,
protocol_no=protocol_no or None,
sample_code=sample_code or None,
reception_source="cell_storage",
qc_biz_type="inbound",
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
create_by=user_id,
create_dept_id=create_dept_id or "",
update_by=user_id,
create_time=datetime.now(),
update_time=datetime.now(),
del_flag=0,
)
.first()
)
if p:
protocol_no = p.protocol_no or ""
sample_code = ""
s = (
db.query(BizProtocolSample)
.filter(BizProtocolSample.id == reception.protocol_sample_id, BizProtocolSample.del_flag == 0)
.first()
)
if s:
sample_code = s.sample_code or ""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
reception_id=reception.id,
protocol_sample_id=reception.protocol_sample_id,
collection_protocol_id=reception.collection_protocol_id,
protocol_no=protocol_no or None,
sample_code=sample_code or None,
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
create_by=user_id,
create_dept_id=create_dept_id or "",
update_by=user_id,
create_time=datetime.now(),
update_time=datetime.now(),
del_flag=0,
)
db.add(wo)
db.flush()
return wo
@@ -174,7 +373,8 @@ def list_work_orders(
.limit(page_size)
.all()
)
return [_wo_to_dict(x) for x in rows], int(total)
items = [_wo_to_dict(x) for x in rows]
return _enrich_work_order_list_items(db, items), int(total)
def get_work_order(
@@ -231,6 +431,49 @@ def _third_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
]
def get_qc_info_map_for_samples(
db: Session,
sample_ids: List[str],
tenant_no: str,
) -> Dict[str, Dict[str, Any]]:
"""按协议/捐赠样本 ID 批量返回质检工单及检测任务摘要,供协议/捐赠查看弹窗使用。"""
if not sample_ids or not tenant_no:
return {}
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.protocol_sample_id.in_(sample_ids),
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
)
.all()
)
out: Dict[str, Dict[str, Any]] = {}
for wo in wos:
sid = wo.protocol_sample_id
if not sid:
continue
tasks = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
for t in tasks:
tt = (t.get("taskType") or "").strip()
ts = (t.get("taskStatus") or "").strip()
tc = (t.get("conclusion") or "").strip()
t["taskTypeLabel"] = _TASK_TYPE_LABELS.get(tt, tt)
t["taskStatusLabel"] = _TASK_STATUS_LABELS.get(ts, ts)
t["conclusionLabel"] = t.get("conclusionLabel") or _CONCLUSION_LABELS.get(tc, tc or "—")
wo_status = (wo.work_order_status or "").strip()
biz = _work_order_qc_biz_type(wo)
out[sid] = {
**_wo_to_dict(wo),
"workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(wo_status, wo_status),
"qcBizTypeLabel": _qc_biz_type_label(biz),
"labItems": _lab_lines(db, wo.id),
"thirdPartyItems": _third_lines(db, wo.id),
"tasks": tasks,
}
return out
def get_work_order_detail(
db: Session, row_id: str, tenant_no: str
) -> Dict[str, Any]:
@@ -238,22 +481,83 @@ def get_work_order_detail(
if not wo:
raise ValueError("工单不存在")
out = _wo_to_dict(wo)
rec = (
db.query(BizSampleReception)
.filter(BizSampleReception.id == wo.reception_id, BizSampleReception.del_flag == 0)
.first()
enriched = _enrich_work_order_list_items(db, [out])[0]
out.update(
{
"receptionTime": enriched.get("receptionTime", ""),
"handlerName": enriched.get("handlerName", ""),
"sampleTypeName": enriched.get("sampleTypeName", ""),
"receptionSourceLabel": enriched.get("receptionSourceLabel", ""),
}
)
out["reception"] = {
"receptionTime": _fmt_dt(rec.reception_time) if rec else "",
"handlerName": (rec.handler_name if rec else "") or "",
"acceptConclusion": (rec.accept_conclusion if rec else "") or "",
}
reception_detail = sample_reception_svc.get_reception_by_id(db, wo.reception_id, tenant_no)
if reception_detail:
out["reception"] = reception_detail
else:
rec = (
db.query(BizSampleReception)
.filter(BizSampleReception.id == wo.reception_id, BizSampleReception.del_flag == 0)
.first()
)
cr = (
check_result_svc.get_check_results_for_reception(db, rec, tenant_no) if rec else []
)
out["reception"] = {
"receptionTime": _fmt_dt(rec.reception_time) if rec else "",
"handlerName": (rec.handler_name if rec else "") or "",
"acceptConclusion": (rec.accept_conclusion if rec else "") or "",
"acceptConclusionLabel": _accept_conclusion_label(
rec.accept_conclusion if rec else ""
),
"sourceType": wo.reception_source or "cell_storage",
"sourceTypeLabel": _reception_source_label(wo.reception_source),
"sampleTypeName": enriched.get("sampleTypeName", ""),
"checkResults": cr,
"receiveAttachments": (rec.receive_attachments if rec else "") or "",
"receiveAttachmentFiles": urls_to_attachment_list(
rec.receive_attachments if rec else None, db, tenant_no
),
}
out["labItems"] = _lab_lines(db, wo.id)
out["thirdPartyItems"] = _third_lines(db, wo.id)
out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
return out
def _soft_delete_pending_work_order_tasks(
db: Session, work_order_id: str, tenant_no: str
) -> None:
"""调整检测前软删待执行任务;存在进行中/已完成任务时不允许调整。"""
tasks = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.all()
)
if any(t.task_status in ("jxz", "ywc") for t in tasks):
raise ValueError("存在进行中或已完成的质检任务,无法调整检测项目")
if not tasks:
return
now = datetime.now()
task_ids = [t.id for t in tasks]
if task_ids:
db.query(BizQcTaskResult).filter(
BizQcTaskResult.task_id.in_(task_ids),
BizQcTaskResult.tenant_no == tenant_no,
BizQcTaskResult.del_flag == 0,
).update(
{"del_flag": 1, "update_time": now},
synchronize_session="fetch",
)
for t in tasks:
t.del_flag = 1
t.update_time = now
db.add(t)
def configure_work_order(
db: Session,
work_order_id: str,
@@ -269,15 +573,12 @@ def configure_work_order(
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
if wo.work_order_status != "dpz":
raise ValueError("仅待配置工单可配置检测项目")
task_cnt = (
db.query(BizQcTask)
.filter(BizQcTask.work_order_id == wo.id, BizQcTask.del_flag == 0)
.count()
)
if task_cnt:
raise ValueError("工单已存在任务,无法重新配置")
if wo.work_order_status == "ywc":
raise ValueError("工单已完成,无法调整检测项目")
if wo.work_order_status not in ("dpz", "ypz"):
raise ValueError("当前工单状态不可配置检测项目")
if wo.work_order_status == "ypz":
_soft_delete_pending_work_order_tasks(db, wo.id, tenant_no)
if not has_lab and not has_third_party:
raise ValueError("请至少选择一种检测方式")
+17 -1
View File
@@ -588,10 +588,22 @@ def get_donation_detail(
s_rows = list_samples(db, donation_id, tenant_no)
transport_rows = list_sample_transports(db, donation_id, tenant_no)
transport_by_sample = {t.sample_id: _transport_to_dict(t) for t in transport_rows}
from app.crud import sample_reception as sample_reception_svc
from app.crud import qc_work_order as qc_work_order_svc
sample_ids = [s.id for s in s_rows]
reception_by_sample = sample_reception_svc.get_receptions_map_for_donation_samples(
db, sample_ids, tenant_no
)
qc_by_sample = qc_work_order_svc.get_qc_info_map_for_samples(
db, sample_ids, tenant_no
)
samples_out: List[Dict[str, Any]] = []
for s in s_rows:
item = _sample_to_dict(s)
item["transport"] = transport_by_sample.get(s.id)
item["sampleReception"] = reception_by_sample.get(s.id)
item["qcInfo"] = qc_by_sample.get(s.id)
samples_out.append(item)
out["samples"] = samples_out
@@ -1246,6 +1258,7 @@ def convert_from_storage(
id=generate_uuid(),
research_donation_id=did,
sample_id=new_sample.id,
sample_code=ss.sample_code or None,
transport_type=src_t.transport_type,
express_company_id=src_t.express_company_id,
waybill_no=src_t.waybill_no,
@@ -1484,6 +1497,7 @@ def convert_to_storage(
id=generate_uuid(),
collection_protocol_id=pid,
sample_id=new_sample.id,
sample_code=ss.sample_code or None,
transport_type=src_t.transport_type,
express_company_id=src_t.express_company_id,
waybill_no=src_t.waybill_no,
@@ -1686,7 +1700,7 @@ def _create_samples(
# transport
transport_raw = raw.get("transport")
if isinstance(transport_raw, dict):
_create_transport(db, donation_id, obj.id, tenant_no, user_id, transport_raw, idx, now)
_create_transport(db, donation_id, obj.id, tenant_no, user_id, transport_raw, idx, now, sample_code=obj.sample_code)
return result
@@ -1700,6 +1714,7 @@ def _create_transport(
raw: dict,
order_num: int,
now: datetime,
sample_code: Optional[str] = None,
) -> Optional[BizResearchDonationSampleTransport]:
tnorm = _normalize_fields(raw)
transport_type = (tnorm.get("transport_type") or "").strip()
@@ -1720,6 +1735,7 @@ def _create_transport(
id=generate_uuid(),
research_donation_id=donation_id,
sample_id=sample_id,
sample_code=sample_code or None,
transport_type=transport_type,
express_company_id=(tnorm.get("express_company_id") or "").strip() or None,
waybill_no=(tnorm.get("waybill_no") or "").strip() or None,
+404 -50
View File
@@ -20,6 +20,20 @@ from app.models.models import (
BizSampleReception,
SysUser,
)
from app.models.research_donation import (
BizResearchDonation,
BizResearchDonationParty,
BizResearchDonationSample,
BizResearchDonationSampleTransport,
)
# 科研捐赠表默认 general_ci,样本接收等为 unicode_ci,跨表比较需统一
_UNICODE_CI = "utf8mb4_unicode_ci"
def _donation_sample_id_col(col):
"""捐赠样本 ID 与 biz_sample_reception.donation_sample_id 比较时统一排序规则。"""
return col.collate(_UNICODE_CI)
def _fmt_dt(t: Optional[datetime]) -> str:
@@ -90,6 +104,152 @@ def _sample_detail_rows_query(db: Session):
)
def _donation_sample_detail_rows_query(db: Session):
"""捐赠样本 + 捐赠单 + 当事人 + 运输(四表 JOIN)。"""
return (
db.query(
BizResearchDonationSample,
BizResearchDonation,
BizResearchDonationParty,
BizResearchDonationSampleTransport,
)
.select_from(BizResearchDonationSample)
.join(
BizResearchDonation,
and_(
BizResearchDonation.id == BizResearchDonationSample.research_donation_id,
BizResearchDonation.del_flag == 0,
),
)
.outerjoin(
BizResearchDonationParty,
and_(
BizResearchDonationParty.research_donation_id == BizResearchDonation.id,
BizResearchDonationParty.del_flag == 0,
),
)
.outerjoin(
BizResearchDonationSampleTransport,
and_(
BizResearchDonationSampleTransport.sample_id == BizResearchDonationSample.id,
BizResearchDonationSampleTransport.del_flag == 0,
),
)
)
def _donation_sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
"""待接收:捐赠样本且尚无有效接收记录。"""
received_donation_ids = (
db.query(BizSampleReception.donation_sample_id)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
BizSampleReception.reception_source == "research_donation",
)
.subquery()
)
q = _donation_sample_detail_rows_query(db).filter(
BizResearchDonationSample.del_flag == 0,
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonation.tenant_no == tenant_no,
BizResearchDonation.donation_status.in_(["sxz", "ygq"]),
~_donation_sample_id_col(BizResearchDonationSample.id).in_(received_donation_ids),
)
if keyword and keyword.strip():
kw = f"%{keyword.strip()}%"
q = q.filter(
or_(
BizResearchDonation.donation_no.like(kw),
BizResearchDonationSample.sample_code.like(kw),
BizResearchDonationParty.mother_name.like(kw),
BizResearchDonationParty.newborn_name.like(kw),
)
)
return q
def _donation_rows_to_sample_list_items(
db: Session,
rows: list,
tenant_no: str,
*,
reception_map: Optional[Dict[str, BizSampleReception]] = None,
) -> List[Dict[str, Any]]:
"""将捐赠样本行转为统一列表项字典(字段名与细胞存储保持一致)。"""
type_ids = list({s.sample_type_id for s, *_ in rows if s.sample_type_id})
type_map: Dict[str, str] = {}
if type_ids:
for t in (
db.query(BizCellSampleType)
.filter(
BizCellSampleType.id.in_(type_ids),
BizCellSampleType.del_flag == 0,
)
.all()
):
type_map[t.id] = t.type_name or ""
express_ids = list(
{tr.express_company_id for *_, tr in rows if tr and tr.express_company_id}
)
express_map: Dict[str, str] = {}
if express_ids:
for e in (
db.query(BizExpressCompany)
.filter(BizExpressCompany.id.in_(express_ids), BizExpressCompany.del_flag == 0)
.all()
):
express_map[e.id] = e.express_name or ""
reception_ids = (
[reception_map[s.id].id for s, *_ in rows if s.id in reception_map]
if reception_map
else []
)
results_map = (
check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
if reception_ids
else {}
)
items: List[Dict[str, Any]] = []
for sample, donation, party, transport in rows:
item: Dict[str, Any] = {
"protocolSampleId": sample.id,
"collectionProtocolId": donation.id,
"protocolNo": donation.donation_no or "",
"sampleCode": sample.sample_code or "",
"sampleTypeId": sample.sample_type_id or "",
"sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
"collectAmount": _dec(sample.collect_amount),
"motherName": (party.mother_name if party else "") or "",
"newbornName": (party.newborn_name if party else "") or "",
"sourceType": "research_donation",
"sourceTypeLabel": "科研捐赠",
"etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
"transportType": (transport.transport_type if transport else "") or "",
"waybillNo": (transport.waybill_no if transport else "") or "",
"expressCompanyId": (transport.express_company_id if transport else "") or "",
"expressCompanyName": express_map.get(
(transport.express_company_id if transport else "") or "", ""
),
"carrierName": (transport.carrier_name if transport else "") or "",
"carrierPhone": (transport.carrier_phone if transport else "") or "",
}
if reception_map and sample.id in reception_map:
rec = reception_map[sample.id]
cr = results_map.get(rec.id)
if not cr:
cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
item.update(_reception_to_dict(rec, check_results=cr))
item["receiveAttachmentFiles"] = urls_to_attachment_list(
rec.receive_attachments, db, tenant_no
)
items.append(item)
return items
def _sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
"""待接收:协议样本且尚无有效接收记录。"""
received_ids = (
@@ -184,6 +344,9 @@ def _rows_to_sample_list_items(
(transport.express_company_id if transport else "") or "", ""
),
"carrierName": (transport.carrier_name if transport else "") or "",
"carrierPhone": (transport.carrier_phone if transport else "") or "",
"sourceType": "cell_storage",
"sourceTypeLabel": "细胞存储",
}
if reception_map and sample.id in reception_map:
rec = reception_map[sample.id]
@@ -205,24 +368,53 @@ def list_pending_protocol_samples(
page_size: int = 10,
tenant_no: Optional[str] = None,
keyword: Optional[str] = None,
source_type: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q_base = _sample_pending_query(db, tenant_no, keyword)
total = q_base.order_by(None).with_entities(func.count(BizProtocolSample.id)).scalar() or 0
# 收集两种来源的样本
all_items: List[Dict[str, Any]] = []
if not source_type or source_type == "cell_storage":
q_base = _sample_pending_query(db, tenant_no, keyword)
cell_total = q_base.order_by(None).with_entities(func.count(BizProtocolSample.id)).scalar() or 0
cell_rows = (
q_base.order_by(
BizProtocolSampleTransport.eta_time.is_(None),
BizProtocolSampleTransport.eta_time.asc(),
BizProtocolSample.create_time.asc(),
)
.all()
)
all_items.extend(_rows_to_sample_list_items(db, cell_rows, tenant_no))
else:
cell_total = 0
if not source_type or source_type == "research_donation":
q_donation = _donation_sample_pending_query(db, tenant_no, keyword)
donation_total = q_donation.order_by(None).with_entities(func.count(BizResearchDonationSample.id)).scalar() or 0
donation_rows = (
q_donation.order_by(
BizResearchDonationSampleTransport.eta_time.is_(None),
BizResearchDonationSampleTransport.eta_time.asc(),
BizResearchDonationSample.create_time.asc(),
)
.all()
)
all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no))
else:
donation_total = 0
# 统一按 etaTime 排序(空值排后)
all_items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or ""))
total = cell_total + donation_total
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(
BizProtocolSampleTransport.eta_time.is_(None),
BizProtocolSampleTransport.eta_time.asc(),
BizProtocolSample.create_time.asc(),
)
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return _rows_to_sample_list_items(db, rows, tenant_no), int(total)
start = (page - 1) * page_size
page_items = all_items[start:start + page_size]
return page_items, int(total)
def list_received_protocol_samples(
@@ -232,16 +424,24 @@ def list_received_protocol_samples(
page_size: int = 10,
tenant_no: Optional[str] = None,
keyword: Optional[str] = None,
source_type: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
# 查询所有接收记录,按 source_type 过滤
q_rec = db.query(BizSampleReception).filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
)
if source_type:
q_rec = q_rec.filter(BizSampleReception.reception_source == source_type)
# 关键词搜索
if keyword and keyword.strip():
kw = f"%{keyword.strip()}%"
sample_ids_kw = (
# 细胞存储样本关键词
cell_sample_ids_kw = (
_sample_detail_rows_query(db)
.with_entities(BizProtocolSample.id)
.filter(
@@ -257,10 +457,28 @@ def list_received_protocol_samples(
)
.subquery()
)
# 捐赠样本关键词
donation_sample_ids_kw = (
_donation_sample_detail_rows_query(db)
.with_entities(_donation_sample_id_col(BizResearchDonationSample.id))
.filter(
BizResearchDonationSample.del_flag == 0,
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonation.tenant_no == tenant_no,
or_(
BizResearchDonation.donation_no.like(kw),
BizResearchDonationSample.sample_code.like(kw),
BizResearchDonationParty.mother_name.like(kw),
BizResearchDonationParty.newborn_name.like(kw),
),
)
.subquery()
)
q_rec = q_rec.filter(
or_(
BizSampleReception.handler_name.like(kw),
BizSampleReception.protocol_sample_id.in_(sample_ids_kw),
BizSampleReception.protocol_sample_id.in_(cell_sample_ids_kw),
BizSampleReception.donation_sample_id.in_(donation_sample_ids_kw),
)
)
@@ -276,19 +494,38 @@ def list_received_protocol_samples(
if not receptions:
return [], int(total)
sample_ids = [r.protocol_sample_id for r in receptions]
reception_map = {r.protocol_sample_id: r for r in receptions}
# 按 reception_source 分组查询详情
cell_receptions = [r for r in receptions if r.reception_source == "cell_storage"]
donation_receptions = [r for r in receptions if r.reception_source == "research_donation"]
rows = (
_sample_detail_rows_query(db)
.filter(BizProtocolSample.id.in_(sample_ids))
.all()
)
row_by_sample = {row[0].id: row for row in rows}
ordered_rows = [row_by_sample[sid] for sid in sample_ids if sid in row_by_sample]
items = _rows_to_sample_list_items(
db, ordered_rows, tenant_no, reception_map=reception_map
)
items: List[Dict[str, Any]] = []
if cell_receptions:
cell_sample_ids = [r.protocol_sample_id for r in cell_receptions]
cell_reception_map = {r.protocol_sample_id: r for r in cell_receptions}
cell_rows = (
_sample_detail_rows_query(db)
.filter(BizProtocolSample.id.in_(cell_sample_ids))
.all()
)
cell_row_by_sample = {row[0].id: row for row in cell_rows}
cell_ordered = [cell_row_by_sample[sid] for sid in cell_sample_ids if sid in cell_row_by_sample]
items.extend(_rows_to_sample_list_items(db, cell_ordered, tenant_no, reception_map=cell_reception_map))
if donation_receptions:
donation_sample_ids = [r.donation_sample_id for r in donation_receptions]
donation_reception_map = {r.donation_sample_id: r for r in donation_receptions}
donation_rows = (
_donation_sample_detail_rows_query(db)
.filter(_donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids))
.all()
)
donation_row_by_sample = {row[0].id: row for row in donation_rows}
donation_ordered = [donation_row_by_sample[sid] for sid in donation_sample_ids if sid in donation_row_by_sample]
items.extend(_donation_rows_to_sample_list_items(db, donation_ordered, tenant_no, reception_map=donation_reception_map))
# 按 reception_time DESC 排序
items.sort(key=lambda x: x.get("receptionTime") or "", reverse=True)
return items, int(total)
@@ -306,6 +543,36 @@ def get_reception_by_id(
)
if not rec:
return None
if (rec.reception_source or "cell_storage") == "research_donation":
dsid = rec.donation_sample_id or ""
if not dsid:
cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
out = _reception_to_dict(rec, check_results=cr)
out["sourceType"] = "research_donation"
out["sourceTypeLabel"] = "科研捐赠"
out["receiveAttachmentFiles"] = urls_to_attachment_list(
rec.receive_attachments, db, tenant_no
)
return out
rows = (
_donation_sample_detail_rows_query(db)
.filter(_donation_sample_id_col(BizResearchDonationSample.id) == dsid)
.all()
)
if not rows:
cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
out = _reception_to_dict(rec, check_results=cr)
out["sourceType"] = "research_donation"
out["sourceTypeLabel"] = "科研捐赠"
out["receiveAttachmentFiles"] = urls_to_attachment_list(
rec.receive_attachments, db, tenant_no
)
return out
items = _donation_rows_to_sample_list_items(
db, rows, tenant_no, reception_map={dsid: rec}
)
return items[0] if items else None
sample = (
db.query(BizProtocolSample)
.filter(
@@ -342,7 +609,10 @@ def get_reception_by_id(
def create_sample_reception(
db: Session,
*,
protocol_sample_id: str,
reception_source: str = "cell_storage",
protocol_sample_id: Optional[str] = None,
donation_sample_id: Optional[str] = None,
research_donation_id: Optional[str] = None,
tenant_no: str,
user_id: Optional[str],
create_dept_id: str = "",
@@ -357,28 +627,71 @@ def create_sample_reception(
receive_attachments: Optional[str],
remark: Optional[str],
) -> BizSampleReception:
sample = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.id == protocol_sample_id,
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
source = (reception_source or "").strip()
if source not in ("cell_storage", "research_donation"):
raise ValueError("来源类型无效")
# 按来源校验样本并获取关联 ID
if source == "cell_storage":
if not protocol_sample_id:
raise ValueError("细胞存储来源必须传入 protocolSampleId")
sample = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.id == protocol_sample_id,
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
)
.first()
)
.first()
)
if not sample:
raise ValueError("样本不存在")
exists = (
db.query(BizSampleReception)
.filter(
BizSampleReception.protocol_sample_id == protocol_sample_id,
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
if not sample:
raise ValueError("样本不存在")
exists = (
db.query(BizSampleReception)
.filter(
BizSampleReception.protocol_sample_id == protocol_sample_id,
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
)
.first()
)
.first()
)
if exists:
raise ValueError("该样本已登记接收,请勿重复操作")
if exists:
raise ValueError("该样本已登记接收,请勿重复操作")
collection_protocol_id = sample.collection_protocol_id
_donation_sample_id = None
_research_donation_id = None
_sample_code = sample.sample_code or ""
else:
# research_donation
if not donation_sample_id:
raise ValueError("科研捐赠来源必须传入 donationSampleId")
dsample = (
db.query(BizResearchDonationSample)
.filter(
_donation_sample_id_col(BizResearchDonationSample.id) == donation_sample_id,
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonationSample.del_flag == 0,
)
.first()
)
if not dsample:
raise ValueError("捐赠样本不存在")
exists = (
db.query(BizSampleReception)
.filter(
BizSampleReception.donation_sample_id == donation_sample_id,
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
)
.first()
)
if exists:
raise ValueError("该捐赠样本已登记接收,请勿重复操作")
collection_protocol_id = ""
protocol_sample_id = ""
_donation_sample_id = donation_sample_id
_research_donation_id = dsample.research_donation_id
_sample_code = dsample.sample_code or ""
conclusion = (accept_conclusion or "").strip()
if conclusion not in ("js", "bj"):
@@ -392,8 +705,12 @@ def create_sample_reception(
obj = BizSampleReception(
id=generate_uuid(),
protocol_sample_id=protocol_sample_id,
collection_protocol_id=sample.collection_protocol_id,
protocol_sample_id=protocol_sample_id or "",
collection_protocol_id=collection_protocol_id or "",
reception_source=source,
donation_sample_id=_donation_sample_id,
research_donation_id=_research_donation_id,
sample_code=_sample_code or None,
reception_time=reception_time,
handler_user_id=handler_user_id,
handler_name=handler_display or None,
@@ -484,3 +801,40 @@ def get_receptions_map_for_samples(
)
out[rec.protocol_sample_id] = item
return out
def get_receptions_map_for_donation_samples(
db: Session,
sample_ids: List[str],
tenant_no: str,
) -> Dict[str, Dict[str, Any]]:
"""按捐赠样本 ID 批量查询接收记录,供捐赠协议单查看弹窗使用。"""
if not sample_ids or not tenant_no:
return {}
rows = (
db.query(BizSampleReception)
.filter(
BizSampleReception.donation_sample_id.in_(sample_ids),
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
)
.all()
)
if not rows:
return {}
reception_ids = [r.id for r in rows]
results_map = check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
out: Dict[str, Dict[str, Any]] = {}
for rec in rows:
sid = rec.donation_sample_id
if not sid:
continue
cr = results_map.get(rec.id)
if not cr:
cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
item = _reception_to_dict(rec, check_results=cr)
item["receiveAttachmentFiles"] = urls_to_attachment_list(
rec.receive_attachments, db, tenant_no
)
out[sid] = item
return out
+12
View File
@@ -31,6 +31,12 @@ from app.models.models import (
BizProcessConsumable,
BizProcessOutput,
BizProcessDataCollection,
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepReagent,
BizProcessRouteStepConsumable,
BizProcessRouteStepOutput,
BizProcessRouteStepDataCollection,
)
from app.models.research_donation import ( # noqa: F401
BizResearchDonation,
@@ -80,4 +86,10 @@ __all__ = [
"BizProcessConsumable",
"BizProcessOutput",
"BizProcessDataCollection",
"BizProcessRoute",
"BizProcessRouteStep",
"BizProcessRouteStepReagent",
"BizProcessRouteStepConsumable",
"BizProcessRouteStepOutput",
"BizProcessRouteStepDataCollection",
]
+152
View File
@@ -918,6 +918,7 @@ class BizProtocolSampleTransport(Base):
id = Column(String(64), primary_key=True, comment="主键ID")
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
sample_id = Column(String(64), nullable=False, comment="样本ID")
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
transport_type = Column(String(16), nullable=False, comment="kdys快递运输 ryxd人员携带")
express_company_id = Column(String(64), nullable=True, comment="快递公司ID")
waybill_no = Column(String(128), nullable=True, comment="快递单号")
@@ -1047,6 +1048,10 @@ class BizSampleReception(Base):
check_no_stratification = Column(SmallInteger, nullable=False, default=0, comment="无分层")
accept_conclusion = Column(String(16), nullable=False, comment="结论 js接收 bj拒收")
receive_attachments = Column(String(4000), nullable=True, comment="接收附件")
reception_source = Column(String(20), nullable=False, default="cell_storage", comment="来源类型 cell_storage细胞存储 research_donation科研捐赠")
donation_sample_id = Column(String(64), nullable=True, comment="捐赠样本ID(仅科研捐赠)")
research_donation_id = Column(String(64), nullable=True, comment="科研捐赠单ID(仅科研捐赠)")
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注信息")
@@ -1145,6 +1150,13 @@ class BizQcWorkOrder(Base):
collection_protocol_id = Column(String(64), nullable=False, comment="协议单ID")
protocol_no = Column(String(64), nullable=True, comment="协议号快照")
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
reception_source = Column(String(20), nullable=False, default="cell_storage", comment="来源类型 cell_storage/research_donation")
qc_biz_type = Column(
String(20),
nullable=False,
default="inbound",
comment="质检业务类型 inbound入库质检 outbound出库质检",
)
work_order_status = Column(String(16), nullable=False, default="dpz", comment="工单状态")
has_lab = Column(String(1), nullable=False, default="0", comment="含实验室自检")
has_third_party = Column(String(1), nullable=False, default="0", comment="含第三方检测")
@@ -1217,12 +1229,14 @@ class BizQcTask(Base):
task_no = Column(String(32), nullable=False, comment="任务号")
task_type = Column(String(16), nullable=False, comment="sysz实验室 dsfjc第三方")
source_line_id = Column(String(64), nullable=False, comment="工单明细行ID")
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
item_code = Column(String(64), nullable=False, comment="项目编码快照")
item_name = Column(String(200), nullable=False, comment="项目名称快照")
company_id = Column(String(64), nullable=True, comment="第三方公司ID")
company_name = Column(String(200), nullable=True, comment="第三方公司名")
fee_amount = Column(Numeric(18, 2), nullable=True, comment="费用快照")
task_status = Column(String(16), nullable=False, default="dzx", comment="任务状态")
conclusion = Column(String(16), nullable=True, comment="检测结论 tg通过 btg不通过")
third_party_attachments = Column(String(4000), nullable=True, comment="第三方材料")
completed_time = Column(DateTime, nullable=True, comment="完成时间")
completed_by = Column(String(64), nullable=True, comment="完成人")
@@ -1962,4 +1976,142 @@ class BizProcessDataCollection(Base):
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRoute(Base):
"""工艺路线 biz_process_route"""
__tablename__ = "biz_process_route"
id = Column(String(64), primary_key=True, comment="主键ID")
route_code = Column(String(64), nullable=False, comment="路线编码")
route_name = Column(String(100), nullable=False, comment="路线名称")
product_type_id = Column(String(64), nullable=True, comment="产品类型id")
version = Column(String(50), nullable=True, comment="版本号")
is_current = Column(CHAR(1), nullable=True, default="0", comment="是否当前版本(0-否 1-是)")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRouteStep(Base):
"""工艺路线步骤 biz_process_route_step"""
__tablename__ = "biz_process_route_step"
id = Column(String(64), primary_key=True, comment="主键ID")
route_id = Column(String(64), nullable=False, comment="路线id")
process_id = Column(String(64), nullable=True, comment="关联工艺id")
step_order = Column(Integer, nullable=True, comment="步骤顺序")
process_code = Column(String(64), nullable=True, comment="工序代码")
category_id = Column(String(64), nullable=True, comment="工序分类id")
process_name = Column(String(100), nullable=True, comment="工序名称")
description = Column(String(500), nullable=True, comment="描述")
equipment_name = Column(String(200), nullable=True, comment="预计使用设备名称")
estimated_duration = Column(String(100), nullable=True, comment="预估耗时")
sop_ids = Column(String(1000), nullable=True, comment="关联SOP文档id(逗号分隔)")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRouteStepReagent(Base):
"""工艺路线步骤试剂 biz_process_route_step_reagent"""
__tablename__ = "biz_process_route_step_reagent"
id = Column(String(64), primary_key=True, comment="主键ID")
step_id = Column(String(64), nullable=False, comment="步骤id")
reagent_code = Column(String(64), nullable=True, comment="试剂编码")
reagent_name = Column(String(100), nullable=True, comment="试剂名称")
spec = Column(String(200), nullable=True, comment="规格")
standard_usage = Column(String(100), nullable=True, comment="标准用量")
unit = Column(String(64), nullable=True, comment="单位")
is_key_material = Column(CHAR(1), nullable=True, default="0", comment="是否关键物料(0-否 1-是)")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRouteStepConsumable(Base):
"""工艺路线步骤耗材 biz_process_route_step_consumable"""
__tablename__ = "biz_process_route_step_consumable"
id = Column(String(64), primary_key=True, comment="主键ID")
step_id = Column(String(64), nullable=False, comment="步骤id")
consumable_code = Column(String(64), nullable=True, comment="耗材编码")
consumable_name = Column(String(100), nullable=True, comment="耗材名称")
spec_model = Column(String(200), nullable=True, comment="耗材规格型号")
standard_usage = Column(String(100), nullable=True, comment="标准用量")
unit = Column(String(64), nullable=True, comment="单位")
is_required = Column(CHAR(1), nullable=True, default="1", comment="是否必需(1-必须 0-可选)")
remark = Column(String(512), nullable=True, comment="备注")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRouteStepOutput(Base):
"""工艺路线步骤产出物 biz_process_route_step_output"""
__tablename__ = "biz_process_route_step_output"
id = Column(String(64), primary_key=True, comment="主键ID")
step_id = Column(String(64), nullable=False, comment="步骤id")
output_type_id = Column(String(64), nullable=True, comment="产出类型id")
output_name = Column(String(200), nullable=True, comment="产出物名称")
output_barcode = Column(String(200), nullable=True, comment="产出物条码")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizProcessRouteStepDataCollection(Base):
"""工艺路线步骤数据采集项 biz_process_route_step_data_collection"""
__tablename__ = "biz_process_route_step_data_collection"
id = Column(String(64), primary_key=True, comment="主键ID")
step_id = Column(String(64), nullable=False, comment="步骤id")
collection_name = Column(String(100), nullable=True, comment="采集项名称")
unit = Column(String(64), nullable=True, comment="单位")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
from app.models.qc_param import BizQcItemParam, BizQcTaskResult # noqa: E402,F401
+1
View File
@@ -159,6 +159,7 @@ class BizResearchDonationSampleTransport(Base):
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
sample_id = Column(String(64), nullable=False, comment="样本ID")
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
transport_type = Column(String(16), nullable=False, comment="kdys快递运输 ryxd人员携带")
express_company_id = Column(String(64), nullable=True, comment="快递公司ID")
waybill_no = Column(String(128), nullable=True, comment="快递单号")
+122
View File
@@ -0,0 +1,122 @@
"""工艺路线管理 Pydantic Schemas"""
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, List
# Route main table
class ProcessRouteCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
route_code: str = Field(..., alias="routeCode")
route_name: str = Field(..., alias="routeName")
product_type_id: Optional[str] = Field(None, alias="productTypeId")
version: Optional[str] = Field(None, alias="version")
remark: Optional[str] = None
class ProcessRouteUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
route_code: Optional[str] = Field(None, alias="routeCode")
route_name: Optional[str] = Field(None, alias="routeName")
product_type_id: Optional[str] = Field(None, alias="productTypeId")
version: Optional[str] = Field(None, alias="version")
remark: Optional[str] = None
class SetCurrentBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
route_id: str = Field(..., alias="routeId")
# Step operations
class StepAddBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
route_id: str = Field(..., alias="routeId")
process_id: str = Field(..., alias="processId")
step_order: Optional[int] = Field(None, alias="stepOrder")
class StepCopyBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
class StepUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
process_code: Optional[str] = Field(None, alias="processCode")
category_id: Optional[str] = Field(None, alias="categoryId")
process_name: Optional[str] = Field(None, alias="processName")
description: Optional[str] = None
equipment_name: Optional[str] = Field(None, alias="equipmentName")
estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
sop_ids: Optional[str] = Field(None, alias="sopIds")
class StepReorderBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
route_id: str = Field(..., alias="routeId")
step_ids: List[str] = Field(..., alias="stepIds")
# Step sub-table items
class StepReagentItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
reagent_code: Optional[str] = Field(None, alias="reagentCode")
reagent_name: Optional[str] = Field(None, alias="reagentName")
spec: Optional[str] = None
standard_usage: Optional[str] = Field(None, alias="standardUsage")
unit: Optional[str] = None
is_key_material: Optional[str] = Field("0", alias="isKeyMaterial")
remark: Optional[str] = None
class StepConsumableItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
consumable_code: Optional[str] = Field(None, alias="consumableCode")
consumable_name: Optional[str] = Field(None, alias="consumableName")
spec_model: Optional[str] = Field(None, alias="specModel")
standard_usage: Optional[str] = Field(None, alias="standardUsage")
unit: Optional[str] = None
is_required: Optional[str] = Field("1", alias="isRequired")
remark: Optional[str] = None
class StepOutputItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
output_type_id: Optional[str] = Field(None, alias="outputTypeId")
output_name: Optional[str] = Field(None, alias="outputName")
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
remark: Optional[str] = None
class StepDataCollectionItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
collection_name: Optional[str] = Field(None, alias="collectionName")
unit: Optional[str] = None
order_num: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
# Sub-table update bodies (step_id + items[])
class StepReagentUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
items: List[StepReagentItem] = Field(...)
class StepConsumableUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
items: List[StepConsumableItem] = Field(...)
class StepOutputUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
items: List[StepOutputItem] = Field(...)
class StepDataCollectionUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
items: List[StepDataCollectionItem] = Field(...)
@@ -1,21 +0,0 @@
-- 细胞存储采集协议 ⇄ 科研捐赠 转移关联(一对一)
CREATE TABLE IF NOT EXISTS `biz_protocol_donation_convert` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`convert_type` varchar(32) NOT NULL COMMENT 'storage_to_research/research_to_storage',
`protocol_id` varchar(64) NOT NULL COMMENT '采集协议单ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`biz_no` varchar(64) NOT NULL COMMENT '转移完成时共用编号快照',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT 0 COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_pdc_protocol` (`protocol_id`, `del_flag`),
KEY `idx_pdc_donation` (`research_donation_id`, `del_flag`),
KEY `idx_pdc_biz_no` (`tenant_no`, `biz_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='细胞存储协议与科研捐赠转移关联';
-8
View File
@@ -1,8 +0,0 @@
-- 细胞存储管理二级菜单:重命名 + 排序(已执行 zkxw.sql 的环境可单独运行)
-- 顺序:采集协议单录入、采集协议单管理、缴费信息、缴费确认(财务)、退费确认(财务)
UPDATE `sys_menu` SET `menu_name` = '采集协议单录入', `order_num` = 1 WHERE `id` = '50000000000000000000000000000002';
UPDATE `sys_menu` SET `menu_name` = '采集协议单管理', `order_num` = 2 WHERE `id` = '50000000000000000000000000000003';
UPDATE `sys_menu` SET `order_num` = 3 WHERE `id` = '904b5173519411f1944300163e3bdd96';
UPDATE `sys_menu` SET `order_num` = 4 WHERE `id` = '50000000000000000000000000000004';
UPDATE `sys_menu` SET `order_num` = 5 WHERE `id` = '50000000000000000000000000000005';
-49
View File
@@ -1,49 +0,0 @@
-- 体检机构项目套餐(主表 + 明细表)
-- 套餐总价不落库,由明细 project_fee 汇总计算
-- ----------------------------
-- Table structure for biz_checkup_institution_package
-- ----------------------------
DROP TABLE IF EXISTS `biz_checkup_institution_package_item`;
DROP TABLE IF EXISTS `biz_checkup_institution_package`;
CREATE TABLE `biz_checkup_institution_package` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`checkup_institution_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '体检机构ID',
`package_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐名称',
`package_code` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐编码(必填,机构内未删除记录唯一,应用层校验)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_ck_pkg_inst` (`checkup_institution_id`,`del_flag`),
KEY `idx_ck_pkg_code` (`checkup_institution_id`,`package_code`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐';
CREATE TABLE `biz_checkup_institution_package_item` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`package_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐ID',
`checkup_institution_project_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '机构项目ID',
`project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称(快照)',
`project_fee` decimal(18,2) DEFAULT NULL COMMENT '套餐内项目费用(元),可独立于机构项目价',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ck_pkg_item` (`package_id`,`checkup_institution_project_id`),
KEY `idx_ck_pkg_item_pkg` (`package_id`,`del_flag`),
KEY `idx_ck_pkg_item_proj` (`checkup_institution_project_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐明细';
@@ -1,3 +0,0 @@
-- 体检机构项目及价格表:增加状态字段
ALTER TABLE `biz_checkup_institution_project`
ADD COLUMN `status` char(1) NOT NULL DEFAULT '1' COMMENT '1正常 0停用' AFTER `project_fee`;
-297
View File
@@ -1,297 +0,0 @@
-- ============================================================================
-- 生产管理模块 DDL(11张业务表 + 菜单 INSERT)
-- ============================================================================
-- ---------------------------------------------------------------------------
-- 1. 生产耗材 biz_consumable
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`consumable_code` varchar(64) NOT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) NOT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_consumable_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生产耗材';
-- ---------------------------------------------------------------------------
-- 2. 试剂 biz_reagent
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_reagent` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`reagent_code` varchar(64) NOT NULL COMMENT '试剂编码',
`reagent_name` varchar(100) NOT NULL COMMENT '试剂名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_reagent_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='试剂';
-- ---------------------------------------------------------------------------
-- 3. SOP文档 biz_sop_document
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_sop_document` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`sop_code` varchar(64) NOT NULL COMMENT 'SOP编号',
`sop_name` varchar(200) NOT NULL COMMENT 'SOP文档名称',
`version_no` varchar(50) DEFAULT NULL COMMENT '版本号',
`sop_attachments` varchar(2000) DEFAULT NULL COMMENT 'SOP文档附件(逗号分隔URL)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_sop_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SOP文档';
-- ---------------------------------------------------------------------------
-- 4. 工序分类 biz_process_category
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_category` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`category_code` varchar(64) NOT NULL COMMENT '分类编码',
`category_name` varchar(100) NOT NULL COMMENT '分类名称',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pcat_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工序分类';
-- ---------------------------------------------------------------------------
-- 5. 产出类型 biz_output_type
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_output_type` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`type_code` varchar(64) NOT NULL COMMENT '类型编码',
`type_name` varchar(100) NOT NULL COMMENT '类型名称',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_otype_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产出类型';
-- ---------------------------------------------------------------------------
-- 6. 耗材包 biz_consumable_pack
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable_pack` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`pack_code` varchar(64) NOT NULL COMMENT '编号',
`pack_name` varchar(100) NOT NULL COMMENT '名称',
`usage_scene` varchar(200) DEFAULT NULL COMMENT '使用场景',
`pack_type` varchar(20) DEFAULT NULL COMMENT '类型(bzhcb-标准耗材包 zdyhcb-自定义耗材包)',
`status` char(1) DEFAULT '1' COMMENT '状态(1-启用 0-停用)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_cpack_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材包';
-- ---------------------------------------------------------------------------
-- 7. 耗材包明细 biz_consumable_pack_item
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable_pack_item` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`pack_id` varchar(64) NOT NULL COMMENT '耗材包id',
`consumable_code` varchar(64) DEFAULT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) DEFAULT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '耗材规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_cpi_pack` (`pack_id`, `del_flag`),
KEY `idx_cpi_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材包明细';
-- ---------------------------------------------------------------------------
-- 8. 工艺 biz_process
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_code` varchar(64) NOT NULL COMMENT '工序代码',
`category_id` varchar(64) DEFAULT NULL COMMENT '工序分类id',
`process_name` varchar(100) NOT NULL COMMENT '工序名称',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`equipment_name` varchar(200) DEFAULT NULL COMMENT '预计使用设备名称',
`estimated_duration` varchar(100) DEFAULT NULL COMMENT '预估耗时',
`sop_ids` varchar(1000) DEFAULT NULL COMMENT '关联SOP文档id(逗号分隔)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_process_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺';
-- ---------------------------------------------------------------------------
-- 9. 工艺试剂 biz_process_reagent
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_reagent` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`reagent_code` varchar(64) DEFAULT NULL COMMENT '试剂编码',
`reagent_name` varchar(100) DEFAULT NULL COMMENT '试剂名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`is_key_material` char(1) DEFAULT '0' COMMENT '是否关键物料(0-否 1-是)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pr_process` (`process_id`, `del_flag`),
KEY `idx_pr_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺试剂';
-- ---------------------------------------------------------------------------
-- 10. 工艺耗材 biz_process_consumable
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_consumable` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`consumable_code` varchar(64) DEFAULT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) DEFAULT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '耗材规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`is_required` char(1) DEFAULT '1' COMMENT '是否必需(1-必须 0-可选)',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pc_process` (`process_id`, `del_flag`),
KEY `idx_pc_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺耗材';
-- ---------------------------------------------------------------------------
-- 11. 工艺产出物 biz_process_output
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_output` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`output_type_id` varchar(64) DEFAULT NULL COMMENT '产出类型id',
`output_name` varchar(200) DEFAULT NULL COMMENT '产出物名称',
`output_barcode` varchar(200) DEFAULT NULL COMMENT '产出物条码',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_po_process` (`process_id`, `del_flag`),
KEY `idx_po_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺产出物';
-- ============================================================================
-- 菜单数据 — 生产管理(父级 id='2' Web端菜单)
-- ============================================================================
-- 生产管理 目录(parent_id=2 即 Web端菜单根)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000001', '生产管理', '2', 'M', '/production', '/index/index', NULL, 1, '1', '1', 'zkxw', 7, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 耗材管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000002', '耗材管理', '90000000000000000000000000000001', 'C', 'consumable', '/production/consumable/index', 'production:consumable:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 耗材包管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000007', '耗材包管理', '90000000000000000000000000000001', 'C', 'consumable-pack', '/production/consumable-pack/index', 'production:consumable-pack:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 试剂管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000003', '试剂管理', '90000000000000000000000000000001', 'C', 'reagent', '/production/reagent/index', 'production:reagent:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺工序分类
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000005', '工艺工序分类', '90000000000000000000000000000001', 'C', 'process-category', '/production/process-category/index', 'production:process-category:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺产出物类型
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000006', '工艺产出物类型', '90000000000000000000000000000001', 'C', 'output-type', '/production/output-type/index', 'production:output-type:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺库管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000008', '工艺库管理', '90000000000000000000000000000001', 'C', 'process', '/production/process/index', 'production:process:list', 1, '1', '1', 'zkxw', 6, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- SOP文档
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000004', 'SOP文档', '90000000000000000000000000000001', 'C', 'sop', '/production/sop/index', 'production:sop:list', 1, '1', '1', 'zkxw', 7, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
@@ -1,9 +0,0 @@
-- 生产耗材相关表增加「单位」字段(与试剂 unit 一致,数据来自物品单位管理)
ALTER TABLE `biz_consumable`
ADD COLUMN `unit` varchar(64) DEFAULT NULL COMMENT '单位' AFTER `standard_usage`;
ALTER TABLE `biz_consumable_pack_item`
ADD COLUMN `unit` varchar(64) DEFAULT NULL COMMENT '单位' AFTER `standard_usage`;
ALTER TABLE `biz_process_consumable`
ADD COLUMN `unit` varchar(64) DEFAULT NULL COMMENT '单位' AFTER `standard_usage`;
-10
View File
@@ -1,10 +0,0 @@
-- 生产管理二级菜单排序调整(已执行 production.sql 的环境可单独运行)
-- 顺序:耗材管理、耗材包管理、试剂管理、工艺工序分类、工艺产出物类型、工艺库管理、SOP文档
UPDATE `sys_menu` SET `order_num` = 1 WHERE `id` = '90000000000000000000000000000002';
UPDATE `sys_menu` SET `order_num` = 2 WHERE `id` = '90000000000000000000000000000007';
UPDATE `sys_menu` SET `order_num` = 3 WHERE `id` = '90000000000000000000000000000003';
UPDATE `sys_menu` SET `order_num` = 4 WHERE `id` = '90000000000000000000000000000005';
UPDATE `sys_menu` SET `order_num` = 5 WHERE `id` = '90000000000000000000000000000006';
UPDATE `sys_menu` SET `order_num` = 6 WHERE `id` = '90000000000000000000000000000008';
UPDATE `sys_menu` SET `order_num` = 7 WHERE `id` = '90000000000000000000000000000004';
@@ -1,23 +0,0 @@
-- ---------------------------------------------------------------------------
-- 工艺数据采集项 biz_process_data_collection
-- 用途:在工艺库中配置该工艺需要采集的数据信息(名称、单位)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_data_collection` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`collection_name` varchar(200) NOT NULL COMMENT '采集项名称',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pdc_process` (`process_id`, `del_flag`),
KEY `idx_pdc_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺数据采集项';
@@ -1,44 +0,0 @@
-- 检测项目参数模板
CREATE TABLE IF NOT EXISTS `biz_qc_item_param` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`item_source` varchar(16) NOT NULL COMMENT 'lab实验室 third_party第三方',
`item_id` varchar(64) NOT NULL COMMENT '检测项目ID',
`param_name` varchar(200) NOT NULL COMMENT '参数名称',
`param_code` varchar(64) NOT NULL COMMENT '参数编码',
`param_unit` varchar(32) DEFAULT NULL COMMENT '单位',
`param_type` varchar(16) NOT NULL DEFAULT 'text' COMMENT 'number/text/select',
`required_flag` char(1) NOT NULL DEFAULT '0' COMMENT '1必填 0非必填',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_qc_param_item` (`item_source`, `item_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='检测项目参数模板';
-- 质检任务检测结果
CREATE TABLE IF NOT EXISTS `biz_qc_task_result` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`task_id` varchar(64) NOT NULL COMMENT '质检任务ID',
`param_id` varchar(64) DEFAULT NULL COMMENT '参数模板ID',
`param_name` varchar(200) NOT NULL COMMENT '参数名快照',
`param_code` varchar(64) DEFAULT NULL COMMENT '参数编码快照',
`param_unit` varchar(32) DEFAULT NULL COMMENT '单位快照',
`param_type` varchar(16) NOT NULL DEFAULT 'text' COMMENT '类型快照',
`param_value` varchar(500) DEFAULT NULL COMMENT '填写值',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_qc_result_task` (`task_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='质检任务检测结果';
+5
View File
@@ -0,0 +1,5 @@
-- 质检任务:检测结论(通过/不通过)
ALTER TABLE `biz_qc_task`
ADD COLUMN `conclusion` varchar(16) DEFAULT NULL
COMMENT '检测结论 tg通过 btg不通过'
AFTER `task_status`;
@@ -1,6 +0,0 @@
-- 第三方检测公司:新增开票信息字段
ALTER TABLE `biz_qc_third_party_company`
ADD COLUMN `invoice_title` varchar(200) DEFAULT NULL COMMENT '发票抬头' AFTER `address`,
ADD COLUMN `tax_id` varchar(64) DEFAULT NULL COMMENT '纳税人识别号' AFTER `invoice_title`,
ADD COLUMN `bank_name` varchar(200) DEFAULT NULL COMMENT '开户银行' AFTER `tax_id`,
ADD COLUMN `bank_account` varchar(64) DEFAULT NULL COMMENT '银行账号' AFTER `bank_name`;
+11
View File
@@ -0,0 +1,11 @@
-- 质检工单/任务:区分入库质检与出库质检(未来)
ALTER TABLE `biz_qc_work_order`
ADD COLUMN `qc_biz_type` varchar(20) NOT NULL DEFAULT 'inbound'
COMMENT '质检业务类型 inbound入库质检 outbound出库质检'
AFTER `reception_source`;
UPDATE `biz_qc_work_order`
SET `qc_biz_type` = 'inbound',
`update_time` = NOW()
WHERE `del_flag` = 0
AND (`qc_biz_type` IS NULL OR `qc_biz_type` = '');
+6
View File
@@ -0,0 +1,6 @@
-- 质量检测:侧栏菜单「质检工单」更名为「入库质检工单」
UPDATE `sys_menu`
SET `menu_name` = '入库质检工单',
`update_time` = NOW()
WHERE `id` = '70000000000000000000000000000007'
AND `del_flag` = 0;
@@ -0,0 +1,10 @@
-- 科研捐赠相关表排序规则与 biz_sample_reception 等表对齐(utf8mb4_unicode_ci)
-- 修复样本接收待办查询:Illegal mix of collations (utf8mb4_general_ci) and (utf8mb4_unicode_ci)
-- 在已有库执行本脚本一次即可
ALTER TABLE `biz_research_donation` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `biz_research_donation_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `biz_research_donation_collection` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `biz_research_donation_party` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `biz_research_donation_sample` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `biz_research_donation_sample_transport` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,6 +0,0 @@
-- 科研捐赠管理二级菜单名称调整(已执行 research_donation_tables.sql 的环境可单独运行)
-- 捐赠协议单 → 捐赠协议单录入
-- 捐赠协议审核 → 捐赠协议单管理
UPDATE `sys_menu` SET `menu_name` = '捐赠协议单录入', `update_time` = NOW() WHERE `id` = '60000000000000000000000000000002' AND `del_flag` = 0;
UPDATE `sys_menu` SET `menu_name` = '捐赠协议单管理', `update_time` = NOW() WHERE `id` = '60000000000000000000000000000003' AND `del_flag` = 0;
-176
View File
@@ -1,176 +0,0 @@
-- 科研捐赠单主表
CREATE TABLE IF NOT EXISTS `biz_research_donation` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`donation_no` varchar(64) NOT NULL COMMENT '捐赠协议编号',
`donation_status` varchar(16) NOT NULL DEFAULT 'dsx' COMMENT '捐赠状态 dsx待上线 sxz上线中 ygq已过期 yzf已作废 zckc转出库存',
`donation_date` char(8) DEFAULT NULL COMMENT '捐赠日期YYYYMMDD',
`source_type` varchar(20) NOT NULL DEFAULT 'direct' COMMENT '来源类型 direct独立新建 from_storage从细胞存储转来',
`source_protocol_id` varchar(64) DEFAULT NULL COMMENT '来源采集协议单ID(from_storage时关联)',
`consent_form_urls` text DEFAULT NULL COMMENT '知情同意书附件URL(逗号分隔)',
`attachment_urls` text DEFAULT NULL COMMENT '附件URL(逗号分隔)',
`collector_user_id` varchar(64) DEFAULT NULL COMMENT '采集专员用户ID',
`effective_time` datetime DEFAULT NULL COMMENT '生效时间',
`effective_desc` varchar(512) DEFAULT NULL COMMENT '生效描述',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`expire_desc` varchar(512) DEFAULT NULL COMMENT '过期描述',
`void_time` datetime DEFAULT NULL COMMENT '作废时间',
`void_desc` varchar(512) DEFAULT NULL COMMENT '作废描述',
`to_storage_time` datetime DEFAULT NULL COMMENT '转库存时间',
`to_storage_desc` varchar(512) DEFAULT NULL COMMENT '转库存描述',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注信息',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rd_tenant_status` (`tenant_no`, `donation_status`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单主表';
-- 科研捐赠单-审核记录
CREATE TABLE IF NOT EXISTS `biz_research_donation_audit` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`auditor_user_id` varchar(64) DEFAULT NULL COMMENT '审核人',
`audit_remark` varchar(1000) DEFAULT NULL COMMENT '审核备注',
`audit_status` varchar(16) NOT NULL COMMENT '审核状态 shtg审核通过/shth审核退回',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rda_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单-审核记录';
-- 科研捐赠单-当事人客户信息
CREATE TABLE IF NOT EXISTS `biz_research_donation_party` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`customer_id` varchar(64) DEFAULT NULL COMMENT '客户ID',
`mother_name` varchar(100) DEFAULT NULL COMMENT '母亲姓名',
`mother_phone` varchar(32) DEFAULT NULL COMMENT '母亲电话',
`mother_id_card` varchar(32) DEFAULT NULL COMMENT '母亲身份证号',
`mother_birth_ym` char(6) DEFAULT NULL COMMENT '母亲出生年月YYYYMM',
`mother_ethnic` varchar(32) DEFAULT NULL COMMENT '母亲民族',
`mother_address` varchar(512) DEFAULT NULL COMMENT '母亲地址',
`father_name` varchar(100) DEFAULT NULL COMMENT '父亲姓名',
`father_phone` varchar(32) DEFAULT NULL COMMENT '父亲电话',
`father_id_card` varchar(32) DEFAULT NULL COMMENT '父亲身份证号',
`newborn_name` varchar(100) DEFAULT NULL COMMENT '新生儿姓名',
`newborn_id_card` varchar(32) DEFAULT NULL COMMENT '新生儿身份证号',
`newborn_delivery_time` datetime DEFAULT NULL COMMENT '新生儿分娩时间',
`newborn_sex` char(1) DEFAULT NULL COMMENT '新生儿性别',
`delivery_mode` varchar(16) DEFAULT NULL COMMENT '分娩方式',
`birth_order` int(11) DEFAULT NULL COMMENT '出生顺序',
`newborn_weight_gram` int(11) DEFAULT NULL COMMENT '新生儿体重(克)',
`apgar_1min` decimal(4,1) DEFAULT NULL COMMENT 'Apgar 1分钟评分',
`apgar_5min` decimal(4,1) DEFAULT NULL COMMENT 'Apgar 5分钟评分',
`collector_user_id` varchar(64) DEFAULT NULL COMMENT '采集专员用户ID',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdp_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单-当事人客户信息';
-- 科研捐赠单-采集信息
CREATE TABLE IF NOT EXISTS `biz_research_donation_collection` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`region_id` varchar(64) DEFAULT NULL COMMENT '区域ID',
`hospital_id` varchar(64) DEFAULT NULL COMMENT '医院ID',
`medical_staff` varchar(200) DEFAULT NULL COMMENT '医护人员',
`sales_user_id` varchar(64) DEFAULT NULL COMMENT '销售人员用户ID',
`sample_collect_time` datetime DEFAULT NULL COMMENT '样本采集时间',
`maternal_blood_collect_time` datetime DEFAULT NULL COMMENT '母血采集时间',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdc_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单-采集信息';
-- 科研捐赠单-样本信息
CREATE TABLE IF NOT EXISTS `biz_research_donation_sample` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`sample_type_id` varchar(64) DEFAULT NULL COMMENT '样本类型ID',
`sample_code_id` varchar(64) DEFAULT NULL COMMENT '样本编码ID',
`sample_code` varchar(32) DEFAULT NULL COMMENT '样本编码',
`collect_amount` decimal(18,4) DEFAULT NULL COMMENT '采集量',
`unit_id` varchar(64) DEFAULT NULL COMMENT '单位ID',
`storage_years` int(11) DEFAULT NULL COMMENT '存储年限',
`sample_status` varchar(16) DEFAULT NULL COMMENT '样本状态',
`storage_in_date` char(8) DEFAULT NULL COMMENT '入库日期YYYYMMDD',
`detect_start_date` char(8) DEFAULT NULL COMMENT '检测开始日期YYYYMMDD',
`void_date` char(8) DEFAULT NULL COMMENT '作废日期YYYYMMDD',
`storage_expire_date` char(8) DEFAULT NULL COMMENT '存储到期日期YYYYMMDD',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rds_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单-样本信息';
-- 科研捐赠单-样本运输
CREATE TABLE IF NOT EXISTS `biz_research_donation_sample_transport` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠单ID',
`sample_id` varchar(64) NOT NULL COMMENT '样本ID',
`transport_type` varchar(16) NOT NULL COMMENT '运输方式 kdys快递运输 ryxd人员携带',
`express_company_id` varchar(64) DEFAULT NULL COMMENT '快递公司ID',
`waybill_no` varchar(128) DEFAULT NULL COMMENT '快递单号',
`dest_site_id` varchar(64) DEFAULT NULL COMMENT '目的站点ID',
`express_fee` decimal(18,2) DEFAULT NULL COMMENT '快递费用(元)',
`eta_time` datetime DEFAULT NULL COMMENT '预计到达时间',
`carrier_name` varchar(100) DEFAULT NULL COMMENT '携带人员姓名',
`carrier_phone` varchar(32) DEFAULT NULL COMMENT '携带人员联系电话',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdst_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单-样本运输';
-- ============================================================
-- 科研捐赠管理 — sys_menu 菜单数据(ID 与 sqxp.ts 路由 ID 对应)
-- parent_id = '2' 表示挂在 Web 端菜单根节点下
-- ============================================================
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000001', '科研捐赠管理', '2', 'M', '/research-donation', '/index/index', NULL, 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000002', '捐赠协议单录入', '60000000000000000000000000000001', 'C', 'list', '/research-donation/list/index', 'biz:researchDonation:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000003', '捐赠协议单管理', '60000000000000000000000000000001', 'C', 'audit', '/research-donation/audit/index', 'biz:researchDonationAudit:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-155
View File
@@ -1,155 +0,0 @@
-- ============================================================
-- 生产管理模块基础测试数据
-- 涵盖:耗材 → 试剂 → 工序分类 → 产出类型 → 耗材包(含明细)
-- 租户编号:zkxw
-- 执行前若需重复导入,可先执行文末「清理脚本」
-- ============================================================
-- ----------------------------
-- 1. 生产耗材(8 条)
-- ----------------------------
INSERT INTO `biz_consumable`
(`id`, `consumable_code`, `consumable_name`, `spec_model`, `standard_usage`, `unit`, `description`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('prod_cons_001', 'HC001', 'T25 细胞培养瓶', '25cm²', '1', '个', '贴壁细胞常规培养',
'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_002', 'HC002', '15mL 离心管', '无菌', '2', '支', '细胞传代离心分装',
'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_003', 'HC003', '10mL 移液管', '无菌独立包装', '1', '支', '培养基与试剂移液',
'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_004', 'HC004', '0.22μm 针头滤器', '33mm', '1', '个', '培养基除菌过滤',
'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_005', 'HC005', '细胞刮刀', '18cm', '1', '把', '贴壁细胞消化前刮取',
'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_006', 'HC006', '冻存管', '2mL 外旋盖', '6', '支', '细胞冻存分装',
'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_007', 'HC007', '细胞计数板', '血球计数板', '1', '块', '台盼蓝计数用',
'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cons_008', 'HC008', '一次性无菌手套', 'M 码', '1', '双', '操作防护',
'zkxw', 8, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 2. 试剂(6 条)
-- ----------------------------
INSERT INTO `biz_reagent`
(`id`, `reagent_code`, `reagent_name`, `spec`, `standard_usage`, `unit`, `description`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('prod_reag_001', 'SJ001', 'DMEM 高糖培养基', '500mL', '10', 'mL', '基础培养基',
'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_reag_002', 'SJ002', '胎牛血清 FBS', '500mL', '1', 'mL', '培养基添加剂',
'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_reag_003', 'SJ003', '0.25% 胰酶-EDTA', '100mL', '2', 'mL', '细胞消化',
'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_reag_004', 'SJ004', 'PBS 缓冲液', '500mL', '5', 'mL', '细胞洗涤',
'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_reag_005', 'SJ005', '台盼蓝染液', '10mL', '0.1', 'mL', '死活细胞染色计数',
'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_reag_006', 'SJ006', 'DMSO 细胞冻存液', '100mL', '0.5', 'mL', '冻存保护剂',
'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 3. 工艺工序分类(5 条)
-- ----------------------------
INSERT INTO `biz_process_category`
(`id`, `category_code`, `category_name`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('prod_pcat_001', 'GXFL001', '细胞复苏', 'zkxw', 1, '从液氮取出至接种前', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pcat_002', 'GXFL002', '细胞传代', 'zkxw', 2, '贴壁细胞常规扩增', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pcat_003', 'GXFL003', '细胞冻存', 'zkxw', 3, '液氮冻存入库', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pcat_004', 'GXFL004', '收获与制剂', 'zkxw', 4, '上清收获或细胞收集', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pcat_005', 'GXFL005', '质检取样', 'zkxw', 5, '留样与送检取样', NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 4. 工艺产出物类型(5 条)
-- ----------------------------
INSERT INTO `biz_output_type`
(`id`, `type_code`, `type_name`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('prod_otype_001', 'CCLX001', '活细胞', 'zkxw', 1, '传代或扩增后的细胞悬液', NULL, '', NOW(), NULL, NOW(), 0),
('prod_otype_002', 'CCLX002', '冻存细胞', 'zkxw', 2, '液氮冻存管', NULL, '', NOW(), NULL, NOW(), 0),
('prod_otype_003', 'CCLX003', '上清液', 'zkxw', 3, '培养上清产物', NULL, '', NOW(), NULL, NOW(), 0),
('prod_otype_004', 'CCLX004', '留样', 'zkxw', 4, '质检留样管', NULL, '', NOW(), NULL, NOW(), 0),
('prod_otype_005', 'CCLX005', '废料', 'zkxw', 5, '废弃培养物', NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 5. 耗材包(4 条:2 标准 + 2 自定义)
-- ----------------------------
INSERT INTO `biz_consumable_pack`
(`id`, `pack_code`, `pack_name`, `usage_scene`, `pack_type`, `status`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('prod_pack_001', 'HCB001', '细胞传代标准包', '贴壁细胞 T25 传代', 'bzhcb', '1',
'zkxw', 1, '含培养瓶、离心管、移液管等', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pack_002', 'HCB002', '细胞冻存标准包', '液氮冻存分装', 'bzhcb', '1',
'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('prod_pack_003', 'HCB003', '复苏接种自定义包', '复苏后首次接种', 'zdyhcb', '1',
'zkxw', 3, '按项目定制', NULL, '', NOW(), NULL, NOW(), 0),
('prod_pack_004', 'HCB004', '质检取样包(停用示例)', '留样分装', 'zdyhcb', '0',
'zkxw', 4, '测试停用状态', NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 6. 耗材包明细(关联上述耗材包与耗材编码)
-- ----------------------------
INSERT INTO `biz_consumable_pack_item`
(`id`, `pack_id`, `consumable_code`, `consumable_name`, `spec_model`, `standard_usage`, `unit`, `remark`,
`tenant_no`, `order_num`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
-- 细胞传代标准包 HCB001
('prod_cpi_001', 'prod_pack_001', 'HC001', 'T25 细胞培养瓶', '25cm²', '1', '个', NULL,
'zkxw', 1, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_002', 'prod_pack_001', 'HC002', '15mL 离心管', '无菌', '2', '支', NULL,
'zkxw', 2, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_003', 'prod_pack_001', 'HC003', '10mL 移液管', '无菌独立包装', '1', '支', NULL,
'zkxw', 3, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_004', 'prod_pack_001', 'HC005', '细胞刮刀', '18cm', '1', '把', NULL,
'zkxw', 4, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_005', 'prod_pack_001', 'HC008', '一次性无菌手套', 'M 码', '1', '双', NULL,
'zkxw', 5, NULL, '', NOW(), NULL, NOW(), 0),
-- 细胞冻存标准包 HCB002
('prod_cpi_006', 'prod_pack_002', 'HC002', '15mL 离心管', '无菌', '2', '支', NULL,
'zkxw', 1, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_007', 'prod_pack_002', 'HC006', '冻存管', '2mL 外旋盖', '6', '支', NULL,
'zkxw', 2, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_008', 'prod_pack_002', 'HC008', '一次性无菌手套', 'M 码', '1', '双', NULL,
'zkxw', 3, NULL, '', NOW(), NULL, NOW(), 0),
-- 复苏接种自定义包 HCB003
('prod_cpi_009', 'prod_pack_003', 'HC001', 'T25 细胞培养瓶', '25cm²', '2', '个', '双瓶接种',
'zkxw', 1, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_010', 'prod_pack_003', 'HC004', '0.22μm 针头滤器', '33mm', '1', '个', NULL,
'zkxw', 2, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_011', 'prod_pack_003', 'HC007', '细胞计数板', '血球计数板', '1', '块', NULL,
'zkxw', 3, NULL, '', NOW(), NULL, NOW(), 0),
-- 质检取样包 HCB004(停用包,明细仍保留供查看)
('prod_cpi_012', 'prod_pack_004', 'HC002', '15mL 离心管', '无菌', '3', '支', NULL,
'zkxw', 1, NULL, '', NOW(), NULL, NOW(), 0),
('prod_cpi_013', 'prod_pack_004', 'HC006', '冻存管', '2mL 外旋盖', '2', '支', '留样管',
'zkxw', 2, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 清理脚本(重复导入前可选执行,按 id 前缀删除)
-- ============================================================
-- DELETE FROM `biz_consumable_pack_item` WHERE `id` LIKE 'prod_cpi_%';
-- DELETE FROM `biz_consumable_pack` WHERE `id` LIKE 'prod_pack_%';
-- DELETE FROM `biz_output_type` WHERE `id` LIKE 'prod_otype_%';
-- DELETE FROM `biz_process_category` WHERE `id` LIKE 'prod_pcat_%';
-- DELETE FROM `biz_reagent` WHERE `id` LIKE 'prod_reag_%';
-- DELETE FROM `biz_consumable` WHERE `id` LIKE 'prod_cons_%';
-160
View File
@@ -1,160 +0,0 @@
-- ============================================================
-- 质检模块基础测试数据
-- 涵盖:第三方检测公司 → 第三方检测项目 → 实验室自检项目
-- 租户编号:zkxw
-- ============================================================
-- ----------------------------
-- 1. 第三方检测公司(6家)
-- ----------------------------
INSERT INTO `biz_qc_third_party_company`
(`id`, `company_code`, `company_name`, `contact_name`, `contact_phone`, `address`,
`invoice_title`, `tax_id`, `bank_name`, `bank_account`,
`status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('qc_tpc_001', 'DSF001', '安徽中检技术服务有限公司', '张建国', '13912345601',
'合肥市高新区望江西路800号A栋3楼', '安徽中检技术服务有限公司', '91340100MA2N0XYZ01',
'中国建设银行合肥高新区支行', '34001688608053001234',
'1', 'zkxw', 1, '长期合作方,具备CMA/CNAS资质',
NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpc_002', 'DSF002', '上海谱尼测试集团合肥分公司', '李明华', '13912345602',
'合肥市包河区繁华大道6588号', '上海谱尼测试集团有限公司', '9131011578056XYZ02',
'中国工商银行上海浦东支行', '10012029192001234567',
'1', 'zkxw', 2, '可做微生物、重金属检测',
NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpc_003', 'DSF003', '华测检测认证集团股份有限公司', '王晓燕', '13912345603',
'合肥市蜀山区长江西路1899号', '华测检测认证集团股份有限公司', '9144030077163XYZ03',
'招商银行深圳福田支行', '755916238910001',
'1', 'zkxw', 3, NULL,
NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpc_004', 'DSF004', '中检集团安徽有限公司', '赵伟', '13912345604',
'合肥市经开区翠微路12号', '中检集团安徽有限公司', '9134010068888XYZ04',
'中国银行合肥政务区支行', '18723456789001',
'1', 'zkxw', 4, '综合性检测机构',
NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpc_005', 'DSF005', '安徽国科检测科技有限公司', '刘芳', '13912345605',
'合肥市庐阳区濉溪路310号', '安徽国科检测科技有限公司', '91340100MA2UWEXYZ5',
'中国农业银行合肥庐阳支行', '12260101040012345',
'1', 'zkxw', 5, NULL,
NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpc_006', 'DSF006', '合肥微谱分析检测有限公司', '陈志强', '13912345606',
'合肥市高新区科学大道118号', '合肥微谱分析检测有限公司', '91340100MA2N0KXYZ6',
'交通银行合肥高新区支行', '341201000018012345678',
'0', 'zkxw', 6, '已暂停合作,状态停用',
NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 2. 第三方检测项目(每家公司2~3个项目,共14个)
-- ----------------------------
INSERT INTO `biz_qc_third_party_item`
(`id`, `company_id`, `item_code`, `item_name`, `fee_amount`, `status`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
-- ── 安徽中检技术服务有限公司 (DSF001) ──
('qc_tpi_001', 'qc_tpc_001', 'DSF001-001', '细胞活性检测', 800.00, '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_002', 'qc_tpc_001', 'DSF001-002', '微生物限度检查', 1200.00, '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_003', 'qc_tpc_001', 'DSF001-003', '内毒素检测(鲎试剂法)', 1500.00, '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── 上海谱尼测试集团合肥分公司 (DSF002) ──
('qc_tpi_004', 'qc_tpc_002', 'DSF002-001', '重金属含量检测(铅镉汞砷)', 2000.00, '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_005', 'qc_tpc_002', 'DSF002-002', '细胞表型流式分析', 2500.00, '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── 华测检测认证集团股份有限公司 (DSF003) ──
('qc_tpi_006', 'qc_tpc_003', 'DSF003-001', '支原体检测(PCR法)', 600.00, '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_007', 'qc_tpc_003', 'DSF003-002', '无菌检测', 900.00, '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_008', 'qc_tpc_003', 'DSF003-003', '细胞鉴别(STR图谱)', 1800.00, '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── 中检集团安徽有限公司 (DSF004) ──
('qc_tpi_009', 'qc_tpc_004', 'DSF004-001', '乙肝五项检测', 500.00, '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_010', 'qc_tpc_004', 'DSF004-002', 'HIV/HCV/梅毒联合筛查', 800.00, '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── 安徽国科检测科技有限公司 (DSF005) ──
('qc_tpi_011', 'qc_tpc_005', 'DSF005-001', '细胞计数与存活率', 300.00, '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_012', 'qc_tpc_005', 'DSF005-002', '病毒检测(qPCR法)', 1600.00, '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_tpi_013', 'qc_tpc_005', 'DSF005-003', '染色体核型分析', 2200.00, '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── 合肥微谱分析检测有限公司 (DSF006, 停用) ──
('qc_tpi_014', 'qc_tpc_006', 'DSF006-001', '蛋白质含量测定', 700.00, '0', 'zkxw', 1, '公司已暂停合作', NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 3. 实验室自检项目(8个)
-- ----------------------------
INSERT INTO `biz_qc_lab_test_item`
(`id`, `item_code`, `item_name`, `description`, `status`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
('qc_lti_001', 'LAB001', '细胞计数与活力检测',
'使用台盼蓝排斥法或自动细胞计数仪,检测样本中细胞总数及存活率',
'1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_002', 'LAB002', '支原体检测(培养法)',
'采用支原体培养基法,观察14天,检测细胞培养物中是否污染支原体',
'1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_003', 'LAB003', '无菌检测(直接接种法)',
'将样本分别接种于需氧/厌氧培养基,观察14天,判断是否无菌',
'1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_004', 'LAB004', '内毒素检测(凝胶法)',
'使用鲎试剂凝胶法测定样本中的细菌内毒素含量',
'1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_005', 'LAB005', '细胞表型检测(CD标记)',
'采用流式细胞术检测细胞表面标记物,如CD34、CD45、CD73、CD90等',
'1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_006', 'LAB006', '细胞分化能力检测',
'诱导分化后通过染色或流式分析,验证干细胞的成骨/成脂/成软骨分化能力',
'1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_007', 'LAB007', '病原微生物快速筛查',
'采用PCR/RT-PCR方法快速筛查乙肝、丙肝、HIV、梅毒、巨细胞病毒等',
'1', 'zkxw', 7, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_lti_008', 'LAB008', '渗透压检测',
'使用渗透压仪测定细胞冻存液或培养基的渗透压值',
'0', 'zkxw', 8, '暂未开展,保留项目', NULL, '', NOW(), NULL, NOW(), 0);
-- ----------------------------
-- 4. 检测项目参数模板(为部分项目配置参数)
-- ----------------------------
INSERT INTO `biz_qc_item_param`
(`id`, `item_source`, `item_id`, `param_name`, `param_code`, `param_unit`, `param_type`, `required_flag`,
`tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES
-- ── LAB001 细胞计数与活力检测 的参数 ──
('qc_ip_001', 'lab', 'qc_lti_001', '细胞总数', 'total_count', '×10⁶', 'number', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_002', 'lab', 'qc_lti_001', '存活率', 'viability', '%', 'number', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_003', 'lab', 'qc_lti_001', '检测方法', 'method', NULL, 'text', '0', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── LAB004 内毒素检测(凝胶法) 的参数 ──
('qc_ip_004', 'lab', 'qc_lti_004', '内毒素含量', 'endotoxin', 'EU/mL', 'number', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_005', 'lab', 'qc_lti_004', '限值标准', 'limit_std', 'EU/mL', 'number', '0', 'zkxw', 2, '根据剂型不同限值不同', NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_006', 'lab', 'qc_lti_004', '判定结果', 'result', NULL, 'text', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── LAB005 细胞表型检测 的参数 ──
('qc_ip_007', 'lab', 'qc_lti_005', 'CD34阳性率', 'cd34_pos', '%', 'number', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_008', 'lab', 'qc_lti_005', 'CD45阳性率', 'cd45_pos', '%', 'number', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_009', 'lab', 'qc_lti_005', 'CD73阳性率', 'cd73_pos', '%', 'number', '0', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── DSF001-001 细胞活性检测(第三方)的参数 ──
('qc_ip_010', 'third_party', 'qc_tpi_001', '细胞活性率', 'activity_rate', '%', 'number', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_011', 'third_party', 'qc_tpi_001', '检测依据', 'standard', NULL, 'text', '0', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
-- ── DSF002-001 重金属含量检测(第三方)的参数 ──
('qc_ip_012', 'third_party', 'qc_tpi_004', '铅(Pb)', 'pb', 'mg/kg', 'number', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_013', 'third_party', 'qc_tpi_004', '镉(Cd)', 'cd', 'mg/kg', 'number', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_014', 'third_party', 'qc_tpi_004', '汞(Hg)', 'hg', 'mg/kg', 'number', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
('qc_ip_015', 'third_party', 'qc_tpi_004', '砷(As)', 'as', 'mg/kg', 'number', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-25
View File
@@ -1,25 +0,0 @@
-- 中试服务管理菜单 (Web端,parent_id=2 为 Web 端根菜单)
-- 与前端路由 sqxp.ts 中 PilotService 路由块对应
-- 一级菜单目录
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000001', '中试服务管理', '2', 'M', '/pilot-service', NULL, NULL, 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- 二级菜单
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000002', '细胞产品类型管理', '80000000000000000000000000000001', 'C', 'cell-product-type', 'pilot-service/cell-product-type/index', NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000003', '细胞产品规格管理', '80000000000000000000000000000001', 'C', 'cell-product-spec', 'pilot-service/cell-product-spec/index', NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000004', '细胞产品价格体系管理', '80000000000000000000000000000001', 'C', 'cell-product-price', 'pilot-service/cell-product-price/index', NULL, 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000005', '细胞产品项目价格管理', '80000000000000000000000000000001', 'C', 'project-price-system', 'pilot-service/project-price-system/index', NULL, 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000006', '渠道信息管理', '80000000000000000000000000000001', 'C', 'channel', 'pilot-service/channel/index', NULL, 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000007', '体检机构及项目价格', '80000000000000000000000000000001', 'C', 'checkup-institution', 'pilot-service/checkup-institution/index', NULL, 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0);
+1503 -45
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,206 @@
# 状态标签注册表设计
> 日期:2026-05-19
> 状态:已批准
> 范围:web-admin 前端工程所有枚举字段标签集中化管理
## 1. 背景与问题
当前 web-admin 中所有状态/结论/类型字段的标签映射散落在 **21 个 Vue 文件** 中,存在以下问题:
1. **颜色不一致**:同一状态值在表格中用 `el-tag type="primary"`(蓝色),查看弹窗中用自定义 `badge--purple`(紫色)
2. **大量重复**:`protocolStatusLabel` 在 6 个文件中重复定义,`newbornSexLabel` 在 6 个文件中重复定义
3. **tagType 缺失**:仅 1/15 的集中化函数有 tagType 变体,其余需在使用处手写
4. **Badge CSS 残留**:3 个复杂查看弹窗(ProtocolViewDialog、DonationViewDialog、ChannelViewDialog)仍使用自定义 `.badge--*` CSS,与 `el-tag` 并存
## 2. 目标
- 新建一个 `src/utils/status-tag-registry.ts`,集中定义所有枚举字段的 **label + tagType**
- 所有页面和弹窗统一从此文件导入,确保同一状态值在任何地方颜色一致
- 消灭所有自定义 badge CSS 系统,统一为 `el-tag`
- 与现有调用签名完全兼容,零迁移成本
## 3. 方案选择
| 方案 | 描述 | 优点 | 缺点 |
|------|------|------|------|
| **A. 单体注册表(选定)** | 一个 `status-tag-registry.ts` 集中管理所有枚举 | Ctrl+Click 一处看到所有映射,改颜色绝不遗漏 | 文件约 300 行 |
| B. 按域分文件 | 拆成 `qc-status.ts`、`protocol-status.ts` 等 | 每个文件职责单一 | 需记住去哪个文件找,import 路径多 |
| C. 运行时注册 | `registerStatusGroup` + `getStatusLabel` 查找 | 可扩展 | 过度设计,丧失类型安全 |
选定方案 A。
## 4. 数据结构
```typescript
// src/utils/status-tag-registry.ts
/** el-tag 的 type 属性值 */
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
/** 单条枚举映射 */
export interface StatusItem {
label: string
tagType: TagType
}
/** 注册表类型 */
export type StatusRegistry = Record<string, StatusItem>
```
不使用类、不使用运行时注册、不使用 Map。纯静态 `Record`,类型安全、tree-shakeable。
## 5. 便捷函数工厂
```typescript
/** 从注册表提取 label */
export function createLabelGetter(registry: StatusRegistry) {
return (code: string): string => registry[code]?.label ?? code ?? '—'
}
/** 从注册表提取 tagType */
export function createTagTypeGetter(registry: StatusRegistry) {
return (code: string): TagType => registry[code]?.tagType ?? 'info'
}
```
导出示例:
```typescript
export const protocolStatus: StatusRegistry = {
dsx: { label: '待生效', tagType: 'info' },
sxz: { label: '生效中', tagType: 'success' },
ygq: { label: '已过期', tagType: 'warning' },
yzf: { label: '已作废', tagType: 'danger' },
zky: { label: '转科研', tagType: 'primary' },
}
export const protocolStatusLabel = createLabelGetter(protocolStatus)
export const protocolStatusTagType = createTagTypeGetter(protocolStatus)
```
现有代码调用 `protocolStatusLabel('dsx')` 完全不变,零迁移成本。
## 6. 完整注册表分组
### 6.1 协议/捐赠/样本域
| 注册表名 | 枚举值 | 原散落文件数 |
|----------|--------|-------------|
| `protocolStatus` | dsx/sxz/ygq/yzf/zky | 6 |
| `donationStatus` | dsx/sxz/ygq/yzf/zckc | 3 |
| `sampleStatus` | ycj/jcz/yrk/yzf | 3 |
| `payStatus` | dfj/jfqrz/yjf/tfqrz/ytf | 3 |
| `auditStatus` | shtg/shth | 2 |
| `acceptConclusion` | js/bj(jjj) | 2 |
| `transportType` | kdys/ryxd | 5+ |
| `sourceType` | direct/from_storage | 1 |
| `newbornSex` | 1/0 | 6 |
| `deliveryMode` | sc/pgc | 6 |
### 6.2 质检域
| 注册表名 | 枚举值 | 原散落文件数 |
|----------|--------|-------------|
| `qcBizType` | inbound/outbound | 1 |
| `taskStatus` | dzx/jxz/ywc | 1 |
| `taskType` | sysz/dsfjc | 1 |
| `qcConclusion` | tg/btg | 1 |
| `workOrderStatus` | dpz/ypz/ywc | 1 |
| `receptionSource` | cell_storage/research_donation | 1 |
### 6.3 渠道域
| 注册表名 | 枚举值 | 原散落文件数 |
|----------|--------|-------------|
| `channelType` | yy/hzjg/zs/zfjg/qt | 2 |
| `agreementType` | kyxy/zsxy/lcxy | 2 |
| `agreementStatus` | dsx/sxz/yzf | 2 |
| `settlementType` | yj/jj/nj/dcjs | 2 |
### 6.4 支付/通用域
| 注册表名 | 枚举值 | 原散落文件数 |
|----------|--------|-------------|
| `payMode` | dj/fq | 2 |
| `transferType` | wxzz/zzbzz/yhkzz/xj | 5+ |
| `enableStatus` | 1/0(通用启用/停用) | 12+ |
### 6.5 系统域
| 注册表名 | 枚举值 | 原散落文件数 |
|----------|--------|-------------|
| `leaveStatus` | draft/pending/approved/rejected/cancelled | 1 |
| `workflowStatus` | running/completed/rejected/cancelled/pending/approved/withdrawn | 1 |
| `menuType` | M/C/F | 1 |
| `userRole` | admin/supplier/client/deliver | 1 |
| `dataScope` | 1-5 | 1 |
| `cleanRoomGrade` | A/B/C/D | 1 |
| `sampleCodeType` | zxbm/qdbm | 1 |
| `packType` | bzhcb/zdyhcb | 1 |
| `paramType` | text/select/multi_select | 2 |
| `orderStatus` | pending_payment/pending_confirm/completed/cancelled/... | 1 |
| `deliveryStatus` | pending_dispatch/pending_delivery/delivering/delivered/... | 1 |
## 7. 与现有文件的关系
| 现有文件 | 处理方式 |
|---------|---------|
| `utils/qc-labels.ts` | 所有函数迁移到 registry,原文件改为从 registry re-export(过渡期兼容) |
| `utils/protocol-payment-labels.ts` | 标签函数迁移到 registry;`formatPaymentAmount`、`isPaymentDueWithinWarnWindow`、`formatPaymentDueDate` 等纯格式化函数保留原文件 |
| 21 个 Vue 文件中的本地函数 | 删除本地函数,改为从 registry 导入 |
## 8. 消灭自定义 Badge 系统
以下 3 个文件的自定义 `.badge` CSS 和 `status-dot` 全部替换为 `<el-tag>`:
| 文件 | 消灭内容 |
|-----|---------|
| `cell-storage/protocol/ProtocolViewDialog.vue` | `.badge` / `badge--*` → `<el-tag>` |
| `research-donation/list/DonationViewDialog.vue` | `.badge` / `badge--*` / `status-dot` → `<el-tag>` |
| `pilot-service/channel/ChannelViewDialog.vue` | `.badge` / `badge--*` → `<el-tag>` |
| `components/business/sample-qc-info-cards/index.vue` | 内联 badge class → `<el-tag>` |
替换后删除 `<style scoped>` 中的 `.badge` 相关 CSS 定义。
## 9. Tag Type 语义色规范
| TagType | 颜色 | 适用语义 | 示例 |
|---------|------|---------|------|
| `success` | 绿色 | 正常、启用、已完成、生效中、通过、已缴费 | 正常/启用/已完成/接收/通过/已入库 |
| `warning` | 橙色 | 进行中、待确认、已过期、预警 | 进行中/已排配/已过期/待确认/检测中 |
| `danger` | 红色 | 异常、已作废、拒绝、不通过、已退费 | 异常/已作废/拒绝/不通过/已退费 |
| `info` | 灰色 | 待执行、待生效、停用、默认 | 待执行/待生效/停用/无结论/其他 |
| `primary` | 蓝色 | 特殊分类、来源类型、转出 | 转科研/医院/框架协议/已采集 |
| `''` | 默认色 | 无特殊语义 | 目录类型 |
## 10. 迁移策略
分 3 波执行,每波可独立验证:
### 波次 1:核心(质检 + 协议状态 + Badge 消灭)
- 创建 `status-tag-registry.ts`
- 迁移质检域 6 组 + 协议/支付域 4 组
- 消灭 ProtocolViewDialog、DonationViewDialog 的 badge 系统
- 迁移 `qc-labels.ts` 的 re-export
- 涉及约 12 个文件
### 波次 2:捐赠 + 渠道
- 迁移捐赠域 5 组 + 渠道域 4 组
- 消灭 ChannelViewDialog 的 badge 系统
- 涉及约 8 个文件
### 波次 3:系统 + 通用 + 订单
- 迁移通用启用/停用、系统域、订单域等
- 消灭 sample-qc-info-cards 的 badge 系统
- 迁移 `protocol-payment-labels.ts` 标签部分为 re-export
- 涉及约 15 个文件
## 11. 验收标准
1. `status-tag-registry.ts` 覆盖所有 30+ 组枚举映射
2. 项目中不存在自定义 `.badge--*` CSS 类
3. 项目中不存在本地定义的 `xxxStatusLabel` / `xxxTagType` 函数(除 re-export 文件外)
4. 所有状态/结论/类型字段在表格和查看弹窗中颜色完全一致
5. LSP 诊断零错误
6. 现有页面功能无回归
@@ -0,0 +1,244 @@
# 统一样本接收 — 支持科研捐赠来源
> 日期:2026-05-19
> 状态:待审核
> 关联:AGENTS.md(database-conventions、api-conventions、ui-conventions)
## 背景与目标
当前【样本接收】模块仅支持来自**细胞存储(采集协议单)**的样本。业务需要**科研捐赠(捐赠协议单)**的样本也走统一的样本接收流程,接收后自动生成质检工单。
**核心诉求**:
1. 样本接收页面统一展示两种来源的待接收/已接收样本,用类型标签区分
2. 完整展示运输信息(运输方式、快递公司、快递单号、预计到达时间、携带人等),方便接收人员核对
3. 科研捐赠样本接收后,同样触发质检工单创建
## 方案:现有表加字段 + 双路径查询
### 1. 数据库变更
在 `biz_sample_reception` 表新增 3 个字段:
```sql
-- docs/sql/sample_reception_add_donation_source.sql
ALTER TABLE biz_sample_reception
ADD COLUMN reception_source VARCHAR(20) NOT NULL DEFAULT 'cell_storage'
COMMENT '来源类型 cell_storage细胞存储 research_donation科研捐赠'
AFTER receive_attachments,
ADD COLUMN donation_sample_id VARCHAR(64) NULL
COMMENT '捐赠样本ID(biz_research_donation_sample.id, 仅科研捐赠来源)'
AFTER reception_source,
ADD COLUMN research_donation_id VARCHAR(64) NULL
COMMENT '科研捐赠单ID(biz_research_donation.id, 仅科研捐赠来源)'
AFTER donation_sample_id;
```
**约束规则**:
- `reception_source = 'cell_storage'`:`protocol_sample_id` + `collection_protocol_id` 必填,`donation_sample_id` = NULL
- `reception_source = 'research_donation'`:`donation_sample_id` + `research_donation_id` 必填,`protocol_sample_id` = NULL
- 现有数据 `reception_source` 默认 `'cell_storage'`,无迁移风险,无需数据回填
### 2. 后端 Model 变更
**文件**:`backend/app/models/models.py` — `BizSampleReception` 类
新增字段:
```python
reception_source = Column(String(20), nullable=False, default="cell_storage", comment="来源类型 cell_storage/research_donation")
donation_sample_id = Column(String(64), nullable=True, comment="捐赠样本ID(仅科研捐赠)")
research_donation_id = Column(String(64), nullable=True, comment="科研捐赠单ID(仅科研捐赠)")
```
### 3. 后端 CRUD 变更
**文件**:`backend/app/crud/sample_reception.py`
#### 3.1 新增捐赠样本查询函数
新增 `_donation_sample_detail_rows_query(db)` — 对应捐赠样本的四表 JOIN:
- `BizResearchDonationSample` + `BizResearchDonation` + `BizResearchDonationParty` + `BizResearchDonationSampleTransport`
新增 `_donation_sample_pending_query(db, tenant_no, keyword)` — 查询未被接收的捐赠样本:
- 排除条件:`BizSampleReception.donation_sample_id IN (已接收的捐赠样本ID)`
- 关键词搜索:`donation_no`、`sample_code`、`mother_name`、`newborn_name`
新增 `_donation_rows_to_sample_list_items(db, rows, tenant_no, ...)` — 将捐赠样本行转为统一列表项字典,字段映射:
```
protocolSampleId → donation_sample.id(注意:字段名保持 protocolSampleId 不变,前端统一使用)
protocolNo → donation.donation_no(协议号/捐赠编号)
sourceType → 'research_donation'
sourceTypeLabel → '科研捐赠'
```
#### 3.2 修改待接收列表函数
修改 `list_pending_protocol_samples()`:
- 分别查询细胞存储待接收样本和捐赠待接收样本
- 合并结果列表
- 按 eta_time 排序(统一排序)
- 手动分页(先合并再截取)
修改 `list_received_protocol_samples()`:
- 从 `BizSampleReception` 查询时,按 `reception_source` 分两组
- `cell_storage` 组走现有 `_sample_detail_rows_query` 逻辑(不变)
- `research_donation` 组走新增的捐赠查询逻辑
- 合并后按 `reception_time DESC` 排序
#### 3.3 修改创建接收记录函数
修改 `create_sample_reception()`:
- 新增参数:`reception_source: str = "cell_storage"`、`donation_sample_id: Optional[str] = None`、`research_donation_id: Optional[str] = None`
- 当 `reception_source = 'research_donation'` 时:
- 校验 `donation_sample_id` 对应的 `BizResearchDonationSample` 存在
- 校验该捐赠样本未被接收(查 `BizSampleReception.donation_sample_id`)
- `protocol_sample_id` 置为空字符串或 NULL
- `collection_protocol_id` 置为空字符串或 NULL
- 接收结论为 `js` 时,仍触发 `qc_wo_svc.create_work_order_from_reception()`
#### 3.4 统一列表项输出格式
两种来源的列表项保持统一字段名,前端无需区分:
| 字段名 | 细胞存储来源 | 科研捐赠来源 |
|--------|-------------|-------------|
| `protocolSampleId` | `biz_protocol_sample.id` | `biz_research_donation_sample.id` |
| `protocolNo` | 采集协议号 | 捐赠编号 |
| `sampleCode` | 样本编码 | 样本编码 |
| `sampleTypeName` | 样本类型名 | 样本类型名 |
| `motherName` | 母亲姓名 | 母亲姓名 |
| `newbornName` | 新生儿姓名 | 新生儿姓名 |
| `sourceType` | `cell_storage` | `research_donation` |
| `sourceTypeLabel` | 细胞存储 | 科研捐赠 |
| `etaTime` | 预计到达时间 | 预计到达时间 |
| `transportType` | 运输方式 | 运输方式 |
| `waybillNo` | 快递单号 | 快递单号 |
| `expressCompanyName` | 快递公司名 | 快递公司名 |
| `carrierName` | 携带人 | 携带人 |
| `carrierPhone` | 携带人电话 | 携带人电话 |
### 4. 后端 API 变更
**文件**:`backend/app/api/sample_reception.py`
#### 4.1 待接收列表增加来源筛选
`GET /web/admin/sample-receptions/pending` 新增可选参数:
- `sourceType`:`cell_storage` / `research_donation` / 不传(全部)
#### 4.2 创建接收记录支持捐赠来源
`SampleReceptionCreateBody` 新增字段:
```python
source_type: Optional[str] = Field("cell_storage", alias="sourceType") # cell_storage / research_donation
donation_sample_id: Optional[str] = Field(None, alias="donationSampleId") # 科研捐赠时必传
```
当 `source_type = 'research_donation'` 时,`protocolSampleId` 可不传,改为传 `donationSampleId`。
### 5. 前端变更
**文件**:`web-admin/src/views/quality-inspection/sample-reception/index.vue`
#### 5.1 搜索区增加来源类型筛选
在 `filter-grid` 中增加:
```html
<el-form-item label="来源类型">
<el-select v-model="searchForm.sourceType" placeholder="全部" clearable>
<el-option label="细胞存储" value="cell_storage" />
<el-option label="科研捐赠" value="research_donation" />
</el-select>
</el-form-item>
```
#### 5.2 表格新增"来源类型"列
在序号列后增加:
```html
<el-table-column prop="sourceTypeLabel" label="来源类型" width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.sourceType === 'research_donation' ? 'warning' : 'success'" size="small">
{{ row.sourceTypeLabel }}
</el-tag>
</template>
</el-table-column>
```
#### 5.3 表格增加运输信息展示列
新增列(在快递单号之后):
- `transportType`:运输方式(快递运输 / 人员携带)
- `expressCompanyName`:快递公司
- `carrierName`:携带人
- `carrierPhone`:携带人电话
#### 5.4 接收弹窗展示完整运输信息
单个接收弹窗(`receiveDialogVisible`)和查看详情弹窗(`viewDialogVisible`)中,在"样本信息"区块后增加"运输信息"区块:
```
运输信息
运输方式:快递运输
快递公司:顺丰速运
快递单号:SF1234567890
预计到达:2026-05-20 14:00:00
携带人:张三 / 13800138000
```
使用 `readonly-grid form-grid-2` 布局,运输方式为"快递运输"时显示快递公司和快递单号,为"人员携带"时显示携带人和电话。
#### 5.5 批量接收弹窗同步调整
`batch-receive-card__meta` 区域增加来源类型标签和运输信息展示。
#### 5.6 提交参数调整
`fetchSampleReceptionSubmit` 调用时,增加 `sourceType` 和 `donationSampleId` 参数:
- 细胞存储样本:`sourceType: 'cell_storage'`,传 `protocolSampleId`
- 科研捐赠样本:`sourceType: 'research_donation'`,传 `donationSampleId`
**文件**:`web-admin/src/api/sample-reception.ts`
`fetchSampleReceptionSubmit` 参数类型增加:
```typescript
sourceType?: 'cell_storage' | 'research_donation'
donationSampleId?: string // sourceType=research_donation 时必传
```
`fetchSampleReceptionPendingList` 参数增加 `sourceType?: string`。
#### 5.7 row-key 兼容
当前 `row-key="protocolSampleId"`,捐赠样本也复用此字段名(后端统一输出),无需修改。
### 6. 变更范围汇总
| 层 | 文件 | 变更类型 |
|----|------|---------|
| SQL | `docs/sql/sample_reception_add_donation_source.sql`(新建) | ALTER TABLE |
| Model | `backend/app/models/models.py`(BizSampleReception) | 新增 3 字段 |
| CRUD | `backend/app/crud/sample_reception.py` | 新增捐赠查询函数 + 修改 pending/received/create |
| API | `backend/app/api/sample_reception.py` | 修改 schema + 路由参数 |
| 前端 API | `web-admin/src/api/sample-reception.ts` | 参数扩展 |
| 前端页面 | `web-admin/src/views/quality-inspection/sample-reception/index.vue` | UI 变更 |
**不涉及的文件**(零影响):
- 采集协议单 CRUD/API/前端
- 捐赠协议单 CRUD/API/前端
- 质检工单 CRUD(`create_work_order_from_reception` 已基于 reception 对象创建,无需改动)
### 7. 质检工单衔接
`create_work_order_from_reception()` 接收 `BizSampleReception` 对象。新增的捐赠接收记录走同一函数,内部通过 `reception.collection_protocol_id` 查询协议信息。对于捐赠来源,需确认该函数是否依赖 `collection_protocol_id` 非空。
如依赖,则需在质检工单创建逻辑中增加 `reception_source` 判断,对捐赠来源改为通过 `research_donation_id` 查询捐赠单信息作为工单的关联协议。**实施时需读取 `qc_work_order.py` 的 `create_work_order_from_reception` 函数确认并适配。**
## 设计决策记录
| 决策 | 选择 | 理由 |
|------|------|------|
| 统一表 vs 独立表 | 统一表加字段 | 用户要求统一入口,且现有细胞存储流程零改动 |
| 新增字段 vs 改用通用FK | 新增字段 | 不破坏现有数据引用,风险最低 |
| 质检工单触发 | 捐赠也触发 | 用户确认需求一致 |
| 前端字段名 | 保持 protocolSampleId 不变 | 后端统一输出,前端改动最小化 |
@@ -526,3 +526,122 @@ Web 管理端弹窗 SHALL 遵循规范:表单 `label-position="top"`;遮罩
import { Plus, Close, Check, Search, RefreshLeft, Edit, Delete, View } from '@element-plus/icons-vue'
```
### Requirement: 状态、结论与类型字段使用 Tag 标签展示
`web-admin` 中所有列表表格和查看弹窗中的**状态**、**结论**、**类型**等枚举类字段 SHALL 使用 Element Plus `<el-tag>` 组件以彩色标签形式展示,SHALL NOT 以纯文本形式展示。具体规则如下:
#### 1. 表格列中的 Tag 展示
`el-table` 中所有状态/结论/类型列 SHALL 在 `<template #default="{ row }">` 内使用 `<el-tag>` 渲染,并通过 `:type` 属性区分语义色。SHALL NOT 使用自定义 `<span class="tag">` 或纯文本。
**标准写法:**
```html
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
```
**多状态映射标准写法(使用辅助函数):**
```html
<el-table-column prop="workOrderStatus" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.workOrderStatus)" size="small">
{{ statusLabel(row.workOrderStatus) }}
</el-tag>
</template>
</el-table-column>
```
```typescript
function statusTagType(s: string): string {
const m: Record<string, string> = { ywc: 'success', ypz: 'warning', dpz: 'info' }
return m[s] || 'info'
}
```
#### 2. 查看弹窗(SystemViewDialog)中的 Tag 展示
使用 `SystemViewDialog` 组件(`src/components/business/system-view-dialog/index.vue`)时,状态/结论/类型字段 SHALL 在 `ViewField` 定义中传入 `tagType` 属性。组件检测到 `tagType` 后自动以 `<el-tag>` 渲染该字段值。
**ViewField 接口扩展(已实现):**
```typescript
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface ViewField {
label: string
value: string | number | boolean | null | undefined
span?: boolean
/** 传入后以 el-tag 渲染,值为 el-tag 的 type 属性 */
tagType?: TagType
}
```
**标准写法:**
```typescript
const viewSections = computed<ViewSection[]>(() => [
{
title: '任务信息',
fields: [
{ label: '任务号', value: d.taskNo },
{ label: '状态', value: statusLabel(d.status), tagType: d.status === 'ywc' ? 'success' : d.status === 'jxz' ? 'warning' : 'info' },
{ label: '检测结论', value: conclusionLabel(d.conclusion), tagType: d.conclusion === 'tg' ? 'success' : d.conclusion === 'btg' ? 'danger' : 'info' },
// 普通字段不加 tagType,仍以纯文本展示
{ label: '备注', value: d.remark || '—', span: true },
]
}
])
```
#### 3. 自定义查看弹窗中的 Tag 展示
不使用 `SystemViewDialog` 的自定义查看弹窗(如 `ProtocolViewDialog`、`DonationViewDialog` 等)中的状态/结论字段 SHALL 同样使用 `<el-tag>` 展示。
**标准写法:**
```html
<dd>
<el-tag :type="row.acceptConclusion === 'js' ? 'success' : 'danger'" size="small">
{{ acceptConclusionLabel(row.acceptConclusion) }}
</el-tag>
</dd>
```
#### 4. Tag 类型语义色映射规范
| Tag Type | 颜色 | 适用语义 | 示例 |
|---|---|---|---|
| `success` | 绿色 | 正常、启用、已完成、生效中、通过 | 正常/启用/已完成/接收/通过 |
| `warning` | 橙色 | 进行中、待确认、已过期、预警 | 进行中/已排配/已过期/待确认 |
| `danger` | 红色 | 异常、停用、已作废、拒绝、不通过 | 异常/已作废/拒绝/不通过 |
| `info` | 灰色 | 待执行、待生效、未知、默认 | 待执行/待生效/停用/无结论 |
| `primary` | 蓝色 | 特殊分类、来源类型、转出 | 转科研/转细胞存储/来源类型/菜单类型 |
**对于启用/停用的状态字段**,SHALL 使用 `success`(启用)和 `info`(停用),SHALL NOT 使用 `danger` 表示停用状态。
#### 5. 公共标签映射函数
多个页面复用的状态标签映射 SHALL 抽取为公共工具函数(如 `utils/protocol-payment-labels.ts`、`utils/qc-labels.ts`),同时提供 `xxxLabel`(文本)和 `xxxTagType`(tag type)两个配套函数。仅单个页面使用的状态标签 SHALL 在页面 `<script setup>` 中定义。
#### Scenario: 新增列表页包含状态列
- **WHEN** 新增列表页的表格中包含状态、结论或类型字段
- **THEN** 该列 SHALL 使用 `<el-tag>` 渲染,通过 `:type` 绑定语义色,SHALL NOT 使用纯文本或自定义 `<span class="tag">`
#### Scenario: 新增查看弹窗包含状态字段
- **WHEN** 新增 `SystemViewDialog` 查看弹窗,且 fields 中包含状态/结论/类型字段
- **THEN** 该字段 SHALL 传入 `tagType` 属性,组件自动以 `<el-tag>` 渲染
#### Scenario: Code review Tag 展示
- **WHEN** PR 修改列表页或查看弹窗中的状态/结论/类型字段展示
- **THEN** 审查者 SHALL 确认使用 `<el-tag>` 组件(表格列)或 `tagType` 属性(SystemViewDialog),SHALL NOT 出现纯文本状态展示或自定义 `<span class="tag">`
+1 -1
View File
@@ -80,7 +80,7 @@
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
const panelTitleBarClass = 'title-bar-heading'
const panelTitleBarTextRules: RegExp[] = [
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表|样本接收列表|接收检查项列表|公司列表|项目列表|工单列表|任务列表|类型列表|规格列表|价格列表|体系列表|渠道列表|机构列表|耗材列表|试剂列表|SOP列表|工艺列表|耗材包列表)/
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表|样本接收列表|接收检查项列表|公司列表|项目列表|工单列表|任务列表|类型列表|规格列表|价格列表|体系列表|渠道列表|机构列表|耗材列表|试剂列表|SOP列表|工艺列表|路线列表|耗材包列表)/
]
const refreshPanelTitleIcons = () => {
+78
View File
@@ -225,3 +225,81 @@ export function fetchDeleteProcess(id: string) {
export function fetchProcessDetail(id: string) {
return request.get<any>({ url: `/web/admin/processes/${id}` })
}
// ---------------------------------------------------------------------------
// 工艺路线管理
// ---------------------------------------------------------------------------
export function fetchProcessRouteList(params?: {
page?: number
pageSize?: number
routeName?: string
routeCode?: string
productTypeId?: string
}) {
return request.get<{
list: any[]
total: number
page: number
pageSize: number
totalPages: number
}>({ url: '/web/admin/process-routes', params })
}
export function fetchProcessRouteDetail(id: string) {
return request.get<any>({ url: `/web/admin/process-routes/${id}` })
}
export function fetchCreateProcessRoute(data: Record<string, any>) {
return request.post({ url: '/web/admin/process-routes', params: data })
}
export function fetchUpdateProcessRoute(id: string, data: Record<string, any>) {
return request.put({ url: `/web/admin/process-routes/${id}`, params: data })
}
export function fetchSetCurrentProcessRoute(routeId: string) {
return request.put({ url: '/web/admin/process-routes/set-current', params: { routeId } })
}
export function fetchDeleteProcessRoute(id: string) {
return request.del({ url: `/web/admin/process-routes/${id}` })
}
// 步骤操作
export function fetchAddRouteStep(data: { routeId: string; processId: string; stepOrder?: number }) {
return request.post({ url: '/web/admin/process-routes/step/add', params: data })
}
export function fetchCopyRouteStep(data: { stepId: string }) {
return request.post({ url: '/web/admin/process-routes/step/copy', params: data })
}
export function fetchUpdateRouteStep(data: Record<string, any>) {
return request.put({ url: '/web/admin/process-routes/step/update', params: data })
}
export function fetchReorderRouteSteps(data: { routeId: string; stepIds: string[] }) {
return request.put({ url: '/web/admin/process-routes/step/reorder', params: data })
}
export function fetchDeleteRouteStep(stepId: string) {
return request.del({ url: `/web/admin/process-routes/step/${stepId}` })
}
// 步骤子表
export function fetchUpdateStepReagents(data: { stepId: string; items: any[] }) {
return request.put({ url: '/web/admin/process-routes/step/reagent/update', params: data })
}
export function fetchUpdateStepConsumables(data: { stepId: string; items: any[] }) {
return request.put({ url: '/web/admin/process-routes/step/consumable/update', params: data })
}
export function fetchUpdateStepOutputs(data: { stepId: string; items: any[] }) {
return request.put({ url: '/web/admin/process-routes/step/output/update', params: data })
}
export function fetchUpdateStepDataCollections(data: { stepId: string; items: any[] }) {
return request.put({ url: '/web/admin/process-routes/step/data-collection/update', params: data })
}
+6 -1
View File
@@ -17,7 +17,12 @@ export function fetchQcTaskResults(taskId: string) {
export function fetchCompleteTaskWithResult(
taskId: string,
data: { results: QcTaskResultItem[]; thirdPartyAttachments?: string }
data: {
conclusion: string
results: QcTaskResultItem[]
remark?: string
thirdPartyAttachments?: string
}
) {
return request.post({
url: `/web/admin/qc-tasks/${taskId}/complete-with-result`,
+44 -2
View File
@@ -8,6 +8,36 @@ type PageList<T> = {
totalPages: number
}
export type QcTaskReceptionDetail = {
protocolNo?: string
sampleCode?: string
sampleTypeName?: string
sourceType?: string
sourceTypeLabel?: string
receptionTime?: string
handlerName?: string
receiveTemperature?: string
volumeMl?: number
netWeightG?: number
acceptConclusion?: string
acceptConclusionLabel?: string
remark?: string
transportType?: string
expressCompanyName?: string
waybillNo?: string
carrierName?: string
carrierPhone?: string
etaTime?: string
checkResults?: Array<{ itemName?: string; checked?: boolean }>
receiveAttachments?: string
receiveAttachmentFiles?: Array<{
id?: string
fileUrl?: string
fileName?: string
fileType?: string
}>
}
export type QcTask = {
id: string
workOrderId: string
@@ -20,6 +50,18 @@ export type QcTask = {
companyName?: string
feeAmount?: number
taskStatus: string
conclusion?: string
conclusionLabel?: string
qcBizType?: string
qcBizTypeLabel?: string
receptionSource?: string
receptionSourceLabel?: string
protocolNo?: string
sampleCode?: string
sampleTypeName?: string
receptionTime?: string
handlerName?: string
workOrderStatus?: string
thirdPartyAttachments?: string
attachmentFiles?: Array<{ id?: string; fileUrl: string; fileName?: string; fileType?: string }>
completedTime?: string
@@ -30,8 +72,7 @@ export type QcTask = {
}
export type QcTaskDetail = QcTask & {
protocolNo?: string
sampleCode?: string
reception?: QcTaskReceptionDetail
attachmentFiles?: Array<{ id?: string; fileUrl: string; fileName?: string }>
results?: Array<{
paramName: string
@@ -48,6 +89,7 @@ export function fetchQcTaskList(params?: {
taskType?: string
taskStatus?: string
workOrderId?: string
qcBizType?: string
}) {
return request.get<PageList<QcTask>>({
url: '/web/admin/qc-tasks',
+45 -5
View File
@@ -8,6 +8,45 @@ type PageList<T> = {
totalPages: number
}
export type SampleReceptionCheckResult = {
itemCode: string
itemName: string
checked: boolean
checkItemId?: string
}
export type QcWorkOrderReceptionDetail = {
protocolNo?: string
sampleCode?: string
sampleTypeName?: string
sourceType?: string
sourceTypeLabel?: string
receptionTime?: string
handlerName?: string
receiveTemperature?: string
volumeMl?: number
netWeightG?: number
acceptConclusion?: string
acceptConclusionLabel?: string
remark?: string
etaTime?: string
transportType?: string
expressCompanyName?: string
waybillNo?: string
carrierName?: string
carrierPhone?: string
checkResults?: SampleReceptionCheckResult[]
receiveAttachments?: string
receiveAttachmentFiles?: Array<{
id?: string
fileUrl?: string
fileName?: string
fileType?: string
fileSize?: number
fileSizeDisplay?: string
}>
}
export type QcWorkOrder = {
id: string
workOrderNo: string
@@ -16,6 +55,11 @@ export type QcWorkOrder = {
collectionProtocolId: string
protocolNo: string
sampleCode: string
receptionSource?: string
receptionSourceLabel?: string
sampleTypeName?: string
receptionTime?: string
handlerName?: string
workOrderStatus: string
hasLab: boolean
hasThirdParty: boolean
@@ -28,11 +72,7 @@ export type QcWorkOrder = {
}
export type QcWorkOrderDetail = QcWorkOrder & {
reception?: {
receptionTime?: string
handlerName?: string
acceptConclusion?: string
}
reception?: QcWorkOrderReceptionDetail
labItems?: Array<{ id: string; labTestItemId: string; itemCode: string; itemName: string }>
thirdPartyItems?: Array<{
id: string
+5 -1
View File
@@ -12,6 +12,7 @@ export function fetchSampleReceptionPendingList(params?: {
page?: number
pageSize?: number
keyword?: string
sourceType?: string
}) {
return request.get<PageList<Record<string, unknown>>>({
url: '/web/admin/sample-receptions/pending',
@@ -23,6 +24,7 @@ export function fetchSampleReceptionReceivedList(params?: {
page?: number
pageSize?: number
keyword?: string
sourceType?: string
}) {
return request.get<PageList<Record<string, unknown>>>({
url: '/web/admin/sample-receptions/received',
@@ -46,7 +48,9 @@ export type SampleReceptionCheckResult = {
}
export function fetchSampleReceptionSubmit(data: {
protocolSampleId: string
protocolSampleId?: string
sourceType?: 'cell_storage' | 'research_donation'
donationSampleId?: string
receptionTime: string
handlerUserId?: string
handlerName?: string
@@ -1,7 +1,7 @@
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
<template>
<div class="protocol-finance-confirm-fields">
<el-form-item :label="remarkLabel" class="form-col-span-2">
<el-form-item v-if="showRemark" :label="remarkLabel" class="form-col-span-2">
<el-input
:model-value="financeRemark"
type="textarea"
@@ -97,11 +97,13 @@
uploadFolder?: string
remarkLabel?: string
attachmentLabel?: string
showRemark?: boolean
}>(),
{
uploadFolder: 'protocol-finance-confirm',
remarkLabel: '财务备注',
attachmentLabel: '财务确认附件'
attachmentLabel: '财务确认附件',
showRemark: true
}
)
@@ -0,0 +1,303 @@
<!-- 质检任务 — 只读查看(参数、结论、附件) -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<SystemViewDialog
v-model:visible="innerVisible"
title="质检任务详情"
width="960px"
:grid-columns="3"
:icon="Tickets"
:meta="viewDetail?.taskNo || ''"
:sections="viewSections"
append-to-body
compact
modal-class="qc-task-view-dialog-mask"
>
<template #reception-extra>
<div class="view-reception-attachments">
<div class="view-kv view-kv-span-2">
<dt>接收附件</dt>
<dd>
<ProtocolAttachmentReadonly
:files="viewReceptionAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</dd>
</div>
</div>
</template>
<template #append>
<div
v-if="viewDetail?.attachmentFiles?.length"
class="view-section qc-task-view-attachments"
>
<h3>{{ viewDetail?.taskType === 'dsfjc' ? '检测材料' : '检测附件' }}</h3>
<ProtocolAttachmentReadonly
:files="viewDetail.attachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</template>
</SystemViewDialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Tickets } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { fetchQcTaskDetail, type QcTaskDetail } from '@/api/qc-task'
import { fetchQcTaskResults } from '@/api/qc-task-result'
import {
taskTypeLabel,
taskStatusLabel,
qcBizTypeLabel,
receptionSourceLabel,
acceptConclusionLabel,
transportTypeLabel,
workOrderStatusLabel,
qcTaskConclusionLabel
} from '@/utils/qc-labels'
import { downloadAttachmentFile } from '@/utils/download-attachment'
const props = defineProps<{
visible: boolean
taskId?: string
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const loading = ref(false)
const viewDetail = ref<QcTaskDetail | null>(null)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
watch(
() => [props.visible, props.taskId] as const,
async ([vis, id]) => {
if (!vis || !id) {
viewDetail.value = null
return
}
loading.value = true
try {
const detail = await fetchQcTaskDetail(id)
try {
const results = await fetchQcTaskResults(id)
;(detail as QcTaskDetail).results = results || []
} catch {
;(detail as QcTaskDetail).results = []
}
viewDetail.value = detail
} catch {
viewDetail.value = null
} finally {
loading.value = false
}
},
{ immediate: true }
)
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
return String(v)
}
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
if (!items?.length) return '—'
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
}
const viewReceptionAttachmentFiles = computed(() => {
const r = viewDetail.value?.reception
if (!r) return []
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
}
const urls = String(r.receiveAttachments || '')
if (!urls.trim()) return []
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((fileUrl, i) => ({ id: `rec_${i}`, fileUrl, fileName: fileUrl.split('/').pop() || '' }))
})
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
const r = d.reception || {}
const transportFields: ViewSection['fields'] = [
{ label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
]
if (r.transportType === 'kdys') {
transportFields.push(
{ label: '快递公司', value: r.expressCompanyName || '—' },
{ label: '快递单号', value: r.waybillNo || '—' }
)
} else if (r.transportType === 'ryxd') {
transportFields.push(
{ label: '携带人', value: r.carrierName || '—' },
{ label: '携带人电话', value: r.carrierPhone || '—' }
)
}
if (r.etaTime) {
transportFields.push({ label: '预计到达', value: r.etaTime })
}
const resultSection =
d.results && d.results.length > 0
? [
{
title: '检测参数',
fields: d.results.map((item) => ({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
}))
}
]
: []
return [
{
title: '任务信息',
fields: [
{ label: '任务号', value: d.taskNo },
{ label: '业务类型', value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''), tagType: 'success' },
{ label: '工单号', value: d.workOrderNo || '—' },
{
label: '工单状态',
value: d.workOrderStatus ? workOrderStatusLabel(d.workOrderStatus) : '—',
tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info'
},
{ label: '检测类型', value: taskTypeLabel(d.taskType), tagType: d.taskType === 'sysz' ? 'success' : 'info' },
{ label: '检测项目', value: d.itemName },
{ label: '项目编码', value: d.itemCode || '—' },
{ label: '第三方公司', value: d.companyName || '—' },
{
label: '任务状态',
value: taskStatusLabel(d.taskStatus),
tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info'
},
{
label: '检测结论',
value: d.conclusionLabel || qcTaskConclusionLabel(d.conclusion || '') || '—',
tagType: d.conclusion === 'tg' ? 'success' : d.conclusion === 'btg' ? 'danger' : 'info'
},
{ label: '完成时间', value: d.completedTime || '—' },
{ label: '创建时间', value: d.createTime || '—' },
{ label: '备注', value: d.remark || '—', span: true }
]
},
{
title: '样本信息',
fields: [
{ label: '协议号', value: r.protocolNo || d.protocolNo || '—' },
{ label: '样本编码', value: r.sampleCode || d.sampleCode || '—' },
{
label: '样本来源',
value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary'
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
]
},
...(transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []),
{
title: '样本接收',
slot: 'reception-extra',
fields: [
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
{
label: '接收温度(℃)',
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
},
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
{
label: '结论',
value:
r.acceptConclusionLabel ||
acceptConclusionLabel(String(r.acceptConclusion || ''))
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
},
...resultSection
]
})
function attachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
</script>
<style lang="scss">
/* 叠在协议/捐赠查看弹窗之上 */
.qc-task-view-dialog-mask {
z-index: 4000 !important;
}
</style>
<style scoped lang="scss">
.view-reception-attachments {
margin-top: 4px;
.view-kv {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
font-size: 13px;
align-items: start;
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
}
}
}
.qc-task-view-attachments {
padding: 12px 18px;
border-bottom: none;
h3 {
margin: 0 0 10px;
padding-left: 10px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
border-left: 3px solid #10b981;
line-height: 1;
}
}
</style>
@@ -0,0 +1,299 @@
<!-- 样本查看 — 质检信息卡片(协议单/捐赠单查看弹窗复用) -->
<template>
<div class="sample-card__section sample-card__section--qc">
<div class="sample-card__section-label">质检信息</div>
<div v-if="!qcInfo" class="sample-qc-empty">{{ emptyHint }}</div>
<template v-else>
<div class="qc-work-order-row">
<div class="qc-kv">
<span class="qc-kv__label">工单号</span>
<span class="qc-kv__value qc-kv__value--code">{{ qcInfo.workOrderNo || '—' }}</span>
</div>
<div class="qc-kv">
<span class="qc-kv__label">业务类型</span>
<span class="qc-kv__value">{{
qcInfo.qcBizTypeLabel || qcBizTypeLabel(qcInfo.qcBizType || '')
}}</span>
</div>
<div class="qc-kv">
<span class="qc-kv__label">工单状态</span>
<span class="qc-kv__value">
<el-tag :type="workOrderStatusTagType(qcInfo.workOrderStatus || '')" size="small">
{{ qcInfo.workOrderStatusLabel || workOrderStatusLabel(qcInfo.workOrderStatus || '') }}
</el-tag>
</span>
</div>
</div>
<div v-if="qcInfo.workOrderStatus === 'dpz' && !qcInfo.tasks?.length" class="sample-qc-hint">
工单待配置检测项目
</div>
<div v-if="qcInfo.tasks?.length" class="qc-detail-cards qc-detail-cards--tasks">
<div
v-for="task in qcInfo.tasks"
:key="task.id"
class="qc-detail-card qc-detail-card--task"
>
<div class="qc-detail-card__header">
<span class="qc-detail-card__name">{{ task.itemName || '—' }}</span>
<el-tag type="info" size="small">{{
task.taskTypeLabel || taskTypeLabel(task.taskType || '')
}}</el-tag>
</div>
<div class="qc-detail-card__body">
<div class="qc-kv">
<span class="qc-kv__label">任务号</span>
<span class="qc-kv__value">{{ task.taskNo || '—' }}</span>
</div>
<div class="qc-kv">
<span class="qc-kv__label">任务状态</span>
<span class="qc-kv__value">
<el-tag :type="taskStatusTagType(task.taskStatus)" size="small">
{{ task.taskStatusLabel || taskStatusLabel(task.taskStatus || '') }}
</el-tag>
</span>
</div>
<div class="qc-kv">
<span class="qc-kv__label">检测结论</span>
<span class="qc-kv__value">
<el-tag
v-if="task.conclusion"
:type="qcTaskConclusionTagType(task.conclusion)"
size="small"
>
{{ task.conclusionLabel || qcTaskConclusionLabel(task.conclusion) }}
</el-tag>
<span v-else class="qc-kv__muted">—</span>
</span>
</div>
</div>
<div class="qc-detail-card__foot">
<button type="button" class="qc-detail-card__link" @click.stop="openTaskView(task)">
点击查看详情
</button>
</div>
</div>
</div>
<div
v-else-if="qcInfo.workOrderStatus !== 'dpz'"
class="sample-qc-hint"
>
暂无检测任务
</div>
</template>
<Teleport to="body">
<QcTaskViewDialog v-model:visible="taskViewVisible" :task-id="taskViewId" />
</Teleport>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
qcBizTypeLabel,
workOrderStatusLabel,
taskTypeLabel,
taskStatusLabel,
qcTaskConclusionLabel,
qcTaskConclusionTagType
} from '@/utils/qc-labels'
import { workOrderStatusTagType, taskStatusTagType } from '@/utils/status-tag-registry'
import QcTaskViewDialog from '@/components/business/qc-task-view-dialog/index.vue'
export type SampleQcTask = {
id?: string
taskNo?: string
taskType?: string
taskTypeLabel?: string
itemName?: string
itemCode?: string
companyName?: string
taskStatus?: string
taskStatusLabel?: string
conclusion?: string
conclusionLabel?: string
completedTime?: string
remark?: string
}
export type SampleQcInfo = {
workOrderNo?: string
workOrderStatus?: string
workOrderStatusLabel?: string
qcBizType?: string
qcBizTypeLabel?: string
configuredTime?: string
hasLab?: boolean
hasThirdParty?: boolean
labItems?: Array<{ id?: string; itemCode?: string; itemName?: string }>
thirdPartyItems?: Array<{
id?: string
itemCode?: string
itemName?: string
companyName?: string
}>
tasks?: SampleQcTask[]
}
export type SampleReceptionBrief = {
acceptConclusion?: string
}
const props = defineProps<{
qcInfo?: SampleQcInfo | null
sampleReception?: SampleReceptionBrief | null
}>()
const taskViewVisible = ref(false)
const taskViewId = ref('')
const emptyHint = computed(() => {
const rec = props.sampleReception
if (!rec) {
return '暂未接收样本,接收结论为「接收」后将自动生成质检工单'
}
if (rec.acceptConclusion === 'bj') {
return '样本拒收,不生成质检工单'
}
if (rec.acceptConclusion === 'js') {
return '已接收,质检工单生成中或待配置'
}
return '暂无质检信息'
})
function openTaskView(task: SampleQcTask) {
if (!task.id) return
taskViewId.value = task.id
taskViewVisible.value = true
}
</script>
<style scoped lang="scss">
.sample-card__section--qc {
margin-top: 4px;
padding-top: 14px;
border-top: 1px dashed #e2e8f0;
}
.sample-qc-empty,
.sample-qc-hint {
font-size: 13px;
color: #94a3b8;
padding: 8px 0;
}
.qc-work-order-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px 24px;
margin-bottom: 12px;
padding: 0 2px;
}
.qc-work-order-row .qc-kv {
flex: 1;
min-width: 140px;
}
.qc-detail-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 10px;
&--tasks {
margin-bottom: 0;
}
}
.qc-detail-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
&--task {
border-left: 3px solid #0ea5e9;
}
}
.qc-detail-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid #f1f5f9;
}
.qc-detail-card__name {
font-size: 13px;
font-weight: 600;
color: #1e293b;
text-align: left;
flex: 1;
word-break: break-word;
}
.qc-detail-card__body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.qc-detail-card__foot {
padding: 2px 12px 8px;
text-align: right;
border-top: 1px dashed #f1f5f9;
}
.qc-detail-card__link {
padding: 0;
border: none;
background: none;
font-size: 11px;
line-height: 1.4;
color: #10b981;
cursor: pointer;
&:hover {
color: #059669;
text-decoration: underline;
}
}
.qc-kv__muted {
color: #94a3b8;
font-weight: 400;
}
.qc-kv {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 12px;
}
.qc-kv__label {
color: #94a3b8;
flex-shrink: 0;
min-width: 56px;
}
.qc-kv__value {
color: #1e293b;
font-weight: 500;
word-break: break-word;
&--code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
}
}
</style>
@@ -3,11 +3,14 @@
<template>
<el-dialog
v-model="innerVisible"
width="720px"
:width="width"
class="system-view-dialog"
:class="[dialogClass, { 'system-view-dialog--compact': compact }]"
align-center
destroy-on-close
:show-close="false"
:append-to-body="appendToBody"
:modal-class="modalClass"
style="backdrop-filter: none"
>
<div class="dialog-scroll">
@@ -25,7 +28,7 @@
<!-- sections -->
<div v-for="section in sections" :key="section.title" class="view-section">
<h3>{{ section.title }}</h3>
<dl class="view-kv-grid">
<dl class="view-kv-grid" :class="`view-kv-grid--cols-${gridColumns}`">
<div
v-for="field in section.fields"
:key="field.label"
@@ -33,9 +36,13 @@
:class="{ 'view-kv-span-2': field.span }"
>
<dt>{{ field.label }}</dt>
<dd>{{ field.value ?? '—' }}</dd>
<dd>
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{ field.value ?? '—' }}</el-tag>
<template v-else>{{ field.value ?? '—' }}</template>
</dd>
</div>
</dl>
<slot v-if="section.slot" :name="section.slot" />
</div>
<slot name="append" />
</div>
@@ -52,24 +59,52 @@
import { computed, type Component } from 'vue'
import { Close } from '@element-plus/icons-vue'
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface ViewField {
label: string
value: string | number | boolean | null | undefined
span?: boolean
/** 传入后以 el-tag 渲染,值为 el-tag 的 type 属性 */
tagType?: TagType
}
export interface ViewSection {
title: string
fields: ViewField[]
/** 区块内追加内容(如附件列表),对应具名插槽名 */
slot?: string
}
const props = defineProps<{
visible: boolean
title: string
icon?: Component
meta?: string
sections: ViewSection[]
}>()
const props = withDefaults(
defineProps<{
visible: boolean
title: string
icon?: Component
meta?: string
sections: ViewSection[]
/** 弹窗宽度,默认 720px */
width?: string
/** 字段栅格列数,默认 2 */
gridColumns?: 2 | 3
/** 挂到 body,用于嵌套在其它弹窗内时叠层展示 */
appendToBody?: boolean
/** 遮罩类名(可配合全局样式提高 z-index) */
modalClass?: string
/** 附加 dialog class */
dialogClass?: string
/** 紧凑布局:缩小头部/区块间距与内容区最大高度 */
compact?: boolean
}>(),
{
width: '720px',
gridColumns: 2,
appendToBody: false,
modalClass: '',
dialogClass: '',
compact: false
}
)
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
@@ -153,7 +188,11 @@
.view-kv-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 28px;
gap: 12px 20px;
&--cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.view-kv {
@@ -180,8 +219,15 @@
grid-column: 1 / -1;
}
@media (max-width: 900px) {
.view-kv-grid--cols-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.view-kv-grid {
.view-kv-grid,
.view-kv-grid--cols-3 {
grid-template-columns: 1fr;
}
@@ -190,6 +236,60 @@
gap: 4px;
}
}
.system-view-dialog--compact {
.dialog-scroll {
max-height: min(68vh, 560px);
}
.view-hero {
gap: 12px;
padding: 12px 18px;
}
.view-hero-icon {
width: 48px;
height: 48px;
border-radius: 10px;
.el-icon {
font-size: 22px !important;
}
}
.view-hero-main h2 {
margin-bottom: 4px;
font-size: 16px;
}
.view-hero-meta {
font-size: 12px;
}
.view-section {
padding: 12px 18px;
h3 {
margin-bottom: 10px;
font-size: 13px;
}
}
.view-kv-grid {
gap: 8px 16px;
}
.view-kv {
font-size: 12px;
grid-template-columns: 84px 1fr;
}
}
</style>
<style lang="scss">
.system-view-dialog--compact.el-dialog .el-dialog__footer {
padding: 8px 18px;
}
</style>
<style lang="scss">
+8 -1
View File
@@ -329,7 +329,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'qc-work-order',
component: '/quality-inspection/qc-work-order/index',
meta: {
title: '质检工单',
title: '入库质检工单',
keepAlive: true,
menuCode: 'biz:qcWorkOrder:list'
}
@@ -456,6 +456,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'sop',
component: '/production/sop/index',
meta: { title: 'SOP文档', keepAlive: true, menuCode: 'production:sop:list' }
},
{
id: '90000000000000000000000000000010',
name: 'ProductionProcessRoute',
path: 'process-route',
component: '/production/process-route/index',
meta: { title: '工艺路线管理', keepAlive: true, menuCode: 'production:process-route:list' }
}
]
},
+10 -39
View File
@@ -1,40 +1,13 @@
export function protocolStatusLabel(s: string) {
const map: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return map[s] || s || '-'
}
export function payModeLabel(s: string) {
if (s === 'dj') return '趸交'
if (s === 'fq') return '分期'
return s || '-'
}
export function payStatusLabel(s: string) {
const map: Record<string, string> = {
dfj: '待缴费',
jfqrz: '缴费确认中',
yjf: '已缴费',
tfqrz: '退费确认中',
ytf: '已退费'
}
return map[s] || '未知'
}
export function transferTypeLabel(t: string) {
const map: Record<string, string> = {
wxzz: '微信转账',
zzbzz: '支付宝转账',
yhkzz: '银行卡转账',
xj: '现金'
}
return map[t] || t || '-'
}
export {
protocolStatusLabel,
protocolStatusTagType,
payStatusLabel,
payStatusTagType,
payModeLabel,
payModeTagType,
transferTypeLabel,
transferTypeTagType
} from './status-tag-registry'
export function formatPaymentDueDate(d: unknown) {
const s = String(d || '')
@@ -43,7 +16,6 @@ export function formatPaymentDueDate(d: unknown) {
return s
}
/** 分期应缴预警天数(与分期预警规则一致:应缴日在当天起未来 N 天内,含逾期) */
export const PAYMENT_DUE_WARN_DAYS = 15
function parsePaymentDueYmd(ymd: unknown): Date | null {
@@ -61,7 +33,6 @@ function startOfLocalDay(d: Date) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
}
/** 应缴日是否不晚于「今天 + warnDays」(含当天与已逾期) */
export function isPaymentDueWithinWarnWindow(
installmentDueDate: unknown,
warnDays = PAYMENT_DUE_WARN_DAYS
+18 -25
View File
@@ -1,25 +1,18 @@
export function workOrderStatusLabel(v: string) {
const m: Record<string, string> = {
dpz: '待配置',
ypz: '已配置',
ywc: '已完成'
}
return m[v] || v || '—'
}
export function taskTypeLabel(v: string) {
const m: Record<string, string> = {
sysz: '实验室自检',
dsfjc: '第三方检测'
}
return m[v] || v || '—'
}
export function taskStatusLabel(v: string) {
const m: Record<string, string> = {
dzx: '待执行',
jxz: '进行中',
ywc: '已完成'
}
return m[v] || v || '—'
}
export {
qcBizTypeLabel,
qcBizTypeTagType,
receptionSourceLabel,
receptionSourceTagType,
acceptConclusionLabel,
acceptConclusionTagType,
transportTypeLabel,
transportTypeTagType,
workOrderStatusLabel,
workOrderStatusTagType,
taskTypeLabel,
taskTypeTagType,
qcTaskConclusionLabel,
qcTaskConclusionTagType,
taskStatusLabel,
taskStatusTagType
} from './status-tag-registry'
+328
View File
@@ -0,0 +1,328 @@
/**
* 状态标签注册表 — 全局集中定义所有枚举字段的 label + tagType
*
* 使用方式:
* import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
* <el-tag :type="protocolStatusTagType(row.status)">{{ protocolStatusLabel(row.status) }}</el-tag>
*
* 规范:同一枚举值在任何地方(表格、查看弹窗、导出)颜色必须一致。
* tagType 语义色:success=正常/已完成, warning=进行中/已过期, danger=异常/已作废,
* info=待执行/停用, primary=特殊分类/转出, ''=默认
*/
// ── 类型定义 ──────────────────────────────────────────────────────────────────
/** el-tag 的 type 属性值 */
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
/** 单条枚举映射 */
export interface StatusItem {
label: string
tagType: TagType
}
/** 注册表类型 */
export type StatusRegistry = Record<string, StatusItem>
// ── 工厂函数 ──────────────────────────────────────────────────────────────────
function createLabelGetter(registry: StatusRegistry) {
return (code: string): string => registry[code]?.label ?? code ?? '—'
}
function createTagTypeGetter(registry: StatusRegistry) {
return (code: string): TagType => registry[code]?.tagType ?? 'info'
}
// ══════════════════════════════════════════════════════════════════════════════
// 协议 / 存储 域
// ══════════════════════════════════════════════════════════════════════════════
export const protocolStatus: StatusRegistry = {
dsx: { label: '待生效', tagType: 'info' },
sxz: { label: '生效中', tagType: 'success' },
ygq: { label: '已过期', tagType: 'warning' },
yzf: { label: '已作废', tagType: 'danger' },
zky: { label: '转科研', tagType: 'primary' }
}
export const protocolStatusLabel = createLabelGetter(protocolStatus)
export const protocolStatusTagType = createTagTypeGetter(protocolStatus)
export const donationStatus: StatusRegistry = {
dsx: { label: '待生效', tagType: 'info' },
sxz: { label: '生效中', tagType: 'success' },
ygq: { label: '已过期', tagType: 'warning' },
yzf: { label: '已作废', tagType: 'danger' },
zckc: { label: '转细胞存储', tagType: 'primary' }
}
export const donationStatusLabel = createLabelGetter(donationStatus)
export const donationStatusTagType = createTagTypeGetter(donationStatus)
export const sampleStatus: StatusRegistry = {
ycj: { label: '已采集', tagType: 'primary' },
jcz: { label: '检测中', tagType: 'warning' },
yrk: { label: '已入库', tagType: 'success' },
yzf: { label: '已作废', tagType: 'danger' }
}
export const sampleStatusLabel = createLabelGetter(sampleStatus)
export const sampleStatusTagType = createTagTypeGetter(sampleStatus)
export const payStatus: StatusRegistry = {
dfj: { label: '待缴费', tagType: 'info' },
jfqrz: { label: '缴费确认中', tagType: 'warning' },
yjf: { label: '已缴费', tagType: 'success' },
tfqrz: { label: '退费确认中', tagType: 'warning' },
ytf: { label: '已退费', tagType: 'danger' }
}
export const payStatusLabel = createLabelGetter(payStatus)
export const payStatusTagType = createTagTypeGetter(payStatus)
export const auditStatus: StatusRegistry = {
shtg: { label: '审核通过', tagType: 'success' },
shth: { label: '审核退回', tagType: 'danger' }
}
export const auditStatusLabel = createLabelGetter(auditStatus)
export const auditStatusTagType = createTagTypeGetter(auditStatus)
export const payMode: StatusRegistry = {
dj: { label: '趸交', tagType: 'info' },
fq: { label: '分期', tagType: 'warning' }
}
export const payModeLabel = createLabelGetter(payMode)
export const payModeTagType = createTagTypeGetter(payMode)
export const transferType: StatusRegistry = {
wxzz: { label: '微信转账', tagType: 'success' },
zzbzz: { label: '支付宝转账', tagType: 'primary' },
yhkzz: { label: '银行卡转账', tagType: 'info' },
xj: { label: '现金', tagType: 'warning' }
}
export const transferTypeLabel = createLabelGetter(transferType)
export const transferTypeTagType = createTagTypeGetter(transferType)
export const transportType: StatusRegistry = {
kdys: { label: '快递运输', tagType: 'info' },
ryxd: { label: '人员携带', tagType: 'success' }
}
export const transportTypeLabel = createLabelGetter(transportType)
export const transportTypeTagType = createTagTypeGetter(transportType)
export const receptionSource: StatusRegistry = {
cell_storage: { label: '细胞存储', tagType: 'primary' },
research_donation: { label: '科研捐赠', tagType: 'success' }
}
export const receptionSourceLabel = createLabelGetter(receptionSource)
export const receptionSourceTagType = createTagTypeGetter(receptionSource)
export const newbornSex: StatusRegistry = {
'1': { label: '男', tagType: 'primary' },
'0': { label: '女', tagType: 'success' }
}
export const newbornSexLabel = createLabelGetter(newbornSex)
export const newbornSexTagType = createTagTypeGetter(newbornSex)
export const deliveryMode: StatusRegistry = {
sc: { label: '顺产', tagType: 'info' },
pgc: { label: '剖宫产', tagType: 'warning' }
}
export const deliveryModeLabel = createLabelGetter(deliveryMode)
export const deliveryModeTagType = createTagTypeGetter(deliveryMode)
export const sourceType: StatusRegistry = {
direct: { label: '独立新建', tagType: 'info' },
from_storage: { label: '从存储转来', tagType: 'primary' }
}
export const sourceTypeLabel = createLabelGetter(sourceType)
export const sourceTypeTagType = createTagTypeGetter(sourceType)
// ══════════════════════════════════════════════════════════════════════════════
// 质检域
// ══════════════════════════════════════════════════════════════════════════════
export const qcBizType: StatusRegistry = {
inbound: { label: '入库质检', tagType: 'success' },
outbound: { label: '出库质检', tagType: 'primary' }
}
export const qcBizTypeLabel = createLabelGetter(qcBizType)
export const qcBizTypeTagType = createTagTypeGetter(qcBizType)
export const workOrderStatus: StatusRegistry = {
dpz: { label: '待配置', tagType: 'info' },
ypz: { label: '已配置', tagType: 'success' },
ywc: { label: '已完成', tagType: 'success' }
}
export const workOrderStatusLabel = createLabelGetter(workOrderStatus)
export const workOrderStatusTagType = createTagTypeGetter(workOrderStatus)
export const taskType: StatusRegistry = {
sysz: { label: '实验室自检', tagType: 'info' },
dsfjc: { label: '第三方检测', tagType: 'primary' }
}
export const taskTypeLabel = createLabelGetter(taskType)
export const taskTypeTagType = createTagTypeGetter(taskType)
export const qcConclusion: StatusRegistry = {
tg: { label: '通过', tagType: 'success' },
btg: { label: '不通过', tagType: 'danger' }
}
export const qcTaskConclusionLabel = createLabelGetter(qcConclusion)
export const qcTaskConclusionTagType = createTagTypeGetter(qcConclusion)
export const taskStatus: StatusRegistry = {
dzx: { label: '待执行', tagType: 'info' },
jxz: { label: '进行中', tagType: 'warning' },
ywc: { label: '已完成', tagType: 'success' }
}
export const taskStatusLabel = createLabelGetter(taskStatus)
export const taskStatusTagType = createTagTypeGetter(taskStatus)
export const acceptConclusion: StatusRegistry = {
js: { label: '接收', tagType: 'success' },
bj: { label: '拒收', tagType: 'danger' }
}
export const acceptConclusionLabel = createLabelGetter(acceptConclusion)
export const acceptConclusionTagType = createTagTypeGetter(acceptConclusion)
// ══════════════════════════════════════════════════════════════════════════════
// 渠道域
// ══════════════════════════════════════════════════════════════════════════════
export const channelType: StatusRegistry = {
yy: { label: '医院', tagType: 'primary' },
hzjg: { label: '合作机构', tagType: 'success' },
zs: { label: '诊所', tagType: 'warning' },
zfjg: { label: '政府机构', tagType: 'danger' },
qt: { label: '其他', tagType: 'info' }
}
export const channelTypeLabel = createLabelGetter(channelType)
export const channelTypeTagType = createTagTypeGetter(channelType)
export const agreementType: StatusRegistry = {
kyxy: { label: '框架协议', tagType: 'primary' },
zsxy: { label: '中试协议', tagType: 'success' },
lcxy: { label: '临床协议', tagType: 'warning' }
}
export const agreementTypeLabel = createLabelGetter(agreementType)
export const agreementTypeTagType = createTagTypeGetter(agreementType)
export const agreementStatus: StatusRegistry = {
dsx: { label: '待生效', tagType: 'info' },
sxz: { label: '生效中', tagType: 'success' },
yzf: { label: '已作废', tagType: 'danger' }
}
export const agreementStatusLabel = createLabelGetter(agreementStatus)
export const agreementStatusTagType = createTagTypeGetter(agreementStatus)
export const settlementType: StatusRegistry = {
yj: { label: '月结', tagType: '' },
jj: { label: '季结', tagType: 'success' },
nj: { label: '年结', tagType: 'warning' },
dcjs: { label: '单次结算', tagType: 'danger' }
}
export const settlementTypeLabel = createLabelGetter(settlementType)
export const settlementTypeTagType = createTagTypeGetter(settlementType)
// ══════════════════════════════════════════════════════════════════════════════
// 订单域
// ══════════════════════════════════════════════════════════════════════════════
export const orderStatus: StatusRegistry = {
pending_payment: { label: '待支付', tagType: 'warning' },
pending_confirm: { label: '待收货', tagType: 'info' },
completed: { label: '已完成', tagType: 'success' },
cancelled: { label: '已取消', tagType: 'danger' }
}
export const orderStatusLabel = createLabelGetter(orderStatus)
export const orderStatusTagType = createTagTypeGetter(orderStatus)
export const deliveryStatusMap: StatusRegistry = {
pending_dispatch: { label: '待分配配送', tagType: 'info' },
delivering: { label: '配送中', tagType: 'warning' },
delivered: { label: '配送完成', tagType: 'success' }
}
export const deliveryStatusLabel = createLabelGetter(deliveryStatusMap)
export const deliveryStatusTagType = createTagTypeGetter(deliveryStatusMap)
export const paymentMethod: StatusRegistry = {
online: { label: '线上支付', tagType: 'success' },
offline: { label: '线下支付', tagType: 'info' }
}
export const paymentMethodLabel = createLabelGetter(paymentMethod)
export const paymentMethodTagType = createTagTypeGetter(paymentMethod)
export const orderPayStatus: StatusRegistry = {
pending_payment: { label: '待支付', tagType: 'warning' },
paid: { label: '已支付', tagType: 'success' }
}
export const orderPayStatusLabel = createLabelGetter(orderPayStatus)
export const orderPayStatusTagType = createTagTypeGetter(orderPayStatus)
// ══════════════════════════════════════════════════════════════════════════════
// 系统 / 通用域
// ══════════════════════════════════════════════════════════════════════════════
/** 通用启用/停用(数据库 status 字段 1/0) */
export const enableStatus: StatusRegistry = {
'1': { label: '正常', tagType: 'success' },
'0': { label: '停用', tagType: 'danger' }
}
export const enableStatusLabel = createLabelGetter(enableStatus)
export const enableStatusTagType = createTagTypeGetter(enableStatus)
/** 性别(客户等业务表) */
export const sex: StatusRegistry = {
'0': { label: '男', tagType: 'primary' },
'1': { label: '女', tagType: 'success' }
}
export const sexLabel = createLabelGetter(sex)
export const sexTagType = createTagTypeGetter(sex)
/** 请假状态 */
export const leaveStatus: StatusRegistry = {
draft: { label: '草稿', tagType: 'info' },
pending: { label: '审批中', tagType: 'warning' },
approved: { label: '已通过', tagType: 'success' },
rejected: { label: '已驳回', tagType: 'danger' },
cancelled: { label: '已取消', tagType: 'info' }
}
export const leaveStatusLabel = createLabelGetter(leaveStatus)
export const leaveStatusTagType = createTagTypeGetter(leaveStatus)
/** 数据权限范围 */
export const dataScope: StatusRegistry = {
'1': { label: '全部数据', tagType: '' },
'2': { label: '本部门数据', tagType: 'info' },
'3': { label: '本部门及以下', tagType: 'info' },
'4': { label: '仅本人', tagType: 'warning' },
'5': { label: '自定义', tagType: 'primary' }
}
export const dataScopeLabel = createLabelGetter(dataScope)
export const dataScopeTagType = createTagTypeGetter(dataScope)
/** 洁净室等级 */
export const cleanRoomGrade: StatusRegistry = {
A: { label: 'A级', tagType: 'warning' },
B: { label: 'B级', tagType: '' },
C: { label: 'C级', tagType: 'info' },
D: { label: 'D级', tagType: 'info' }
}
export const cleanRoomGradeLabel = createLabelGetter(cleanRoomGrade)
export const cleanRoomGradeTagType = createTagTypeGetter(cleanRoomGrade)
/** 样本编码类型 */
export const sampleCodeType: StatusRegistry = {
zxbm: { label: '造血编码', tagType: 'info' },
qdbm: { label: '脐带编码', tagType: 'primary' }
}
export const sampleCodeTypeLabel = createLabelGetter(sampleCodeType)
export const sampleCodeTypeTagType = createTagTypeGetter(sampleCodeType)
/** 耗材包类型 */
export const packType: StatusRegistry = {
bzhcb: { label: '标准耗材包', tagType: 'info' },
zdyhcb: { label: '自定义耗材包', tagType: 'primary' }
}
export const packTypeLabel = createLabelGetter(packType)
export const packTypeTagType = createTagTypeGetter(packType)
@@ -237,16 +237,11 @@
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { useAuth } from '@/hooks/core/useAuth'
import { sexLabel } from '@/utils/status-tag-registry'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCustomer' })
function sexLabel(s: string) {
if (s === '0') return '男'
if (s === '1') return '女'
return '未知'
}
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCustomer.CustomerItem[]>([])
const selectedRows = ref<Api.BizCustomer.CustomerItem[]>([])
@@ -393,16 +393,11 @@
fetchBatchRevokeSampleCodes
} from '@/api/admin'
import { useAuth } from '@/hooks/core/useAuth'
import { sampleCodeTypeLabel } from '@/utils/status-tag-registry'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizSampleCode' })
function sampleCodeTypeLabel(t: string) {
if (t === 'zxbm') return '造血编码'
if (t === 'qdbm') return '脐带编码'
return t || '-'
}
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizSampleCode.SampleCodeItem[]>([])
const selectedRows = ref<Api.BizSampleCode.SampleCodeItem[]>([])
@@ -61,7 +61,7 @@
</el-table-column>
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
<template #default="{ row }">
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
@@ -87,7 +87,9 @@
</el-table-column>
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
<template #default>
{{ activeTab === 'pending' ? '缴费确认中' : '已缴费' }}
<el-tag :type="activeTab === 'pending' ? 'warning' : 'success'" size="small">
{{ activeTab === 'pending' ? '缴费确认中' : '已缴费' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -248,6 +250,7 @@
formatPaymentDueDate,
payModeLabel,
protocolStatusLabel,
protocolStatusTagType,
transferTypeLabel
} from '@/utils/protocol-payment-labels'
@@ -166,7 +166,7 @@
header-align="center"
>
<template #default="{ row }">
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -433,6 +433,14 @@
import { fetchUploadAttachment } from '@/api/business'
import { maskIdCard } from '@/utils/mask-sensitive'
import { isPaymentDueWithinWarnWindow } from '@/utils/protocol-payment-labels'
import {
protocolStatusLabel,
protocolStatusTagType,
payModeLabel,
payStatusLabel,
payStatusTagType,
transferTypeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStoragePaymentInfo' })
@@ -496,55 +504,6 @@
viewVisible.value = true
}
function protocolStatusLabel(s: string) {
const map: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return map[s] || s || '-'
}
function payModeLabel(s: string) {
if (s === 'dj') return '趸交'
if (s === 'fq') return '分期'
return s || '-'
}
function payStatusLabel(s: string) {
const map: Record<string, string> = {
dfj: '待缴费',
jfqrz: '缴费确认中',
yjf: '已缴费',
tfqrz: '退费确认中',
ytf: '已退费'
}
return map[s] || '未知'
}
function payStatusTagType(s: string) {
const map: Record<string, string> = {
dfj: 'info',
jfqrz: 'warning',
yjf: 'success',
tfqrz: 'warning',
ytf: 'danger'
}
return (map[s] || 'info') as 'info' | 'warning' | 'success' | 'danger'
}
function transferTypeLabel(t: string) {
const map: Record<string, string> = {
wxzz: '微信转账',
zzbzz: '支付宝转账',
yhkzz: '银行卡转账',
xj: '现金'
}
return map[t] || t || '-'
}
function formatDate(d: string) {
if (!d || d.length !== 8) return d || '-'
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`
@@ -112,9 +112,11 @@
align="center"
header-align="center"
>
<template #default="{ row }">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</template>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="registerDate"
@@ -318,33 +320,10 @@
fetchDeleteCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import { protocolStatusLabel, protocolStatusTagType, newbornSexLabel, deliveryModeLabel } from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageProtocolAudit' })
/* ====== 标签映射 ====== */
function protocolStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return m[s] || s || '-'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '-'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '-'
}
/* ====== 列表 ====== */
const searchForm = reactive<{ keyword: string; protocolStatus: string }>({
keyword: '',
@@ -1211,6 +1211,15 @@
import { fetchEthnicList, type EthnicOption } from '@/api/ethnic'
import { useUserStore } from '@/store/modules/user'
import { syncFqPaymentDetailRowCount } from '@/utils/protocol-payment-installment'
import {
payModeLabel,
newbornSexLabel,
deliveryModeLabel,
sampleStatusLabel,
transportTypeLabel,
transferTypeLabel,
payStatusLabel
} from '@/utils/status-tag-registry'
const userStore = useUserStore()
@@ -1332,47 +1341,6 @@
(e: 'saved'): void
}>()
/* ====== 标签映射 ====== */
function protocolStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return m[s] || s || '-'
}
function payModeLabel(s: string) {
if (s === 'dj') return '趸交'
if (s === 'fq') return '分期'
return s || '-'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '-'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '-'
}
function sampleStatusLabel(s: string) {
const m: Record<string, string> = { ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废' }
return m[s] || s || '-'
}
function transportTypeLabel(s: string) {
if (s === 'kdys') return '快递运输'
if (s === 'ryxd') return '人员携带'
return s || '-'
}
function siteName(id: string) {
if (!id) return '-'
return siteOptions.value.find((x) => x.id === id)?.siteName || '-'
@@ -1411,11 +1379,6 @@
return `样本 ${index + 1}`
}
function transferTypeLabel(s: string) {
const m: Record<string, string> = { wxzz: '微信', zzbzz: '支付宝', yhkzz: '银行卡', xj: '现金' }
return m[s] || s || '-'
}
function formatPaymentDateYmd(val: string) {
const s = String(val || '').trim()
if (s.length === 8 && /^\d{8}$/.test(s)) {
@@ -1464,17 +1427,6 @@
}
}
function payStatusLabel(s: string) {
const m: Record<string, string> = {
dfj: '待缴费',
jfqrz: '缴费确认中',
yjf: '已缴费',
tfqrz: '退费确认中',
ytf: '已退费'
}
return m[s] || s || '-'
}
/* ====== Tab 常量 ====== */
const formTabs = [
{ key: 'basic', label: '基本信息' },
@@ -34,8 +34,9 @@
<div class="kpi-card">
<div class="kpi-label">协议状态</div>
<div class="kpi-value">
<span class="status-dot" :class="statusDotClass(String(protocol?.protocolStatus || ''))" />
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
<el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
</el-tag>
</div>
</div>
<div class="kpi-card">
@@ -53,7 +54,7 @@
<div class="section-title"><h3>基本信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-item"><div class="data-label">协议号</div><div class="data-value">{{ dash(protocol?.protocolNo) }}</div></div>
<div class="data-item"><div class="data-label">协议状态</div><div class="data-value"><span class="badge" :class="statusBadgeClass(String(protocol?.protocolStatus || ''))">{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}</span></div></div>
<div class="data-item"><div class="data-label">协议状态</div><div class="data-value"><el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}</el-tag></div></div>
<div class="data-item"><div class="data-label">登记日期</div><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div>
<div class="data-item"><div class="data-label">采集专员</div><div class="data-value">{{ collectorDisplay }}</div></div>
<div class="data-item"><div class="data-label">创建时间</div><div class="data-value">{{ dash(protocol?.createTime) }}</div></div>
@@ -162,9 +163,9 @@
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
{{ sampleTypeName(row.sampleTypeId) }}
</div>
<span class="badge" :class="sampleStatusBadgeClass(String(row.sampleStatus || ''))">
<el-tag :type="sampleStatusTagType(String(row.sampleStatus || ''))" size="small">
{{ sampleStatusLabel(String(row.sampleStatus || '')) }}
</span>
</el-tag>
</div>
<div class="sample-card__body">
<div class="sample-card__section">
@@ -238,14 +239,9 @@
<div class="data-item">
<div class="data-label">结论</div>
<div class="data-value">
<span
class="badge"
:class="
row.sampleReception.acceptConclusion === 'js' ? 'success' : 'danger'
"
>
<el-tag :type="acceptConclusionTagType(String(row.sampleReception.acceptConclusion || ''))" size="small">
{{ acceptConclusionLabel(String(row.sampleReception.acceptConclusion || '')) }}
</span>
</el-tag>
</div>
</div>
<div class="data-item data-item--full">
@@ -284,6 +280,7 @@
</template>
<div v-else class="sample-reception-empty">暂未接收</div>
</div>
<SampleQcInfoCards :qc-info="row.qcInfo" :sample-reception="row.sampleReception" />
</div>
</div>
</div>
@@ -333,12 +330,9 @@
{{ detail.dueAmount ? `¥${detail.dueAmount}` : '—' }}
</span>
</div>
<span
class="badge"
:class="payDetailBadgeClass(String(detail.payStatus || ''))"
>
<el-tag :type="payStatusTagType(String(detail.payStatus || ''))" size="small">
{{ payStatusLabel(String(detail.payStatus || '')) }}
</span>
</el-tag>
</div>
<!-- Info row -->
@@ -427,7 +421,7 @@
<!-- 缴费确认 -->
<div class="flow-detail-card__kv">
<span class="flow-detail-card__label">确认状态</span>
<span class="flow-detail-card__value badge badge--green">已缴费</span>
<span class="flow-detail-card__value"><el-tag type="success" size="small">已缴费</el-tag></span>
</div>
<div v-if="detail.transferTime" class="flow-detail-card__kv">
<span class="flow-detail-card__label">缴费时间</span>
@@ -446,7 +440,7 @@
<!-- 财务确认 -->
<div class="flow-detail-card__kv">
<span class="flow-detail-card__label">确认状态</span>
<span class="flow-detail-card__value badge badge--green">已确认</span>
<span class="flow-detail-card__value"><el-tag type="success" size="small">已确认</el-tag></span>
</div>
<div v-if="detail.updateTime" class="flow-detail-card__kv">
<span class="flow-detail-card__label">确认时间</span>
@@ -491,7 +485,7 @@
<!-- 退费确认(已退费) -->
<div class="flow-detail-card__kv">
<span class="flow-detail-card__label">确认状态</span>
<span class="flow-detail-card__value badge badge--red">已退费</span>
<span class="flow-detail-card__value"><el-tag type="danger" size="small">已退费</el-tag></span>
</div>
<div v-if="detail.updateTime" class="flow-detail-card__kv">
<span class="flow-detail-card__label">确认时间</span>
@@ -590,12 +584,12 @@
header-align="center"
>
<template #default="{ row }">
<span
class="tag"
:class="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
<el-tag
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
size="small"
>
{{ auditStatusLabel(String(row.auditStatus || '')) }}
</span>
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -650,6 +644,7 @@
import { computed, nextTick, ref, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import {
fetchCollectionProtocolAuditList,
fetchCollectionProtocolDetail,
@@ -665,6 +660,22 @@
fetchUserList
} from '@/api/admin'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
protocolStatusLabel,
protocolStatusTagType,
auditStatusLabel,
payModeLabel,
newbornSexLabel,
deliveryModeLabel,
sampleStatusLabel,
sampleStatusTagType,
transportTypeLabel,
transferTypeLabel,
payStatusLabel,
payStatusTagType,
acceptConclusionLabel,
acceptConclusionTagType
} from '@/utils/status-tag-registry'
const props = withDefaults(
defineProps<{
@@ -821,39 +832,6 @@
return dash(val)
}
function protocolStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return m[s] || s || '—'
}
function statusDotClass(s: string): string {
const m: Record<string, string> = {
dsx: 'pending',
sxz: 'active',
ygq: 'expired',
yzf: 'void',
zky: 'research'
}
return m[s] || ''
}
function statusBadgeClass(s: string): string {
const m: Record<string, string> = {
dsx: 'badge--amber',
sxz: 'badge--green',
ygq: 'badge--gray',
yzf: 'badge--red',
zky: 'badge--purple'
}
return m[s] || 'badge--gray'
}
function payDetailColorClass(payStatus: string): string {
const s = String(payStatus || '')
if (s === 'ytf') return 'red'
@@ -861,15 +839,6 @@
return 'green'
}
function payDetailBadgeClass(payStatus: string): string {
const s = String(payStatus || '')
if (s === 'ytf') return 'badge--red'
if (s === 'tfqrz') return 'badge--amber'
if (s === 'yjf') return 'badge--green'
if (s === 'jfqrz') return 'badge--amber'
return 'badge--gray'
}
interface MiniFlowStep {
key: string
label: string
@@ -1059,73 +1028,12 @@
return `left: calc(${leftPct}% + 14px); width: calc(${widthPct}% - 14px);`
}
function auditStatusLabel(s: string) {
if (s === 'shtg') return '审核通过'
if (s === 'shth') return '审核退回'
return s || '—'
}
function auditorDisplay(row: Record<string, unknown>) {
const name = String(row.auditorUserName || '').trim()
if (name) return name
return dash(row.auditorUserId)
}
function payModeLabel(s: string) {
if (s === 'dj') return '趸交'
if (s === 'fq') return '分期'
return s || '—'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '—'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '—'
}
function sampleStatusLabel(s: string) {
const m: Record<string, string> = { ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废' }
return m[s] || s || '—'
}
function sampleStatusBadgeClass(s: string): string {
const m: Record<string, string> = {
ycj: 'badge--blue',
jcz: 'badge--amber',
yrk: 'badge--green',
yzf: 'badge--red'
}
return m[s] || 'badge--gray'
}
function transportTypeLabel(s: string) {
if (s === 'kdys') return '快递运输'
if (s === 'ryxd') return '人员携带'
return s || '—'
}
function transferTypeLabel(s: string) {
const m: Record<string, string> = { wxzz: '微信', zzbzz: '支付宝', yhkzz: '银行卡', xj: '现金' }
return m[s] || s || '—'
}
function payStatusLabel(s: string) {
const m: Record<string, string> = {
dfj: '待缴费',
jfqrz: '缴费确认中',
yjf: '已缴费',
tfqrz: '退费确认中',
ytf: '已退费'
}
return m[s] || s || '—'
}
function sampleTypeName(id: string) {
return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
}
@@ -1163,12 +1071,6 @@
return '—'
}
function acceptConclusionLabel(v: string) {
if (v === 'js') return '接收'
if (v === 'bj') return '拒收'
return v || '—'
}
function sampleReceptionCheckList(reception: Record<string, any>) {
const list = reception?.checkResults
return Array.isArray(list) ? list : []
@@ -1549,35 +1451,6 @@
gap: 6px;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
&.pending {
background: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
&.active {
background: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
&.expired {
background: #94a3b8;
box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.2);
}
&.void {
background: #ef4444;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
&.research {
background: #8b5cf6;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
}
/* ---- Section Card ---- */
.section-card {
border-radius: 12px;
@@ -1671,41 +1544,6 @@
}
}
/* ---- Badge ---- */
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.6;
&--green {
background: #ecfdf5;
color: #059669;
}
&--amber {
background: #fffbeb;
color: #d97706;
}
&--gray {
background: #f1f5f9;
color: #64748b;
}
&--red {
background: #fef2f2;
color: #dc2626;
}
&--purple {
background: #faf5ff;
color: #7c3aed;
}
&--blue {
background: #eff6ff;
color: #2563eb;
}
}
/* ---- Attachment / Remark Row ---- */
.attachment-row {
margin-top: 12px;
@@ -96,9 +96,11 @@
align="center"
header-align="center"
>
<template #default="{ row }">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</template>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="registerDate"
@@ -276,33 +278,10 @@
fetchDeleteCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import { protocolStatusLabel, protocolStatusTagType, newbornSexLabel, deliveryModeLabel } from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageProtocol' })
/* ====== 标签映射 ====== */
function protocolStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zky: '转科研'
}
return m[s] || s || '-'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '-'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '-'
}
/* ====== 列表 ====== */
const searchForm = reactive<{ keyword: string; protocolStatus: string }>({
keyword: '',
@@ -61,7 +61,7 @@
</el-table-column>
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
<template #default="{ row }">
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
@@ -100,7 +100,9 @@
</el-table-column>
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
<template #default>
{{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
<el-tag :type="activeTab === 'pending' ? 'warning' : 'success'" size="small">
{{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -265,6 +267,7 @@
formatPaymentDueDate,
payModeLabel,
protocolStatusLabel,
protocolStatusTagType,
transferTypeLabel
} from '@/utils/protocol-payment-labels'
@@ -374,6 +374,7 @@
import { Plus, Close, Check, View, Edit, Delete, User, OfficeBuilding } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
import { cleanRoomGradeLabel, cleanRoomGradeTagType } from '@/utils/status-tag-registry'
import {
fetchCleanRoomList,
fetchCreateCleanRoom,
@@ -418,22 +419,9 @@
roomName: [{ required: true, message: '请输入名称', trigger: 'blur' }]
}
// ── 等级映射 ──────────────────────────────────────
const gradeOptions = [
{ label: 'A级', value: 'A' },
{ label: 'B级', value: 'B' },
{ label: 'C级', value: 'C' },
{ label: 'D级', value: 'D' }
]
function gradeTagType(grade: string) {
const map: Record<string, string> = { A: 'warning', B: '', C: 'info', D: 'info' }
return map[grade] ?? ''
}
function gradeLabel(grade: string) {
return gradeOptions.find((o) => o.value === grade)?.label ?? grade
}
// ── 等级映射(使用 registry 函数) ──────────────────
const gradeTagType = cleanRoomGradeTagType
const gradeLabel = cleanRoomGradeLabel
// ── 选择 ──────────────────────────────────────────
const handleSelectionChange = (rows: any[]) => {
@@ -451,7 +439,7 @@
{ label: '编号', value: row.roomCode },
{ label: '名称', value: row.roomName },
{ label: '等级', value: gradeLabel(row.roomGrade) },
{ label: '状态', value: row.roomStatus === 'zc' ? '正常' : '异常' },
{ label: '状态', value: row.roomStatus === 'zc' ? '正常' : '异常', tagType: row.roomStatus === 'zc' ? 'success' : 'danger' },
{ label: '面积', value: row.areaSqm ? `${row.areaSqm} m²` : '' },
{ label: '备注', value: row.remark, span: true }
]
@@ -34,9 +34,9 @@
<div class="kpi-card">
<div class="kpi-label">渠道类型</div>
<div class="kpi-value">
<span class="badge" :class="channelTypeBadgeClass(channel.channelType)">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</span>
</el-tag>
</div>
</div>
<div class="kpi-card">
@@ -63,9 +63,9 @@
<div class="data-item">
<div class="data-label">渠道类型</div>
<div class="data-value">
<span class="badge" :class="channelTypeBadgeClass(channel.channelType)">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</span>
</el-tag>
</div>
</div>
<div class="data-item">
@@ -167,21 +167,21 @@
>
<div class="agreement-card__header">
<div class="agreement-card__tags">
<span class="badge" :class="agreementTypeBadgeClass(agr.agreementType)">
<el-tag :type="agreementTypeTagType(agr.agreementType)" size="small">
{{ agreementTypeLabel(agr.agreementType) }}
</span>
<span class="badge" :class="agreementStatusBadgeClass(agr.status)">
</el-tag>
<el-tag :type="agreementStatusTagType(agr.status)" size="small">
{{ agreementStatusLabel(agr.status) }}
</span>
</el-tag>
</div>
</div>
<div class="data-grid data-grid--4">
<div v-if="agr.settlementType" class="data-item">
<div class="data-label">结算方式</div>
<div class="data-value">
<span class="badge" :class="settlementTypeBadgeClass(agr.settlementType)">
<el-tag :type="settlementTypeTagType(agr.settlementType)" size="small">
{{ settlementTypeLabel(agr.settlementType) }}
</span>
</el-tag>
</div>
</div>
<div v-if="agr.creditLimit != null" class="data-item">
@@ -236,6 +236,16 @@
import { Close } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
channelTypeLabel,
channelTypeTagType,
agreementTypeLabel,
agreementTypeTagType,
agreementStatusLabel,
agreementStatusTagType,
settlementTypeLabel,
settlementTypeTagType
} from '@/utils/status-tag-registry'
import {
fetchChannelInvoices,
fetchChannelMailAddresses,
@@ -259,33 +269,6 @@
{ key: 'agreement', label: '协议信息' }
] as const
const channelTypeMap: Record<string, string> = {
yy: '医院',
hzjg: '合作机构',
zs: '诊所',
zfjg: '政府机构',
qt: '其他'
}
const agreementTypeMap: Record<string, string> = {
kyxy: '框架协议',
zsxy: '中试协议',
lcxy: '临床协议'
}
const settlementTypeMap: Record<string, string> = {
yj: '月结',
jj: '季结',
nj: '年结',
dcjs: '单次结算'
}
const agreementStatusMap: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
yzf: '已作废'
}
const loading = ref(false)
const activeTab = ref('basic')
const invoiceList = ref<any[]>([])
@@ -325,50 +308,6 @@
return String(val)
}
function channelTypeLabel(type: string) {
const key = String(type || '').trim()
if (!key) return '—'
return channelTypeMap[key] || key
}
function channelTypeBadgeClass(type: string): string {
const m: Record<string, string> = {
yy: 'badge--blue',
hzjg: 'badge--green',
zs: 'badge--amber',
zfjg: 'badge--red',
qt: 'badge--gray'
}
return m[String(type || '')] || 'badge--gray'
}
function agreementTypeLabel(type: string) {
return agreementTypeMap[String(type || '')] || dash(type)
}
function agreementTypeBadgeClass(type: string): string {
const m: Record<string, string> = {
kyxy: 'badge--blue',
zsxy: 'badge--green',
lcxy: 'badge--amber'
}
return m[String(type || '')] || 'badge--gray'
}
function settlementTypeLabel(type: string) {
return settlementTypeMap[String(type || '')] || dash(type)
}
function settlementTypeBadgeClass(type: string): string {
const m: Record<string, string> = {
yj: 'badge--gray',
jj: 'badge--green',
nj: 'badge--amber',
dcjs: 'badge--red'
}
return m[String(type || '')] || 'badge--gray'
}
function normalizeAgreementRow(row: Record<string, any>) {
const status = String(row.agreementStatus ?? row.status ?? 'dsx').trim() || 'dsx'
return {
@@ -381,19 +320,6 @@
}
}
function agreementStatusLabel(status: string) {
return agreementStatusMap[String(status || '')] || dash(status)
}
function agreementStatusBadgeClass(status: string): string {
const m: Record<string, string> = {
dsx: 'badge--amber',
sxz: 'badge--green',
yzf: 'badge--red'
}
return m[String(status || '')] || 'badge--gray'
}
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
@@ -660,40 +586,6 @@
}
}
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.6;
&--green {
background: #ecfdf5;
color: #059669;
}
&--amber {
background: #fffbeb;
color: #d97706;
}
&--gray {
background: #f1f5f9;
color: #64748b;
}
&--red {
background: #fef2f2;
color: #dc2626;
}
&--blue {
background: #eff6ff;
color: #2563eb;
}
}
.attachment-row {
margin-top: 12px;
padding-top: 12px;
@@ -90,7 +90,7 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="channelTagType[row.channelType] || 'info'" size="small">
<el-tag :type="channelTypeTagType(row.channelType)" size="small">
{{ channelTypeLabel(row.channelType) }}
</el-tag>
</template>
@@ -198,9 +198,9 @@
style="width: 100%"
>
<el-option
v-for="(label, key) in channelTypeMap"
v-for="(item, key) in channelTypeRegistry"
:key="key"
:label="label"
:label="item.label"
:value="key"
/>
</el-select>
@@ -569,16 +569,16 @@
<div class="protocol-card__header">
<div class="protocol-card__tags">
<el-tag
:type="agreementTypeTagMap[agr.agreementType]?.type || 'info'"
:type="agreementTypeTagType(agr.agreementType)"
size="small"
>
{{ agreementTypeTagMap[agr.agreementType]?.label || agr.agreementType }}
{{ agreementTypeLabel(agr.agreementType) }}
</el-tag>
<el-tag
:type="agreementStatusTagMap[agr.status]?.type || 'info'"
:type="agreementStatusTagType(agr.status)"
size="small"
>
{{ agreementStatusTagMap[agr.status]?.label || agr.status || '—' }}
{{ agreementStatusLabel(agr.status) }}
</el-tag>
</div>
<div class="protocol-card__actions">
@@ -623,10 +623,10 @@
<span class="field-label">结算方式</span>
<span class="field-value">
<el-tag
:type="settlementTypeTagMap[agr.settlementType]?.type || 'info'"
:type="settlementTypeTagType(agr.settlementType)"
size="small"
>
{{ settlementTypeTagMap[agr.settlementType]?.label || agr.settlementType }}
{{ settlementTypeLabel(agr.settlementType) }}
</el-tag>
</span>
</div>
@@ -822,10 +822,10 @@
<div class="form-grid-2">
<el-form-item label="协议状态" class="form-col-span-2">
<el-tag
:type="agreementStatusTagMap[agreementFormStatus]?.type || 'info'"
:type="agreementStatusTagType(agreementFormStatus)"
size="default"
>
{{ agreementStatusTagMap[agreementFormStatus]?.label || agreementFormStatus || '—' }}
{{ agreementStatusLabel(agreementFormStatus) }}
</el-tag>
<span v-if="!agreementIsEdit" class="form-label-tip" style="margin-left: 8px">
保存后为待生效,可在协议卡片点击「生效」变为生效中
@@ -834,7 +834,7 @@
<el-form-item label="协议类型" prop="agreementType">
<el-select v-model="agreementFormData.agreementType" placeholder="请选择协议类型" style="width: 100%">
<el-option
v-for="(item, key) in agreementTypeTagMap"
v-for="(item, key) in agreementType"
:key="key"
:label="item.label"
:value="key"
@@ -844,7 +844,7 @@
<el-form-item label="结算方式" prop="settlementType">
<el-select v-model="agreementFormData.settlementType" placeholder="请选择结算方式" style="width: 100%">
<el-option
v-for="(item, key) in settlementTypeTagMap"
v-for="(item, key) in settlementType"
:key="key"
:label="item.label"
:value="key"
@@ -1047,6 +1047,19 @@
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
channelType as channelTypeRegistry,
channelTypeLabel,
channelTypeTagType,
agreementType,
agreementTypeLabel,
agreementTypeTagType,
agreementStatusLabel,
agreementStatusTagType,
settlementType,
settlementTypeLabel,
settlementTypeTagType
} from '@/utils/status-tag-registry'
import {
fetchChannelList,
fetchCreateChannel,
@@ -1080,26 +1093,6 @@
// ---------------------------------------------------------------------------
// 常量映射
// ---------------------------------------------------------------------------
const channelTypeMap: Record<string, string> = {
yy: '医院',
hzjg: '合作机构',
zs: '诊所',
zfjg: '政府机构',
qt: '其他'
}
const channelTagType: Record<string, string> = {
yy: 'primary',
hzjg: 'success',
zs: 'warning',
zfjg: 'danger',
qt: 'info'
}
function channelTypeLabel(type: string) {
const key = String(type || '').trim()
if (!key) return '—'
return channelTypeMap[key] || key
}
/** 子表列表接口可能直接返回数组,也可能包在 list 中 */
function normalizeSubList(res: unknown): any[] {
@@ -1197,23 +1190,6 @@
return protocolUrlsToAttachments(String(row?.protocolAttachments || ''))
}
const agreementTypeTagMap: Record<string, { label: string; type: string }> = {
kyxy: { label: '框架协议', type: 'primary' },
zsxy: { label: '中试协议', type: 'success' },
lcxy: { label: '临床协议', type: 'warning' }
}
const settlementTypeTagMap: Record<string, { label: string; type: string }> = {
yj: { label: '月结', type: '' },
jj: { label: '季结', type: 'success' },
nj: { label: '年结', type: 'warning' },
dcjs: { label: '单次结算', type: 'danger' }
}
const agreementStatusTagMap: Record<string, { label: string; type: string }> = {
dsx: { label: '待生效', type: 'info' },
sxz: { label: '生效中', type: 'success' },
yzf: { label: '已作废', type: 'danger' }
}
// ---------------------------------------------------------------------------
// 搜索 & 主表
// ---------------------------------------------------------------------------
@@ -34,9 +34,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<span :class="['tag', row.status == '1' ? 'success' : 'muted']">
{{ row.status == '1' ? '正常' : '停用' }}
</span>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -124,9 +122,7 @@
</el-table-column>
<el-table-column label="状态" width="90" align="center" header-align="center">
<template #default="{ row }">
<span :class="['tag', row.status == '1' ? 'success' : 'muted']">
{{ row.status == '1' ? '正常' : '停用' }}
</span>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
@@ -187,9 +183,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<span :class="['tag', row.status == '1' ? 'success' : 'muted']">
{{ row.status == '1' ? '正常' : '停用' }}
</span>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -74,9 +74,7 @@
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" header-align="center" />
<el-table-column prop="tag" label="状态" width="90" align="center" header-align="center">
<template #default="{ row }">
<span :class="['tag', row.status == '1' ? 'success' : 'muted']">
{{ row.status == '1' ? '正常' : '停用' }}
</span>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="400" fixed="right" align="center" header-align="center">
@@ -207,9 +205,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center" header-align="center">
<template #default="{ row }">
<span :class="['tag', row.status == '1' ? 'success' : 'muted']">
{{ row.status == '1' ? '正常' : '停用' }}
</span>
<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 />
@@ -583,7 +579,7 @@
{ label: '机构地址', value: row.institutionAddress, span: true },
{ label: '联系人', value: row.contactName },
{ label: '联系电话', value: row.contactPhone },
{ label: '状态', value: row.status == '1' ? '正常' : '停用' },
{ label: '状态', value: row.status == '1' ? '正常' : '停用', tagType: row.status == '1' ? 'success' : 'info' },
{ label: '备注', value: row.remark, span: true }
]
}
@@ -365,7 +365,8 @@
{ label: '体系编码', value: row.systemCode },
{
label: '状态',
value: String(row.status) === '1' ? '正常' : '停用'
value: String(row.status) === '1' ? '正常' : '停用',
tagType: String(row.status) === '1' ? 'success' : 'info'
},
{ label: '备注', value: row.remark, span: true },
{ label: '创建时间', value: row.createTime }
@@ -398,6 +398,7 @@
fetchAllConsumables
} from '@/api/production'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { packTypeLabel } from '@/utils/status-tag-registry'
defineOptions({ name: 'ProductionConsumablePack' })
@@ -416,12 +417,6 @@
const viewLoading = ref(false)
const viewDetail = ref<any>(null)
function packTypeLabel(packType?: string) {
if (packType === 'bzhcb') return '标准耗材包'
if (packType === 'zdyhcb') return '自定义耗材包'
return packType || '—'
}
const formData = reactive({
packCode: '',
packName: '',
File diff suppressed because it is too large. Load diff
+148 -27
View File
@@ -112,7 +112,7 @@
/>
<el-table-column
prop="estimatedDuration"
label="预估耗时"
label="预估耗时(min)"
min-width="100"
align="center"
header-align="center"
@@ -405,13 +405,27 @@
<div id="form-section-data-collections" class="form-tab-pane">
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>数据采集</h3></div>
<div class="section-title-left">
<h3>数据采集</h3>
<span class="inner-table-sort-tip">拖拽左侧手柄可调整排序</span>
</div>
<el-button class="btn-main" type="primary" size="small" @click="addDataCollectionRow">
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
</div>
<el-table :data="dataCollectionList" stripe class="inner-table">
<el-table
ref="dataCollectionTableRef"
:data="dataCollectionList"
row-key="_clientKey"
stripe
class="inner-table data-collection-sort-table"
>
<el-table-column label="" width="52" align="center" header-align="center" class-name="drag-col">
<template #default>
<el-icon class="data-collection-drag-handle" title="拖拽排序"><Rank /></el-icon>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
@@ -430,15 +444,9 @@
<el-input v-model="row.unit" placeholder="如 °C、mL、%" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="排序号" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-input-number
v-model="row.orderNum"
:min="0"
:max="9999"
controls-position="right"
class="inner-cell-input"
/>
<el-table-column label="排序号" width="88" align="center" header-align="center">
<template #default="{ $index }">
<span class="data-collection-order-num">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
@@ -448,7 +456,7 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="dataCollectionList.splice($index, 1)">删除</el-button>
<el-button link type="danger" size="small" @click="removeDataCollectionRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -622,7 +630,9 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check } from '@element-plus/icons-vue'
import { Plus, Close, Check, Rank } from '@element-plus/icons-vue'
import Sortable from 'sortablejs'
import type { SortableEvent } from 'sortablejs'
import {
fetchProcessList,
fetchProcessDetail,
@@ -712,6 +722,57 @@
const consumableList = ref<any[]>([])
const outputList = ref<any[]>([])
const dataCollectionList = ref<any[]>([])
const dataCollectionTableRef = ref()
let dataCollectionSortable: Sortable | null = null
let dataCollectionKeySeq = 0
function nextDataCollectionClientKey() {
dataCollectionKeySeq += 1
return `dc_${Date.now()}_${dataCollectionKeySeq}`
}
function withDataCollectionClientKey(row: Record<string, any>) {
return {
...row,
_clientKey: row._clientKey || nextDataCollectionClientKey()
}
}
function syncDataCollectionOrderNum() {
dataCollectionList.value.forEach((item, index) => {
item.orderNum = index + 1
})
}
function destroyDataCollectionSortable() {
dataCollectionSortable?.destroy()
dataCollectionSortable = null
}
function initDataCollectionSortable() {
destroyDataCollectionSortable()
nextTick(() => {
const tableEl = dataCollectionTableRef.value?.$el as HTMLElement | undefined
const tbody = tableEl?.querySelector('.el-table__body-wrapper tbody')
if (!tbody || dataCollectionList.value.length === 0) return
dataCollectionSortable = Sortable.create(tbody as HTMLElement, {
handle: '.data-collection-drag-handle',
animation: 150,
ghostClass: 'data-collection-row-ghost',
onEnd: (evt: SortableEvent) => {
const { oldIndex, newIndex } = evt
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
const list = [...dataCollectionList.value]
const [moved] = list.splice(oldIndex, 1)
list.splice(newIndex, 0, moved)
dataCollectionList.value = list
syncDataCollectionOrderNum()
}
})
})
}
// 选择试剂弹窗
const selectReagentVisible = ref(false)
const filteredReagentList = ref<any[]>([])
@@ -881,14 +942,17 @@
resetForm()
await loadReferenceData()
dialogVisible.value = true
nextTick(() => formRef.value?.clearValidate?.())
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
}
async function handleEdit(row: any) {
resetForm()
isEdit.value = true
dialogTitle.value = '编辑工艺'
editingId.value = row.id
resetForm()
await loadReferenceData()
try {
const detail = await fetchProcessDetail(row.id)
@@ -914,15 +978,20 @@
...o,
remark: o.remark ?? ''
}))
dataCollectionList.value = (detail.dataCollections || []).map((d: any) => ({
...d,
remark: d.remark ?? ''
}))
dataCollectionList.value = (detail.dataCollections || []).map((d: any) =>
withDataCollectionClientKey({
...d,
remark: d.remark ?? ''
})
)
} catch {
ElMessage.error('加载详情失败')
}
dialogVisible.value = true
nextTick(() => formRef.value?.clearValidate?.())
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
}
async function handleSubmit() {
@@ -1184,14 +1253,34 @@
}
function addDataCollectionRow() {
dataCollectionList.value.push({
collectionName: '',
unit: '',
orderNum: dataCollectionList.value.length + 1,
remark: ''
})
dataCollectionList.value.push(
withDataCollectionClientKey({
collectionName: '',
unit: '',
orderNum: dataCollectionList.value.length + 1,
remark: ''
})
)
syncDataCollectionOrderNum()
nextTick(() => initDataCollectionSortable())
}
function removeDataCollectionRow(index: number) {
dataCollectionList.value.splice(index, 1)
syncDataCollectionOrderNum()
nextTick(() => initDataCollectionSortable())
}
watch(dialogVisible, (visible) => {
if (!visible) {
destroyDataCollectionSortable()
}
})
onBeforeUnmount(() => {
destroyDataCollectionSortable()
})
// 初始化加载列表 & 分类数据(用于列表列的分类名映射)
onMounted(async () => {
await loadData()
@@ -1286,6 +1375,7 @@
.section-title-left {
display: flex;
align-items: center;
gap: 12px;
&::before {
content: '';
@@ -1320,6 +1410,37 @@
grid-column: 1 / -1;
}
.inner-table-sort-tip {
font-size: 12px;
font-weight: 400;
color: var(--mint-text-muted, #64748b);
}
.data-collection-drag-handle {
cursor: move;
color: #909399;
font-size: 18px;
&:hover {
color: #059669;
}
}
.data-collection-order-num {
display: inline-block;
min-width: 24px;
font-size: 14px;
color: var(--mint-text-muted, #64748b);
line-height: 32px;
}
.data-collection-sort-table {
:deep(.data-collection-row-ghost) {
opacity: 0.55;
background: #ecfdf5 !important;
}
}
.inner-table {
width: 100%;
@@ -58,7 +58,7 @@
<el-table-column prop="description" label="描述" min-width="200" align="center" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">{{ row.status === '1' ? '启用' : '停用' }}</span>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="320" fixed="right" align="center">
@@ -284,7 +284,7 @@
fields: [
{ label: '项目编码', value: row.itemCode },
{ label: '项目名称', value: row.itemName },
{ label: '状态', value: row.status === '1' ? '启用' : '停用' }
{ label: '状态', value: row.status === '1' ? '启用' : '停用', tagType: row.status === '1' ? 'success' : 'info' }
]
},
{
@@ -4,7 +4,7 @@
<div class="page-container">
<header class="page-head">
<h1>质检任务</h1>
<p class="page-desc">按检测项目跟进任务;实验室自检需开始/完成,第三方需上传检测材料后完成。</p>
<p class="page-desc">按检测项目跟进任务;实验室自检需开始后再填写结果,第三方在填写结果时一并上传检测材料。</p>
</header>
<section class="panel">
@@ -16,7 +16,17 @@
<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-input
v-model="searchForm.keyword"
placeholder="任务号/工单号/样本编码/项目"
clearable
/>
</el-form-item>
<el-form-item label="业务类型">
<el-select v-model="searchForm.qcBizType" placeholder="全部" clearable style="width: 100%">
<el-option label="入库质检" value="inbound" />
<el-option label="出库质检" value="outbound" />
</el-select>
</el-form-item>
<el-form-item label="任务类型">
<el-select v-model="searchForm.taskType" placeholder="全部" clearable style="width: 100%">
@@ -45,33 +55,75 @@
<el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
<el-table-column prop="taskType" label="类型" width="110" align="center">
<el-table-column prop="qcBizTypeLabel" label="业务类型" width="100" align="center">
<template #default="{ row }">
<span :class="['tag', row.taskType === 'sysz' ? 'success' : 'muted']">{{ taskTypeLabel(row.taskType) }}</span>
<el-tag type="success" size="small">{{ row.qcBizTypeLabel || qcBizTypeLabel(row.qcBizType || '') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
<el-table-column prop="receptionSourceLabel" label="样本来源" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.receptionSource === 'research_donation' ? 'warning' : 'success'" size="small">
{{ row.receptionSourceLabel || receptionSourceLabel(row.receptionSource || '') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
<template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
</el-table-column>
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center">
<template #default="{ row }">{{ row.sampleCode || '—' }}</template>
</el-table-column>
<el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center">
<template #default="{ row }">{{ row.receptionTime || '—' }}</template>
</el-table-column>
<el-table-column prop="handlerName" label="接收经手人" min-width="110" align="center">
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
</el-table-column>
<el-table-column prop="taskType" label="检测类型" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.taskType === 'sysz' ? 'success' : 'info'" size="small">{{ taskTypeLabel(row.taskType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="itemName" label="检测项目" min-width="160" align="center" />
<el-table-column prop="companyName" label="第三方公司" min-width="120" align="center">
<template #default="{ row }">{{ row.companyName || '—' }}</template>
</el-table-column>
<el-table-column prop="taskStatus" label="状态" width="100" align="center">
<el-table-column prop="conclusion" label="质检结论" width="100" align="center">
<template #default="{ row }">
<span :class="['tag', row.taskStatus === 'ywc' ? 'success' : row.taskStatus === 'jxz' ? 'warning' : 'muted']">
{{ taskStatusLabel(row.taskStatus) }}
</span>
<el-tag
v-if="row.conclusion === 'tg'"
type="success"
size="small"
>
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag
v-else-if="row.conclusion === 'btg'"
type="danger"
size="small"
>
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag v-else type="info" size="small">—</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right" align="center">
<el-table-column prop="taskStatus" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.taskStatus === 'ywc' ? 'success' : row.taskStatus === 'jxz' ? 'warning' : 'info'" size="small">
{{ taskStatusLabel(row.taskStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right" align="center">
<template #default="{ row }">
<template v-if="row.taskType === 'sysz' && row.taskStatus === 'dzx'">
<el-button class="btn-op-item primary" @click="handleStart(row)"><el-icon><VideoPlay /></el-icon>开始</el-button>
</template>
<template v-if="row.taskType === 'sysz' && row.taskStatus === 'jxz'">
<el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
</template>
<template v-if="row.taskType === 'dsfjc' && row.taskStatus !== 'ywc'">
<el-button class="btn-op-item" @click="openAttach(row)"><el-icon><Upload /></el-icon>上传材料</el-button>
<template v-if="canFillResult(row)">
<el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
</template>
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
@@ -90,20 +142,6 @@
</div>
</div>
<el-dialog v-model="attachVisible" title="上传第三方检测材料" width="720px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
<ProtocolFinanceConfirmFields
v-model:finance-remark="attachRemark"
v-model:attachments="attachments"
remark-label="备注"
attachment-label="检测材料"
upload-folder="qc-third-party"
/>
<template #footer>
<el-button class="btn-ghost" @click="attachVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button class="btn-main" type="primary" :loading="attachLoading" @click="saveAttachments"><el-icon><Check /></el-icon>保存</el-button>
</template>
</el-dialog>
<el-dialog
v-model="resultVisible"
title="填写检测结果"
@@ -113,48 +151,93 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top" class="biz-dialog-form">
<el-form v-loading="resultLoading" label-position="top" class="biz-dialog-form">
<div class="form-section">
<div class="section-title">检测参数</div>
<div v-if="resultParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
该项目暂无配置参数
<div
v-if="!resultLoading && resultParams.length === 0"
style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px"
>
该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
</div>
<div class="form-grid-2">
<el-form-item v-for="(param, idx) in resultParams" :key="idx" :label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')">
<el-form-item
v-for="(param, idx) in resultParams"
:key="param.paramId || param.paramCode || idx"
:label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')"
>
<el-input v-if="param.paramType === 'text'" v-model="param.paramValue" placeholder="请输入" />
<el-input-number v-else v-model="param.paramValue" :precision="2" style="width: 100%" placeholder="请输入" />
<el-input-number
v-else
:model-value="toResultNumber(param.paramValue)"
:precision="2"
style="width: 100%"
placeholder="请输入"
@update:model-value="(v) => setResultNumber(param, v)"
/>
</el-form-item>
</div>
</div>
<div v-if="resultTaskType === 'dsfjc'" style="margin-top: 12px">
<div class="form-section result-conclusion-section">
<div class="section-title">检测结论</div>
<el-form-item label="结论" required>
<el-radio-group v-model="resultConclusion">
<el-radio value="tg">通过</el-radio>
<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"
remark-label="备注"
attachment-label="检测材料"
upload-folder="qc-third-party"
:show-remark="false"
:attachment-label="resultAttachmentLabel"
:upload-folder="resultUploadFolder"
/>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="resultVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"><el-icon><Check /></el-icon>提交并完成</el-button>
<el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"><el-icon><Check /></el-icon>{{ resultSubmitLabel }}</el-button>
</template>
</el-dialog>
<SystemViewDialog
v-model:visible="viewVisible"
title="任务详情"
width="960px"
:grid-columns="3"
:icon="Tickets"
:meta="viewDetail?.taskNo || ''"
:sections="viewSections"
>
<template #reception-extra>
<div class="view-reception-attachments">
<div class="view-kv view-kv-span-2">
<dt>接收附件</dt>
<dd>
<ProtocolAttachmentReadonly
:files="viewReceptionAttachmentFiles"
@preview="handleReceptionAttachmentPreview"
@download="handleReceptionAttachmentDownload"
/>
</dd>
</div>
</div>
</template>
<template #append>
<div
v-if="viewDetail?.attachmentFiles?.length"
class="view-section qc-task-view-attachments"
>
<h3>检测材料</h3>
<h3>{{ viewDetail?.taskType === 'dsfjc' ? '检测材料' : '检测附件' }}</h3>
<ProtocolAttachmentReadonly
:files="viewDetail.attachmentFiles"
@preview="handleViewAttachmentPreview"
@@ -170,7 +253,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check, Search, RefreshLeft, VideoPlay, Upload, View, Tickets, EditPen } from '@element-plus/icons-vue'
import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen } from '@element-plus/icons-vue'
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
@@ -183,36 +266,61 @@
fetchQcTaskList,
fetchQcTaskDetail,
fetchStartQcTask,
fetchCompleteQcTask,
fetchUpdateQcTaskAttachments,
type QcTask,
type QcTaskDetail
} from '@/api/qc-task'
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
import { taskTypeLabel, taskStatusLabel } from '@/utils/qc-labels'
import {
taskTypeLabel,
taskStatusLabel,
qcBizTypeLabel,
receptionSourceLabel,
acceptConclusionLabel,
transportTypeLabel,
workOrderStatusLabel,
qcTaskConclusionLabel
} from '@/utils/qc-labels'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'QualityInspectionQcTask' })
const searchForm = reactive({ keyword: '', taskType: '', taskStatus: '' })
const searchForm = reactive({ keyword: '', qcBizType: '', taskType: '', taskStatus: '' })
const tableData = ref<QcTask[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const attachVisible = ref(false)
const attachLoading = ref(false)
const attachTaskId = ref('')
const attachRemark = ref('')
const attachments = ref<FinanceAttachmentItem[]>([])
const resultVisible = ref(false)
const resultLoading = ref(false)
const resultTaskId = ref('')
const resultTaskType = ref('')
const resultTaskStatus = ref('')
const resultParams = ref<QcTaskResultItem[]>([])
const resultRemark = ref('')
const resultConclusion = ref('')
const resultAttachments = ref<FinanceAttachmentItem[]>([])
function canFillResult(row: QcTask) {
if (row.taskType === 'sysz') {
return row.taskStatus === 'jxz' || row.taskStatus === 'ywc'
}
if (row.taskType === 'dsfjc') {
return row.taskStatus === 'dzx' || row.taskStatus === 'jxz' || row.taskStatus === 'ywc'
}
return false
}
const resultSubmitLabel = computed(() =>
resultTaskStatus.value === 'ywc' ? '保存' : '提交并完成'
)
const resultAttachmentLabel = computed(() =>
resultTaskType.value === 'dsfjc' ? '检测材料' : '检测附件'
)
const resultUploadFolder = computed(() =>
resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
)
const viewVisible = ref(false)
const viewDetail = ref<QcTaskDetail | null>(null)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
@@ -245,31 +353,163 @@
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
const viewReceptionAttachmentFiles = computed(() => {
const r = viewDetail.value?.reception
if (!r) return []
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
}
const urls = String(r.receiveAttachments || '')
if (!urls.trim()) return []
return urls
.split(',')
.map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
})
function handleReceptionAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
function normalizeResultParams(items: QcTaskResultItem[]): QcTaskResultItem[] {
return (items || []).map((p) => ({
...p,
paramType: p.paramType || 'text',
paramValue: p.paramValue ?? ''
}))
}
function toResultNumber(v: string | undefined) {
if (v === '' || v === undefined || v === null) return undefined
const n = Number(v)
return Number.isFinite(n) ? n : undefined
}
function setResultNumber(param: QcTaskResultItem, v: number | undefined) {
param.paramValue = v === undefined || v === null ? '' : String(v)
}
function hasResultValue(v: unknown) {
return v !== '' && v !== undefined && v !== null
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
return String(v)
}
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
if (!items?.length) return '—'
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
}
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
const r = d.reception || {}
const transportFields: ViewSection['fields'] = [
{ label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
]
if (r.transportType === 'kdys') {
transportFields.push(
{ label: '快递公司', value: r.expressCompanyName || '—' },
{ label: '快递单号', value: r.waybillNo || '—' }
)
} else if (r.transportType === 'ryxd') {
transportFields.push(
{ label: '携带人', value: r.carrierName || '—' },
{ label: '携带人电话', value: r.carrierPhone || '—' }
)
}
if (r.etaTime) {
transportFields.push({ label: '预计到达', value: r.etaTime })
}
const resultSection =
(d as any).results && (d as any).results.length > 0
d.results && d.results.length > 0
? [
{
title: '检测结果',
fields: (d as any).results.map((r: any) => ({
label: r.paramName + (r.paramUnit ? `(${r.paramUnit})` : ''),
value: r.paramValue || '—'
fields: d.results.map((item) => ({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
}))
}
]
: []
return [
{
title: '基本信息',
title: '任务信息',
fields: [
{ label: '任务号', value: d.taskNo },
{ label: '业务类型', value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''), tagType: 'success' },
{ label: '工单号', value: d.workOrderNo },
{ label: '类型', value: taskTypeLabel(d.taskType) },
{
label: '工单状态',
value: d.workOrderStatus ? workOrderStatusLabel(d.workOrderStatus) : '—',
tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info'
},
{ label: '检测类型', value: taskTypeLabel(d.taskType), tagType: d.taskType === 'sysz' ? 'success' : 'info' },
{ label: '检测项目', value: d.itemName },
{ label: '项目编码', value: d.itemCode || '—' },
{ label: '第三方公司', value: d.companyName || '—' },
{ label: '状态', value: taskStatusLabel(d.taskStatus) }
{ label: '检测费用', value: d.feeAmount != null ? `¥${d.feeAmount}` : '—' },
{ label: '任务状态', value: taskStatusLabel(d.taskStatus), tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info' },
{
label: '检测结论',
value: d.conclusionLabel || qcTaskConclusionLabel(d.conclusion || ''),
tagType: d.conclusion === 'tg' ? 'success' : d.conclusion === 'btg' ? 'danger' : 'info'
},
{ label: '完成时间', value: d.completedTime || '—' },
{ label: '创建时间', value: d.createTime || '—' },
{ label: '备注', value: d.remark || '—', span: true }
]
},
{
title: '样本信息',
fields: [
{ label: '协议号', value: r.protocolNo || d.protocolNo || '—' },
{ label: '样本编码', value: r.sampleCode || d.sampleCode || '—' },
{
label: '样本来源',
value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary'
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
]
},
...(transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []),
{
title: '样本接收',
slot: 'reception-extra',
fields: [
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
{
label: '接收温度(℃)',
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
},
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
{
label: '结论',
value:
r.acceptConclusionLabel ||
acceptConclusionLabel(String(r.acceptConclusion || ''))
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
},
...resultSection
@@ -283,6 +523,7 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
qcBizType: searchForm.qcBizType || undefined,
taskType: searchForm.taskType || undefined,
taskStatus: searchForm.taskStatus || undefined
})
@@ -299,6 +540,7 @@
}
function handleReset() {
searchForm.keyword = ''
searchForm.qcBizType = ''
searchForm.taskType = ''
searchForm.taskStatus = ''
pagination.page = 1
@@ -311,46 +553,6 @@
loadData()
}
async function handleComplete(row: QcTask) {
await fetchCompleteQcTask(row.id)
ElMessage.success('已完成')
loadData()
}
async function openAttach(row: QcTask) {
attachTaskId.value = row.id
attachRemark.value = row.remark || ''
attachments.value = apiFilesToFinanceItems(row.attachmentFiles)
attachVisible.value = true
if (attachments.value.length) return
try {
const detail = await fetchQcTaskDetail(row.id)
attachRemark.value = detail.remark || ''
attachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
} catch {
/* 列表未带附件时拉详情 */
}
}
async function saveAttachments() {
if (attachments.value.some((a) => a._uploading)) {
ElMessage.warning('附件正在上传,请稍候')
return
}
attachLoading.value = true
try {
await fetchUpdateQcTaskAttachments(
attachTaskId.value,
financeAttachmentsToUrlString(attachments.value)
)
ElMessage.success('已保存')
attachVisible.value = false
loadData()
} finally {
attachLoading.value = false
}
}
async function handleView(row: QcTask) {
const detail = await fetchQcTaskDetail(row.id)
try {
@@ -364,23 +566,29 @@
}
async function openResult(row: QcTask) {
if (!canFillResult(row)) {
if (row.taskType === 'sysz') {
ElMessage.warning('请先开始任务后再填写结果')
} else {
ElMessage.warning('当前状态不可填写结果')
}
return
}
resultTaskStatus.value = row.taskStatus
resultTaskId.value = row.id
resultTaskType.value = row.taskType
resultRemark.value = row.remark || ''
resultConclusion.value = ''
resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
resultLoading.value = true
try {
if (row.taskType === 'dsfjc' && !resultAttachments.value.length) {
const detail = await fetchQcTaskDetail(row.id)
resultRemark.value = detail.remark || ''
resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
}
const detail = await fetchQcTaskDetail(row.id)
resultTaskStatus.value = detail.taskStatus || row.taskStatus
resultRemark.value = detail.remark || row.remark || ''
resultConclusion.value = detail.conclusion || ''
resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
const results = await fetchQcTaskResults(row.id)
if (results && results.length > 0) {
resultParams.value = results
} else {
resultParams.value = []
}
resultParams.value = normalizeResultParams(results || [])
} catch {
resultParams.value = []
} finally {
@@ -394,16 +602,34 @@
ElMessage.warning('附件正在上传,请稍候')
return
}
if (!resultConclusion.value) {
ElMessage.warning('请选择检测结论')
return
}
if (resultTaskType.value === 'dsfjc' && !financeAttachmentsToUrlString(resultAttachments.value)) {
ElMessage.warning('请上传第三方检测材料')
return
}
resultLoading.value = true
try {
const data: { results: QcTaskResultItem[]; thirdPartyAttachments?: string } = {
results: resultParams.value.filter((p) => p.paramValue !== '' && p.paramValue !== undefined)
}
if (resultTaskType.value === 'dsfjc') {
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
const data: {
conclusion: string
results: QcTaskResultItem[]
remark?: string
thirdPartyAttachments?: string
} = {
conclusion: resultConclusion.value,
remark: resultRemark.value,
results: resultParams.value
.filter((p) => hasResultValue(p.paramValue))
.map((p) => ({
...p,
paramValue: p.paramValue === undefined || p.paramValue === null ? '' : String(p.paramValue)
}))
}
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
await fetchCompleteTaskWithResult(resultTaskId.value, data)
ElMessage.success('已完成')
ElMessage.success(resultTaskStatus.value === 'ywc' ? '已保存' : '已完成')
resultVisible.value = false
loadData()
} finally {
@@ -432,6 +658,21 @@
border-left: 3px solid #10b981;
}
.result-conclusion-section {
margin-top: 12px;
}
.result-remark-section {
margin-top: 12px;
}
.result-attachment-section {
margin-top: 12px;
background: #fafafa;
border-radius: 8px;
padding: 20px;
}
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
@@ -452,4 +693,27 @@
line-height: 1;
}
}
.view-reception-attachments {
margin-top: 4px;
.view-kv {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
font-size: 13px;
align-items: start;
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
}
}
}
</style>
@@ -1,10 +1,10 @@
<!-- 质量检测 — 质检工单 -->
<!-- 质量检测 — 入库质检工单 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>质检工单</h1>
<p class="page-desc">样本接收(结论为接收)后自动生成工单;在此配置检测方式与具体项目。</p>
<h1>入库质检工单</h1>
<p class="page-desc">样本接收(结论为接收)后自动生成工单;在此配置或调整检测方式与具体项目。</p>
</header>
<section class="panel">
@@ -41,25 +41,45 @@
<el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" />
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center" />
<el-table-column prop="receptionSourceLabel" label="来源类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.receptionSource === 'research_donation' ? 'warning' : 'success'" size="small">
{{ row.receptionSourceLabel || receptionSourceLabel(row.receptionSource || '') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
<template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
</el-table-column>
<el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center">
<template #default="{ row }">{{ row.receptionTime || '—' }}</template>
</el-table-column>
<el-table-column prop="handlerName" label="接收经手人" min-width="110" align="center">
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
</el-table-column>
<el-table-column prop="workOrderStatus" label="状态" width="100" align="center">
<template #default="{ row }">
<span :class="['tag', row.workOrderStatus === 'ywc' ? 'success' : row.workOrderStatus === 'ypz' ? 'warning' : 'muted']">
<el-tag :type="row.workOrderStatus === 'ywc' ? 'success' : row.workOrderStatus === 'ypz' ? 'warning' : 'info'" size="small">
{{ workOrderStatusLabel(row.workOrderStatus) }}
</span>
</el-tag>
</template>
</el-table-column>
<el-table-column label="检测方式" min-width="160" align="center">
<template #default="{ row }">
<span v-if="row.hasLab" class="tag success">实验室</span>
<span v-if="row.hasThirdParty" class="tag muted" style="margin-left: 4px">第三方</span>
<span v-if="!row.hasLab && !row.hasThirdParty">—</span>
<span>{{ formatDetectMethods(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="row.workOrderStatus === 'dpz'" class="btn-op-item primary" @click="handleConfigure(row)"><el-icon><Setting /></el-icon>配置检测</el-button>
<el-button
v-if="row.workOrderStatus === 'dpz' || row.workOrderStatus === 'ypz'"
class="btn-op-item primary"
@click="handleConfigure(row)"
>
<el-icon><Setting /></el-icon>{{ row.workOrderStatus === 'ypz' ? '调整检测' : '配置检测' }}
</el-button>
</template>
</el-table-column>
</el-table>
@@ -75,7 +95,15 @@
</div>
</div>
<el-dialog v-model="configureVisible" title="配置检测" width="800px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
<el-dialog
v-model="configureVisible"
:title="configureIsEdit ? '调整检测' : '配置检测'"
width="800px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top" class="biz-dialog-form">
<el-form-item label="检测方式">
<el-checkbox v-model="configureForm.hasLab">实验室自检</el-checkbox>
@@ -113,7 +141,22 @@
:icon="Document"
:meta="detail?.workOrderNo || ''"
:sections="viewSections"
/>
>
<template #reception-extra>
<div class="view-reception-attachments">
<div class="view-kv view-kv-span-2">
<dt>接收附件</dt>
<dd>
<ProtocolAttachmentReadonly
:files="viewReceptionAttachmentFiles"
@preview="handleReceptionAttachmentPreview"
@download="handleReceptionAttachmentDownload"
/>
</dd>
</div>
</div>
</template>
</SystemViewDialog>
</section>
</div>
</template>
@@ -131,9 +174,16 @@
type QcWorkOrder,
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { workOrderStatusLabel } from '@/utils/qc-labels'
import {
workOrderStatusLabel,
receptionSourceLabel,
acceptConclusionLabel,
transportTypeLabel
} from '@/utils/qc-labels'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'QualityInspectionQcWorkOrder' })
@@ -144,6 +194,7 @@
const configureVisible = ref(false)
const configureLoading = ref(false)
const configureIsEdit = ref(false)
const configuringId = ref('')
const labOptions = ref<QcLabTestItem[]>([])
const thirdOptions = ref<QcThirdPartyItem[]>([])
@@ -167,10 +218,91 @@
const viewVisible = ref(false)
const detail = ref<QcWorkOrderDetail | null>(null)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewReceptionAttachmentFiles = computed(() => {
const r = detail.value?.reception
if (!r) return []
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
}
const urls = String(r.receiveAttachments || '')
if (!urls.trim()) return []
return urls
.split(',')
.map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
})
function receptionAttachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleReceptionAttachmentPreview(file: Record<string, unknown>) {
const url = receptionAttachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: receptionAttachmentFullUrl })
}
function formatDetectMethods(row: QcWorkOrder) {
const parts: string[] = []
if (row.hasLab) parts.push('实验室')
if (row.hasThirdParty) parts.push('第三方')
return parts.length ? parts.join(' \\ ') : '—'
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
return String(v)
}
function formatCheckResults(
items?: Array<{ itemName?: string; checked?: boolean }>
) {
if (!items?.length) return '—'
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
}
const viewSections = computed<ViewSection[]>(() => {
if (!detail.value) return []
const d = detail.value
const r = d.reception || {}
const transportFields: ViewSection['fields'] = [
{
label: '运输方式',
value: transportTypeLabel(String(r.transportType || ''))
}
]
if (r.transportType === 'kdys') {
transportFields.push(
{ label: '快递公司', value: r.expressCompanyName || '—' },
{ label: '快递单号', value: r.waybillNo || '—' }
)
} else if (r.transportType === 'ryxd') {
transportFields.push(
{ label: '携带人', value: r.carrierName || '—' },
{ label: '携带人电话', value: r.carrierPhone || '—' }
)
}
if (r.etaTime) {
transportFields.push({ label: '预计到达', value: r.etaTime })
}
const transportSection =
transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
return [
{
title: '基本信息',
@@ -178,7 +310,36 @@
{ label: '工单号', value: d.workOrderNo },
{ label: '协议号', value: d.protocolNo },
{ label: '样本编码', value: d.sampleCode },
{ label: '状态', value: workOrderStatusLabel(d.workOrderStatus) }
{
label: '来源类型',
value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary'
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' },
{ label: '状态', value: workOrderStatusLabel(d.workOrderStatus), tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info' },
{ label: '创建时间', value: d.createTime || '—' }
]
},
...transportSection,
{
title: '样本接收',
slot: 'reception-extra',
fields: [
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
{
label: '接收温度(℃)',
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
},
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
{
label: '结论',
value: r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')),
tagType: r.acceptConclusion === 'js' ? 'success' : r.acceptConclusion === 'jj' ? 'danger' : 'info'
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
},
{
@@ -235,12 +396,23 @@
async function handleConfigure(row: QcWorkOrder) {
configuringId.value = row.id
configureForm.hasLab = false
configureForm.hasThirdParty = false
configureForm.labItemIds = []
configureForm.thirdPartyItemIds = []
configureIsEdit.value = row.workOrderStatus === 'ypz'
labOptions.value = (await fetchQcLabTestItemsActive()) || []
thirdOptions.value = (await fetchQcThirdPartyItemsActive()) || []
if (configureIsEdit.value) {
const d = await fetchQcWorkOrderDetail(row.id)
configureForm.hasLab = !!d.hasLab
configureForm.hasThirdParty = !!d.hasThirdParty
configureForm.labItemIds = (d.labItems || []).map((li) => li.labTestItemId).filter(Boolean)
configureForm.thirdPartyItemIds = (d.thirdPartyItems || [])
.map((li) => li.thirdPartyItemId)
.filter(Boolean)
} else {
configureForm.hasLab = false
configureForm.hasThirdParty = false
configureForm.labItemIds = []
configureForm.thirdPartyItemIds = []
}
configureVisible.value = true
}
@@ -252,7 +424,7 @@
configureLoading.value = true
try {
await fetchConfigureQcWorkOrder(configuringId.value, { ...configureForm })
ElMessage.success('配置成功,已生成质检任务')
ElMessage.success(configureIsEdit.value ? '检测项目已更新' : '配置成功,已生成质检任务')
configureVisible.value = false
loadData()
} finally {
@@ -302,4 +474,27 @@
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.view-reception-attachments {
margin-top: 4px;
.view-kv {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
font-size: 13px;
align-items: start;
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
}
}
}
</style>
@@ -81,9 +81,7 @@
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
<el-table-column prop="status" label="状态" width="90" align="center" header-align="center">
<template #default="{ row }">
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
{{ row.status === '1' ? '启用' : '停用' }}
</span>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
@@ -40,6 +40,12 @@
clearable
/>
</el-form-item>
<el-form-item label="来源类型">
<el-select v-model="searchForm.sourceType" placeholder="全部" clearable>
<el-option label="细胞存储" value="cell_storage" />
<el-option label="科研捐赠" value="research_donation" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">
@@ -99,6 +105,13 @@
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
<el-table-column prop="sourceTypeLabel" label="来源类型" width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.sourceType === 'research_donation' ? 'warning' : 'success'" size="small">
{{ row.sourceTypeLabel || '细胞存储' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.sampleCode || '-' }}</template>
</el-table-column>
@@ -114,9 +127,21 @@
<el-table-column prop="etaTime" label="预计到达时间" min-width="170" align="center" header-align="center">
<template #default="{ row }">{{ row.etaTime || '-' }}</template>
</el-table-column>
<el-table-column prop="transportType" label="运输方式" width="100" align="center" header-align="center">
<template #default="{ row }">{{ transportTypeLabel(String(row.transportType || '')) }}</template>
</el-table-column>
<el-table-column prop="waybillNo" label="快递单号" min-width="140" align="center" header-align="center">
<template #default="{ row }">{{ row.waybillNo || '-' }}</template>
</el-table-column>
<el-table-column prop="expressCompanyName" label="快递公司" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.expressCompanyName || '-' }}</template>
</el-table-column>
<el-table-column prop="carrierName" label="携带人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.carrierName || '-' }}</template>
</el-table-column>
<el-table-column prop="carrierPhone" label="携带人联系电话" min-width="130" align="center" header-align="center">
<template #default="{ row }">{{ row.carrierPhone || '-' }}</template>
</el-table-column>
<el-table-column
v-if="activeTab === 'received'"
prop="receptionTime"
@@ -144,9 +169,9 @@
header-align="center"
>
<template #default="{ row }">
<span :class="['tag', row.acceptConclusion === 'js' ? 'success' : 'danger']">
<el-tag :type="row.acceptConclusion === 'js' ? 'success' : 'danger'" size="small">
{{ acceptConclusionLabel(String(row.acceptConclusion || '')) }}
</span>
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center" header-align="center">
@@ -196,6 +221,22 @@
</div>
</div>
</div>
<div v-if="currentRow" class="form-section-divider" />
<h4 class="detail-section-title">运输信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item">
<span class="label">运输方式</span>{{ transportTypeLabel(String(currentRow?.transportType || '')) }}
</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ currentRow?.etaTime || '-' }}</div>
<template v-if="currentRow?.transportType === 'kdys'">
<div class="readonly-item"><span class="label">快递公司</span>{{ currentRow?.expressCompanyName || '-' }}</div>
<div class="readonly-item"><span class="label">快递单号</span>{{ currentRow?.waybillNo || '-' }}</div>
</template>
<template v-else-if="currentRow?.transportType === 'ryxd'">
<div class="readonly-item"><span class="label">携带人</span>{{ currentRow?.carrierName || '-' }}</div>
<div class="readonly-item"><span class="label">携带人电话</span>{{ currentRow?.carrierPhone || '-' }}</div>
</template>
</div>
<div class="form-section-divider" />
<h4 class="detail-section-title">接收登记</h4>
<el-form
@@ -308,6 +349,22 @@
<div class="readonly-item"><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div>
</div>
<div class="form-section-divider" />
<h4 class="detail-section-title">运输信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item">
<span class="label">运输方式</span>{{ transportTypeLabel(String(viewRow.transportType || '')) }}
</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div>
<template v-if="viewRow.transportType === 'kdys'">
<div class="readonly-item"><span class="label">快递公司</span>{{ viewRow.expressCompanyName || '-' }}</div>
<div class="readonly-item"><span class="label">快递单号</span>{{ viewRow.waybillNo || '-' }}</div>
</template>
<template v-else-if="viewRow.transportType === 'ryxd'">
<div class="readonly-item"><span class="label">携带人</span>{{ viewRow.carrierName || '-' }}</div>
<div class="readonly-item"><span class="label">携带人电话</span>{{ viewRow.carrierPhone || '-' }}</div>
</template>
</div>
<div class="form-section-divider" />
<h4 class="detail-section-title">接收信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item"><span class="label">接收时间</span>{{ viewRow.receptionTime || '-' }}</div>
@@ -316,7 +373,7 @@
<div class="readonly-item"><span class="label">体积(ml)</span>{{ formatMetric(viewRow.volumeMl) }}</div>
<div class="readonly-item"><span class="label">净重(g)</span>{{ formatMetric(viewRow.netWeightG) }}</div>
<div class="readonly-item">
<span class="label">结论</span>{{ acceptConclusionLabel(String(viewRow.acceptConclusion || '')) }}
<span class="label">结论</span><el-tag :type="viewRow.acceptConclusion === 'js' ? 'success' : 'danger'" size="small">{{ acceptConclusionLabel(String(viewRow.acceptConclusion || '')) }}</el-tag>
</div>
<div class="readonly-item form-col-span-2 check-items-readonly">
<span class="label">检查项</span>
@@ -378,6 +435,13 @@
<div class="readonly-item form-col-span-2">
<span class="label">客户</span>{{ entry.motherName || '—' }} / {{ entry.newbornName || '—' }}
</div>
<div class="readonly-item">
<span class="label">来源</span>
<el-tag :type="entry.sourceType === 'research_donation' ? 'warning' : 'success'" size="small">
{{ entry.sourceType === 'research_donation' ? '科研捐赠' : '细胞存储' }}
</el-tag>
</div>
<div class="readonly-item"><span class="label">运输方式</span>{{ entry.transportType === 'kdys' ? '快递运输' : entry.transportType === 'ryxd' ? '人员携带' : '-' }}</div>
</div>
<el-form label-position="top" class="biz-dialog-form">
<div class="form-grid-2">
@@ -494,6 +558,7 @@
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
import { useUserStore } from '@/store/modules/user'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { transportTypeLabel, acceptConclusionLabel } from '@/utils/status-tag-registry'
defineOptions({ name: 'QualityInspectionSampleReception' })
@@ -504,7 +569,7 @@
]
const activeTab = ref('pending')
const searchForm = reactive({ keyword: '' })
const searchForm = reactive({ keyword: '', sourceType: '' })
const tableData = ref<Record<string, unknown>[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -530,6 +595,8 @@
waybillNo: string
motherName: string
newbornName: string
sourceType: 'cell_storage' | 'research_donation'
transportType: string
receptionTime: string
handlerUserId: string
handlerName: string
@@ -590,12 +657,6 @@
.filter(Boolean) as Array<Record<string, unknown>>
})
function acceptConclusionLabel(v: string) {
if (v === 'js') return '接收'
if (v === 'bj') return '拒收'
return v || '-'
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '-'
const n = Number(v)
@@ -685,6 +746,8 @@
waybillNo: String(row.waybillNo || ''),
motherName: String(row.motherName || ''),
newbornName: String(row.newbornName || ''),
sourceType: (row.sourceType as 'cell_storage' | 'research_donation') || 'cell_storage',
transportType: String(row.transportType || ''),
receptionTime: formatNowDatetime(),
handlerUserId: userId,
handlerName: label,
@@ -766,7 +829,13 @@
for (const entry of batchEntries.value) {
try {
await fetchSampleReceptionSubmit({
protocolSampleId: entry.protocolSampleId,
sourceType: entry.sourceType,
donationSampleId: entry.sourceType === 'research_donation'
? entry.protocolSampleId
: undefined,
protocolSampleId: entry.sourceType !== 'research_donation'
? entry.protocolSampleId
: undefined,
receptionTime: entry.receptionTime,
handlerUserId: entry.handlerUserId,
handlerName: entry.handlerName,
@@ -819,7 +888,8 @@
const params = {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
keyword: searchForm.keyword || undefined,
sourceType: searchForm.sourceType || undefined,
}
const res =
activeTab.value === 'pending'
@@ -847,6 +917,7 @@
function handleReset() {
searchForm.keyword = ''
searchForm.sourceType = ''
pagination.page = 1
loadData()
}
@@ -906,7 +977,13 @@
submitLoading.value = true
try {
await fetchSampleReceptionSubmit({
protocolSampleId: String(currentRow.value.protocolSampleId),
sourceType: (currentRow.value.sourceType as string) || 'cell_storage',
donationSampleId: currentRow.value.sourceType === 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
protocolSampleId: currentRow.value.sourceType !== 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
receptionTime: receiveForm.receptionTime,
handlerUserId: receiveForm.handlerUserId,
handlerName: receiveForm.handlerName,
@@ -73,9 +73,7 @@
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
{{ row.status === '1' ? '启用' : '停用' }}
</span>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="400" fixed="right" align="center">
@@ -208,9 +206,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
{{ row.status === '1' ? '启用' : '停用' }}
</span>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right" align="center">
@@ -537,7 +533,7 @@
{ label: '联系人', value: row.contactName },
{ label: '联系电话', value: row.contactPhone },
{ label: '地址', value: row.address, span: true },
{ label: '状态', value: row.status === '1' ? '启用' : '停用' },
{ label: '状态', value: row.status === '1' ? '启用' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{ label: '备注', value: row.remark, span: true }
]
},
@@ -128,9 +128,11 @@
align="center"
header-align="center"
>
<template #default="{ row }">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</template>
<template #default="{ row }">
<el-tag :type="donationStatusTagType(String(row.donationStatus || ''))" size="small">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="donationDate"
@@ -332,33 +334,15 @@
fetchDeleteResearchDonation,
type ResearchDonationRow
} from '@/api/research-donation'
import {
donationStatusLabel,
donationStatusTagType,
newbornSexLabel,
deliveryModeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'ResearchDonationAudit' })
/* ====== 标签映射 ====== */
function donationStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zckc: '转细胞存储'
}
return m[s] || s || '-'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '-'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '-'
}
/* ====== 列表 ====== */
const searchForm = reactive<{ keyword: string; donationStatus: string }>({
keyword: '',
@@ -937,6 +937,7 @@
} from '@/api/research-donation'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { transportTypeLabel } from '@/utils/status-tag-registry'
import {
fetchCustomerList,
fetchUserList,
@@ -1072,12 +1073,6 @@
(e: 'saved'): void
}>()
function transportTypeLabel(s: string) {
if (s === 'kdys') return '快递运输'
if (s === 'ryxd') return '人员携带'
return s || '-'
}
function siteName(id: string) {
if (!id) return '-'
return siteOptions.value.find((x) => x.id === id)?.siteName || '-'
@@ -34,11 +34,9 @@
<div class="kpi-card">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<span
class="status-dot"
:class="statusDotClass(String(donation?.donationStatus || ''))"
/>
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
<el-tag :type="donationStatusTagType(String(donation?.donationStatus || ''))" size="small">
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
</el-tag>
</div>
</div>
<div class="kpi-card">
@@ -62,11 +60,7 @@
<div class="data-item"
><div class="data-label">状态</div
><div class="data-value"
><span
class="badge"
:class="statusBadgeClass(String(donation?.donationStatus || ''))"
>{{ donationStatusLabel(String(donation?.donationStatus || '')) }}</span
></div
><el-tag :type="donationStatusTagType(String(donation?.donationStatus || ''))" size="small">{{ donationStatusLabel(String(donation?.donationStatus || '')) }}</el-tag></div
></div
>
<div class="data-item"
@@ -307,9 +301,9 @@
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
{{ sampleTypeName(row.sampleTypeId) }}
</div>
<span class="badge" :class="sampleStatusBadgeClass(String(row.sampleStatus || ''))">
<el-tag :type="sampleStatusTagType(String(row.sampleStatus || ''))" size="small">
{{ sampleStatusLabel(String(row.sampleStatus || '')) }}
</span>
</el-tag>
</div>
<div class="sample-card__body">
<div class="sample-card__section">
@@ -363,6 +357,7 @@
</div>
</div>
</div>
<SampleQcInfoCards :qc-info="row.qcInfo" :sample-reception="row.sampleReception" />
</div>
</div>
</div>
@@ -433,12 +428,12 @@
header-align="center"
>
<template #default="{ row }">
<span
class="tag"
:class="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
<el-tag
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
size="small"
>
{{ auditStatusLabel(String(row.auditStatus || '')) }}
</span>
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -493,6 +488,7 @@
import { computed, nextTick, ref, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import {
fetchResearchDonationAuditList,
fetchResearchDonationDetail,
@@ -508,6 +504,18 @@
fetchUserList
} from '@/api/admin'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
donationStatusLabel,
donationStatusTagType,
sourceTypeLabel,
auditStatusLabel,
auditStatusTagType,
newbornSexLabel,
deliveryModeLabel,
sampleStatusLabel,
sampleStatusTagType,
transportTypeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'DonationViewDialog' })
@@ -666,90 +674,12 @@
return dash(val)
}
function donationStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zckc: '转细胞存储'
}
return m[s] || s || '—'
}
function statusDotClass(s: string): string {
const m: Record<string, string> = {
dsx: 'pending',
sxz: 'active',
ygq: 'expired',
yzf: 'void',
zckc: 'research'
}
return m[s] || ''
}
function statusBadgeClass(s: string): string {
const m: Record<string, string> = {
dsx: 'badge--amber',
sxz: 'badge--green',
ygq: 'badge--gray',
yzf: 'badge--red',
zckc: 'badge--purple'
}
return m[s] || 'badge--gray'
}
function sourceTypeLabel(s: string) {
if (s === 'direct') return '独立新建'
if (s === 'from_storage') return '从存储转来'
return s || '—'
}
function auditStatusLabel(s: string) {
if (s === 'shtg') return '审核通过'
if (s === 'shth') return '审核退回'
return s || '—'
}
function auditorDisplay(row: Record<string, unknown>) {
const name = String(row.auditorUserName || '').trim()
if (name) return name
return dash(row.auditorUserId)
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '—'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '—'
}
function sampleStatusLabel(s: string) {
const m: Record<string, string> = { ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废' }
return m[s] || s || '—'
}
function sampleStatusBadgeClass(s: string): string {
const m: Record<string, string> = {
ycj: 'badge--blue',
jcz: 'badge--amber',
yrk: 'badge--green',
yzf: 'badge--red'
}
return m[s] || 'badge--gray'
}
function transportTypeLabel(s: string) {
if (s === 'kdys') return '快递运输'
if (s === 'ryxd') return '人员携带'
return s || '—'
}
function sampleTypeName(id: string) {
return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
}
@@ -1020,35 +950,6 @@
gap: 6px;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
&.pending {
background: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
&.active {
background: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
&.expired {
background: #94a3b8;
box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.2);
}
&.void {
background: #ef4444;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
&.research {
background: #8b5cf6;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
}
/* ---- Section Card ---- */
.section-card {
border-radius: 12px;
@@ -1144,41 +1045,6 @@
}
}
/* ---- Badge ---- */
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.6;
&--green {
background: #ecfdf5;
color: #059669;
}
&--amber {
background: #fffbeb;
color: #d97706;
}
&--gray {
background: #f1f5f9;
color: #64748b;
}
&--red {
background: #fef2f2;
color: #dc2626;
}
&--purple {
background: #faf5ff;
color: #7c3aed;
}
&--blue {
background: #eff6ff;
color: #2563eb;
}
}
/* ---- Attachment / Remark Row ---- */
.attachment-row {
margin-top: 12px;
@@ -96,9 +96,11 @@
align="center"
header-align="center"
>
<template #default="{ row }">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</template>
<template #default="{ row }">
<el-tag :type="donationStatusTagType(String(row.donationStatus || ''))" size="small">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="donationDate"
@@ -269,33 +271,15 @@
fetchDeleteResearchDonation,
type ResearchDonationRow
} from '@/api/research-donation'
import {
donationStatusLabel,
donationStatusTagType,
newbornSexLabel,
deliveryModeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'ResearchDonationList' })
/* ====== 标签映射 ====== */
function donationStatusLabel(s: string) {
const m: Record<string, string> = {
dsx: '待生效',
sxz: '生效中',
ygq: '已过期',
yzf: '已作废',
zckc: '转细胞存储'
}
return m[s] || s || '-'
}
function newbornSexLabel(s: string) {
if (s === '1') return '男'
if (s === '0') return '女'
return s || '-'
}
function deliveryModeLabel(s: string) {
if (s === 'sc') return '顺产'
if (s === 'pgc') return '剖宫产'
return s || '-'
}
/* ====== 列表 ====== */
const searchForm = reactive<{ keyword: string; donationStatus: string }>({
keyword: '',
@@ -479,7 +479,7 @@
{ label: '分类名称', value: row.name },
{ label: '上级分类', value: row.parentId ? parentCategoryList.value.find((c: any) => c.id === row.parentId)?.name || row.parentId : '无' },
{ label: '价格系数', value: formatPriceCoefficient(row.priceCoefficient) },
{ label: '状态', value: row.isActive ? '启用' : '禁用' },
{ label: '状态', value: row.isActive ? '启用' : '禁用', tagType: row.isActive ? 'success' : 'info' },
{ label: '图片', value: row.imageUrl || '无' },
{ label: '创建时间', value: row.createAt }
]
+1 -1
View File
@@ -399,7 +399,7 @@
fields: [
{ label: '组织名称', value: viewRow.value?.deptName },
{ label: '组织类别', value: getDeptCategoryLabel(viewRow.value?.deptCategoryCode || '') },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用' },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用', tagType: viewRow.value?.status === '1' ? 'success' : 'info' },
{ label: '负责人', value: viewRow.value?.leader },
{ label: '联系电话', value: viewRow.value?.leaderPhone },
{ label: '备注', value: viewRow.value?.remark, span: true }
+3 -15
View File
@@ -177,6 +177,7 @@
<script setup lang="ts">
import { Close, Check } from '@element-plus/icons-vue'
import { leaveStatusLabel, leaveStatusTagType } from '@/utils/status-tag-registry'
import {
fetchLeaveList,
fetchCreateLeave,
@@ -208,21 +209,8 @@ const formData = reactive({
remark: ''
})
const statusMap: Record<string, { label: string; type: string }> = {
draft: { label: '草稿', type: 'info' },
pending: { label: '审批中', type: 'warning' },
approved: { label: '已通过', type: 'success' },
rejected: { label: '已驳回', type: 'danger' },
cancelled: { label: '已取消', type: 'info' }
}
function statusLabel(s: string) {
return statusMap[s]?.label || s
}
function statusTagType(s: string) {
return statusMap[s]?.type || 'info'
}
const statusLabel = leaveStatusLabel
const statusTagType = leaveStatusTagType
function resetForm() {
formData.leaveType = 'personal'
+3 -3
View File
@@ -343,13 +343,13 @@
title: '基本信息',
fields: [
{ label: '菜单名称', value: viewRow.value?.menuName },
{ label: '菜单类型', value: menuTypeTagMap[viewRow.value?.menuType || '']?.label || viewRow.value?.menuType },
{ label: '菜单类型', value: menuTypeTagMap[viewRow.value?.menuType || '']?.label || viewRow.value?.menuType, tagType: 'primary' },
{ label: '路由路径', value: viewRow.value?.path },
{ label: '组件路径', value: viewRow.value?.component },
{ label: '权限标识', value: viewRow.value?.perms },
{ label: '是否外链', value: viewRow.value?.isFrame === 1 ? '否' : '是' },
{ label: '显示状态', value: viewRow.value?.visible === '1' ? '显示' : '隐藏' },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用' },
{ label: '显示状态', value: viewRow.value?.visible === '1' ? '显示' : '隐藏', tagType: viewRow.value?.visible === '1' ? 'success' : 'info' },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用', tagType: viewRow.value?.status === '1' ? 'success' : 'info' },
{ label: '备注', value: viewRow.value?.remark, span: true }
]
}
+2 -2
View File
@@ -72,7 +72,7 @@
<el-table-column prop="postCode" label="职位编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="status" label="状态" min-width="80" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
@@ -330,7 +330,7 @@
fields: [
{ label: '职位名称', value: row.postName },
{ label: '职位编码', value: row.postCode },
{ label: '状态', value: row.status === '1' ? '正常' : '停用' },
{ label: '状态', value: row.status === '1' ? '正常' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
+4 -13
View File
@@ -94,7 +94,7 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
@@ -373,6 +373,7 @@
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path'
import { dataScopeLabel as dataScopeLabelFn, dataScopeTagType } from '@/utils/status-tag-registry'
import {
fetchRoleList,
fetchCreateRole,
@@ -389,17 +390,7 @@
defineOptions({ name: 'SystemRole' })
const DATA_SCOPE_MAP: Record<string, string> = {
'1': '全部数据',
'2': '本部门数据',
'3': '本部门及以下',
'4': '仅本人',
'5': '自定义'
}
function dataScopeLabel(val: string) {
return DATA_SCOPE_MAP[val] || '未知'
}
const dataScopeLabel = dataScopeLabelFn
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.Role.RoleItem[]>([])
@@ -813,7 +804,7 @@
fields: [
{ label: '角色名称', value: row.roleName },
{ label: '数据权限', value: dataScopeLabel(row.dataScope) },
{ label: '状态', value: row.status === '1' ? '正常' : '停用' },
{ label: '状态', value: row.status === '1' ? '正常' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
+3 -3
View File
@@ -77,7 +77,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" min-width="80" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
@@ -255,8 +255,8 @@
fields: [
{ label: '站点名称', value: row.siteName },
{ label: '站点编码', value: row.siteCode },
{ label: '站点类型', value: getSiteTypeLabel(row.siteType) },
{ label: '状态', value: row.status === '1' ? '正常' : '停用' },
{ label: '站点类型', value: getSiteTypeLabel(row.siteType), tagType: 'primary' },
{ label: '状态', value: row.status === '1' ? '正常' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{ label: '地址', value: row.address, span: true }
]
},
+2 -2
View File
@@ -100,7 +100,7 @@
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
@@ -450,7 +450,7 @@
label: '性别',
value: ({ '0': '男', '1': '女', '2': '未知' } as Record<string, string>)[row.sex] || '未知'
},
{ label: '状态', value: row.status === '1' ? '正常' : '停用' },
{ label: '状态', value: row.status === '1' ? '正常' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]