提交
This commit is contained in:
1 parent
93cd506a90
commit
3b875ec370
93 files changed
+11514
-2304
No files matched your search
@@ -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
@@ -1 +1 @@
|
||||
11478
|
||||
7085
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("请至少选择一种检测方式")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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="快递单号")
|
||||
|
||||
@@ -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='细胞存储协议与科研捐赠转移关联';
|
||||
@@ -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';
|
||||
@@ -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`;
|
||||
@@ -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`;
|
||||
@@ -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='质检任务检测结果';
|
||||
@@ -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`;
|
||||
@@ -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` = '');
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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_%';
|
||||
@@ -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);
|
||||
@@ -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
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">`
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
|
||||
const panelTitleBarClass = 'title-bar-heading'
|
||||
const panelTitleBarTextRules: RegExp[] = [
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表|样本接收列表|接收检查项列表|公司列表|项目列表|工单列表|任务列表|类型列表|规格列表|价格列表|体系列表|渠道列表|机构列表|耗材列表|试剂列表|SOP列表|工艺列表|耗材包列表)/
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表|样本接收列表|接收检查项列表|公司列表|项目列表|工单列表|任务列表|类型列表|规格列表|价格列表|体系列表|渠道列表|机构列表|耗材列表|试剂列表|SOP列表|工艺列表|路线列表|耗材包列表)/
|
||||
]
|
||||
|
||||
const refreshPanelTitleIcons = () => {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
@@ -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 }
|
||||
]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user