This commit is contained in:
zlwu committed 2026-05-22 16:25:04 +08:00
1 parent 53d3b21f57
commit 1f5de9030e
54 files changed
+10954 -4322

No files matched your search

+1 -1
View File
@@ -1 +1 @@
98717
64042
+14
View File
@@ -1368,6 +1368,20 @@ async def update_production_task(
return error_response(400, str(e))
@router.delete("/production-tasks/{task_id}")
async def delete_production_task(
task_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
production_task_svc.delete_production_task(db, task_id, tenant_no, user.get("user_id"))
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/production-tasks/{task_id}/status")
async def update_production_task_status(
task_id: str,
+350 -29
View File
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import (
BizCellProduct,
BizCellSampleType,
BizProcess,
BizProcessConsumable,
@@ -33,7 +34,10 @@ from app.models.models import (
BizProductionWorkOrder,
BizProtocolSample,
BizSampleReception,
BizStorageSlotOccupancy,
)
from app.crud import storage_occupancy as occupancy_svc
from app.crud.cell_product import _row_to_dict as _cp_row_to_dict, _enrich_product_dicts as _cp_enrich
def _fmt_dt(t: Optional[datetime]) -> str:
@@ -148,8 +152,8 @@ def _step_data_collection_to_dict(d: BizProductionTaskStepDataCollection) -> Dic
}
def _task_input_to_dict(i: BizProductionTaskInput) -> Dict[str, Any]:
return {
def _task_input_to_dict(i: BizProductionTaskInput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": i.id,
"taskId": i.task_id,
"inputType": i.input_type or "",
@@ -158,10 +162,20 @@ def _task_input_to_dict(i: BizProductionTaskInput) -> Dict[str, Any]:
"inputName": i.input_name or "",
"remark": i.remark or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages", "storageSlotId",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _task_output_to_dict(o: BizProductionTaskOutput) -> Dict[str, Any]:
return {
def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": o.id,
"taskId": o.task_id,
"cellProductId": o.cell_product_id or "",
@@ -173,6 +187,16 @@ def _task_output_to_dict(o: BizProductionTaskOutput) -> Dict[str, Any]:
"remark": o.remark or "",
"attachments": o.attachments or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _soft_delete_sub(db: Session, model_class, parent_field: str, parent_id: str, now: datetime) -> None:
@@ -494,15 +518,46 @@ def get_production_task_detail(db: Session, task_id: str, tenant_no: str) -> Opt
inputs = db.query(BizProductionTaskInput).filter(
and_(BizProductionTaskInput.task_id == task_id, BizProductionTaskInput.del_flag == 0)
).order_by(BizProductionTaskInput.create_time.asc()).all()
).order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc()).all()
outputs = db.query(BizProductionTaskOutput).filter(
and_(BizProductionTaskOutput.task_id == task_id, BizProductionTaskOutput.del_flag == 0)
).order_by(BizProductionTaskOutput.create_time.asc()).all()
).order_by(BizProductionTaskOutput.order_num.asc(), BizProductionTaskOutput.create_time.asc()).all()
result["steps"] = step_list
result["inputs"] = [_task_input_to_dict(i) for i in inputs]
result["outputs"] = [_task_output_to_dict(o) for o in outputs]
# --- enrich inputs with cell product info ---
input_dicts = [_task_input_to_dict(i) for i in inputs]
cp_input_ids = [i.input_id for i in inputs if i.input_type == "cell_product" and i.input_id]
input_product_map: Dict[str, Dict[str, Any]] = {}
if cp_input_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_input_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
input_product_map = {d["id"]: d for d in cp_dicts}
for idx, inp in enumerate(inputs):
if inp.input_type == "cell_product" and inp.input_id and inp.input_id in input_product_map:
input_dicts[idx] = _task_input_to_dict(inp, product_dict=input_product_map[inp.input_id])
result["inputs"] = input_dicts
# --- enrich outputs with cell product info ---
output_dicts = [_task_output_to_dict(o) for o in outputs]
cp_output_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
output_product_map: Dict[str, Dict[str, Any]] = {}
if cp_output_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_output_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
output_product_map = {d["id"]: d for d in cp_dicts}
for idx, out in enumerate(outputs):
if out.cell_product_id and out.cell_product_id in output_product_map:
output_dicts[idx] = _task_output_to_dict(out, product_dict=output_product_map[out.cell_product_id])
result["outputs"] = output_dicts
return result
@@ -852,42 +907,265 @@ def set_task_inputs(
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
old_rows = db.query(BizProductionTaskInput).filter(
and_(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.del_flag == 0,
)
).all()
for old_row in old_rows:
# 如果是细胞产品,恢复状态
if old_row.input_type == "cell_product" and old_row.input_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status == "in_production":
product.product_status = "frozen"
product.update_by = user_id
product.update_time = now
db.add(product)
# --- 2. 全量软删除旧行 ---
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
for it in items:
# --- 3. 按传入顺序批量新建 ---
for idx, it in enumerate(items):
inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
# 如果是细胞产品,设置状态为 in_production
if inp_type == "cell_product" and inp_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status in ("frozen", "thawed"):
product.product_status = "in_production"
product.update_by = user_id
product.update_time = now
db.add(product)
db.add(BizProductionTaskInput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
input_type=(it.get("input_type") or "").strip() or None,
input_id=(it.get("input_id") or "").strip() or None,
input_code=(it.get("input_code") or "").strip() or None,
input_name=(it.get("input_name") or "").strip() or None,
input_type=inp_type or None,
input_id=inp_id or None,
input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.flush()
db.commit()
def _parse_optional_date(val):
"""解析可选日期字符串"""
if not val:
return None
from dateutil.parser import parse as _parse_dt
try:
return _parse_dt(str(val))
except Exception:
return None
def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
"""为细胞产品创建库位占用记录"""
if not product.storage_slot_id:
return
now = datetime.now()
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=product.storage_slot_id,
item_type="cell_product",
item_id=product.id,
item_code=product.product_code or "",
item_name=product.product_name or "",
item_source="production",
stored_at=now,
occupancy_status="stored",
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(occ)
db.flush()
occupancy_svc._refresh_slot_cache(db, product.storage_slot_id)
def _update_product_from_output(product: BizCellProduct, it: Dict[str, Any], user_id, now):
"""从 output item 更新细胞产品字段"""
field_map = [
("product_name", "product_name", "productName"),
("product_type_id", "product_type_id", "productTypeId"),
("spec_id", "spec_id", "specId"),
("batch_no", "batch_no", "batchNo"),
("storage_slot_id", "storage_slot_id", "storageSlotId"),
("unit", "unit", "unit"),
("product_images", "product_images", "productImages"),
("remark", "remark", "remark"),
]
for db_field, snake_key, camel_key in field_map:
val = it.get(snake_key) if it.get(snake_key) is not None else it.get(camel_key)
if val is not None:
setattr(product, db_field, (str(val) or "").strip() or None)
for num_field, snake_key, camel_key in [
("expected_cell_count", "expected_cell_count", "expectedCellCount"),
("viability", "viability", "viability"),
]:
val = it.get(snake_key)
if val is None:
val = it.get(camel_key)
if val is not None:
setattr(product, num_field, val)
for date_field, snake_key, camel_key in [
("freeze_date", "freeze_date", "freezeDate"),
("expiry_date", "expiry_date", "expiryDate"),
]:
val = it.get(snake_key) or it.get(camel_key)
if val is not None:
setattr(product, date_field, _parse_optional_date(val))
passage = it.get("passage")
if passage is not None:
product.passage = (str(passage) or "").strip() or None
quantity = it.get("quantity")
if quantity is not None:
product.quantity = quantity
product.update_by = user_id
product.update_time = now
def set_task_outputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
_soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
# --- 1. 加载旧 outputs ---
old_rows = db.query(BizProductionTaskOutput).filter(
and_(
BizProductionTaskOutput.task_id == task_id,
BizProductionTaskOutput.del_flag == 0,
)
).all()
old_cp_map: Dict[str, BizProductionTaskOutput] = {}
for o in old_rows:
if o.cell_product_id:
old_cp_map[o.cell_product_id] = o
# --- 2. 构建新的 cp_id 集合 ---
new_cp_ids: set = set()
for it in items:
quantity = it.get("quantity")
db.add(BizProductionTaskOutput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
cell_product_id=(it.get("cell_product_id") or "").strip() or None,
storage_slot_id=(it.get("storage_slot_id") or "").strip() or None,
output_name=(it.get("output_name") or "").strip() or None,
output_barcode=(it.get("output_barcode") or "").strip() or None,
quantity=quantity if quantity is not None else None,
unit=(it.get("unit") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
attachments=(it.get("attachments") 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,
))
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id:
new_cp_ids.add(cp_id)
# --- 3. 处理删除(旧有新无) ---
for cp_id, old_row in old_cp_map.items():
if cp_id not in new_cp_ids:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
product.product_status = "voided"
product.update_by = user_id
product.update_time = now
db.add(product)
if product.storage_slot_id:
occupancy_svc.remove_occupancy_by_item(db, "cell_product", cp_id, user_id=user_id)
old_row.del_flag = 1
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
# --- 4. 处理新增和修改 ---
for idx, it in enumerate(items):
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id and cp_id in old_cp_map:
# === 修改 ===
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
old_slot_id = product.storage_slot_id
_update_product_from_output(product, it, user_id, now)
new_slot_id = product.storage_slot_id
db.add(product)
if old_slot_id != new_slot_id:
if old_slot_id:
occupancy_svc.remove_occupancy_by_item(db, "cell_product", cp_id, user_id=user_id)
if new_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
old_row = old_cp_map[cp_id]
old_row.output_name = product.product_name if product else ""
old_row.output_barcode = product.product_code if product else ""
old_row.storage_slot_id = product.storage_slot_id if product else None
old_row.quantity = it.get("quantity")
old_row.unit = (it.get("unit") or "").strip() or None
old_row.remark = (it.get("remark") or "").strip() or None
old_row.order_num = idx
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
else:
# === 新增 ===
product = BizCellProduct(
id=generate_uuid(),
product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
product_name=(it.get("product_name") or it.get("productName") or "").strip(),
product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
passage=(it.get("passage") or "").strip() or None,
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else it.get("expectedCellCount"),
viability=it.get("viability"),
freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
product_status=it.get("product_status") or it.get("productStatus") or "frozen",
source_type="production",
production_task_id=task_id,
quantity=it.get("quantity"),
unit=(it.get("unit") or "").strip() or None,
expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
tenant_no=tenant_no,
order_num=idx,
remark=(it.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,
)
db.add(product)
db.flush()
if product.storage_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
output_row = BizProductionTaskOutput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
cell_product_id=product.id,
storage_slot_id=product.storage_slot_id,
output_name=product.product_name or "",
output_barcode=product.product_code or "",
quantity=product.quantity,
unit=product.unit,
remark=product.remark,
attachments=(it.get("attachments") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(output_row)
db.flush()
db.commit()
@@ -1009,6 +1287,49 @@ def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
return _step_to_dict(new_step)
def delete_production_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
"""软删除生产任务及其所有子表数据(步骤 + 投入 + 产出)"""
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
# 1. 软删除所有步骤及其子表
steps = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
).all()
for step in steps:
step.del_flag = 1
step.update_by = user_id
step.update_time = now
db.add(step)
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step.id, now)
# 2. 软删除投入
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
# 3. 软删除产出
_soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
# 4. 软删除任务本体
task.del_flag = 1
task.update_by = user_id
task.update_time = now
db.add(task)
db.commit()
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_backend_app_api_production_py", "label": "production.py", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L1"}, {"id": "api_production_get_tenant_no", "label": "_get_tenant_no()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L54"}, {"id": "api_production_consumablecreatebody", "label": "ConsumableCreateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L67"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "api_production_consumableupdatebody", "label": "ConsumableUpdateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L79"}, {"id": "api_production_list_consumables", "label": "list_consumables()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L92"}, {"id": "api_production_list_all_consumables", "label": "list_all_consumables()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L106"}, {"id": "api_production_create_consumable", "label": "create_consumable()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L116"}, {"id": "api_production_update_consumable", "label": "update_consumable()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L142"}, {"id": "api_production_delete_consumable", "label": "delete_consumable()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L169"}, {"id": "api_production_reagentcreatebody", "label": "ReagentCreateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L186"}, {"id": "api_production_reagentupdatebody", "label": "ReagentUpdateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L198"}, {"id": "api_production_list_reagents", "label": "list_reagents()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L211"}, {"id": "api_production_list_all_reagents", "label": "list_all_reagents()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L225"}, {"id": "api_production_create_reagent", "label": "create_reagent()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L235"}, {"id": "api_production_update_reagent", "label": "update_reagent()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L261"}, {"id": "api_production_delete_reagent", "label": "delete_reagent()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L288"}, {"id": "api_production_sopdocumentcreatebody", "label": "SopDocumentCreateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L305"}, {"id": "api_production_sopdocumentupdatebody", "label": "SopDocumentUpdateBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L315"}, {"id": "api_production_list_sop_documents", "label": "list_sop_documents()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L326"}, {"id": "api_production_list_all_sop_documents", "label": "list_all_sop_documents()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L340"}, {"id": "api_production_create_sop_document", "label": "create_sop_document()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L350"}, {"id": "api_production_update_sop_document", "label": "update_sop_document()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L377"}, {"id": "api_production_delete_sop_document", "label": "delete_sop_document()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py", "source_location": "L406"}, {"id": "api_production_processcategorycreatebody", "label": "ProcessCategoryCreateBody", "file_type": "code", "source_file": "/Users/wuziloLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_backend_app_crud_production_task_py", "label": "production_task.py", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L1"}, {"id": "crud_production_task_fmt_dt", "label": "_fmt_dt()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L43"}, {"id": "crud_production_task_generate_task_no", "label": "_generate_task_no()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L47"}, {"id": "crud_production_task_task_to_dict", "label": "_task_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L61"}, {"id": "crud_production_task_step_to_dict", "label": "_step_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L83"}, {"id": "crud_production_task_step_reagent_to_dict", "label": "_step_reagent_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L103"}, {"id": "crud_production_task_step_consumable_to_dict", "label": "_step_consumable_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L118"}, {"id": "crud_production_task_step_output_to_dict", "label": "_step_output_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L133"}, {"id": "crud_production_task_step_data_collection_to_dict", "label": "_step_data_collection_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L144"}, {"id": "crud_production_task_task_input_to_dict", "label": "_task_input_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L155"}, {"id": "crud_production_task_task_output_to_dict", "label": "_task_output_to_dict()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L177"}, {"id": "crud_production_task_soft_delete_sub", "label": "_soft_delete_sub()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L202"}, {"id": "crud_production_task_clone_step_reagents", "label": "_clone_step_reagents()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L215"}, {"id": "crud_production_task_clone_step_consumables", "label": "_clone_step_consumables()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L233"}, {"id": "crud_production_task_clone_step_outputs", "label": "_clone_step_outputs()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L251"}, {"id": "crud_production_task_clone_step_data_collections", "label": "_clone_step_data_collections()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L268"}, {"id": "crud_production_task_clone_route_to_task", "label": "_clone_route_to_task()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L284"}, {"id": "crud_production_task_create_production_task", "label": "create_production_task()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L317"}, {"id": "crud_production_task_list_production_tasks", "label": "list_production_tasks()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L354"}, {"id": "crud_production_task_get_production_task_detail", "label": "get_production_task_detail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L474"}, {"id": "crud_production_task_update_production_task", "label": "update_production_task()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L564"}, {"id": "crud_production_task_update_task_status", "label": "update_task_status()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py", "source_location": "L589"}, {"id": "crud_production_task_add_task_step", "label": "add_task_step()", "file_type": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "label": "production_task.py", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L1"}, {"id": "schemas_production_task_productiontaskcreate", "label": "ProductionTaskCreate", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L6"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "schemas_production_task_productiontasklistquery", "label": "ProductionTaskListQuery", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L15"}, {"id": "schemas_production_task_productiontaskupdate", "label": "ProductionTaskUpdate", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L23"}, {"id": "schemas_production_task_productiontaskstatusupdate", "label": "ProductionTaskStatusUpdate", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L30"}, {"id": "schemas_production_task_taskstepadd", "label": "TaskStepAdd", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L35"}, {"id": "schemas_production_task_taskstepupdate", "label": "TaskStepUpdate", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L46"}, {"id": "schemas_production_task_taskstepmaterialitem", "label": "TaskStepMaterialItem", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L56"}, {"id": "schemas_production_task_taskstepoutputitem", "label": "TaskStepOutputItem", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L69"}, {"id": "schemas_production_task_taskstepdatacollectionitem", "label": "TaskStepDataCollectionItem", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L76"}, {"id": "schemas_production_task_taskinputitem", "label": "TaskInputItem", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L83"}, {"id": "schemas_production_task_taskoutputitem", "label": "TaskOutputItem", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L92"}, {"id": "schemas_production_task_taskstepreorderbody", "label": "TaskStepReorderBody", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L120"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "target": "pydantic", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "target": "typing", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "target": "datetime", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "target": "schemas_production_task_productiontaskcreate", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L6", "weight": 1.0}, {"source": "schemas_production_task_productiontaskcreate", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L6", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_backend_app_schemas_production_task_py", "target": "schemas_production_task_productiontasklistquery", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py", "source_location": "L15", "weight": 1.0}, {"source": "schemas_production_task_productiontasklistquery", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_filLine truncated
+18 -2
View File
@@ -91,12 +91,28 @@ class TaskInputItem(BaseModel):
class TaskOutputItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
# 关联字段
cell_product_id: Optional[str] = Field(None, alias="cellProductId")
# 细胞产品完整字段
product_code: Optional[str] = Field(None, alias="productCode")
product_name: Optional[str] = Field(None, alias="productName")
product_type_id: Optional[str] = Field(None, alias="productTypeId")
spec_id: Optional[str] = Field(None, alias="specId")
passage: Optional[str] = None
batch_no: Optional[str] = Field(None, alias="batchNo")
storage_slot_id: Optional[str] = Field(None, alias="storageSlotId")
output_name: Optional[str] = Field(None, alias="outputName")
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
expected_cell_count: Optional[float] = Field(None, alias="expectedCellCount")
viability: Optional[float] = None
freeze_date: Optional[str] = Field(None, alias="freezeDate")
quantity: Optional[float] = None
unit: Optional[str] = None
expiry_date: Optional[str] = Field(None, alias="expiryDate")
product_images: Optional[str] = Field(None, alias="productImages")
product_status: Optional[str] = Field(None, alias="productStatus")
source_type: Optional[str] = Field(None, alias="sourceType")
# 兼容旧字段
output_name: Optional[str] = Field(None, alias="outputName")
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
remark: Optional[str] = None
attachments: Optional[str] = None
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+101 -121
View File
@@ -1,12 +1,12 @@
# Graph Report - app (2026-05-22)
## Corpus Check
- 137 files · ~121,901 words
- 137 files · ~123,015 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 2434 nodes · 9565 edges · 54 communities detected
- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5568 edges (avg confidence: 0.63)
- 2441 nodes · 9642 edges · 50 communities detected
- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5633 edges (avg confidence: 0.63)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -54,20 +54,16 @@
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 46|Community 46]]
- [[_COMMUNITY_Community 47|Community 47]]
- [[_COMMUNITY_Community 48|Community 48]]
- [[_COMMUNITY_Community 49|Community 49]]
- [[_COMMUNITY_Community 50|Community 50]]
- [[_COMMUNITY_Community 51|Community 51]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 53|Community 53]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
## God Nodes (most connected - your core abstractions)
1. `success_response()` - 384 edges
2. `error_response()` - 307 edges
1. `success_response()` - 385 edges
2. `error_response()` - 308 edges
3. `SysUser` - 218 edges
4. `BizQcMaterialUsage` - 199 edges
5. `BizMaterialStockLog` - 195 edges
@@ -75,7 +71,7 @@
7. `BizQcMaterial` - 195 edges
8. `BizMaterial` - 192 edges
9. `BizQcItemParam` - 190 edges
10. `generate_uuid()` - 134 edges
10. `generate_uuid()` - 135 edges
## Surprising Connections (you probably didn't know these)
- `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog` [INFERRED]
@@ -93,260 +89,244 @@
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (401): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+393 more)
Nodes (396): attachment_download_url(), AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, dict_data_options(), DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody (+388 more)
### Community 1 - "Community 1"
Cohesion: 0.02
Nodes (407): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+399 more)
Nodes (373): create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post(), create_role_management() (+365 more)
### Community 2 - "Community 2"
Cohesion: 0.05
Nodes (266): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+258 more)
Nodes (277): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+269 more)
### Community 3 - "Community 3"
Cohesion: 0.02
Nodes (213): list_collection_regions(), get_designer_data(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), generate_uuid(), delete() (+205 more)
Nodes (158): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+150 more)
### Community 4 - "Community 4"
Cohesion: 0.04
Nodes (112): _format_attachment_file_size(), get_protocol_nos_used_in_protocols(), urls_to_attachment_list(), create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), _row_to_dict() (+104 more)
Cohesion: 0.03
Nodes (127): generate_uuid(), create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists() (+119 more)
### Community 5 - "Community 5"
Cohesion: 0.05
Nodes (85): _persist_protocol_sub_resources(), _add_months_yyyymmdd(), align_fq_payment_details_payload(), _apply_submit_receipt_finance(), _apply_submit_refund_finance(), attachments_to_urls(), _audit_to_dict(), batch_submit_receipt_finance() (+77 more)
Cohesion: 0.03
Nodes (80): get_user_by_id(), _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+72 more)
### Community 6 - "Community 6"
Cohesion: 0.05
Nodes (43): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, BaseStorage, convert_heic_to_jpeg() (+35 more)
Nodes (91): get_role_menus(), _format_attachment_file_size(), get_protocol_nos_used_in_protocols(), urls_to_attachment_list(), _accept_conclusion_label(), _attach_reception_detail(), complete_production_work_order(), create_production_work_order_from_reception() (+83 more)
### Community 7 - "Community 7"
Cohesion: 0.04
Nodes (51): BaseHTTPMiddleware, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+43 more)
Nodes (80): activate_channel_agreement(), cell_product_price_code_exists(), cell_product_spec_code_exists(), cell_product_type_code_exists(), _channel_agreement_price_exists(), _channel_agreement_price_item_key(), checkup_package_code_exists(), create_cell_product_price() (+72 more)
### Community 8 - "Community 8"
Cohesion: 0.05
Nodes (59): list_roles_for_select(), _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+51 more)
Cohesion: 0.07
Nodes (70): _persist_protocol_sub_resources(), _add_months_yyyymmdd(), align_fq_payment_details_payload(), _apply_submit_receipt_finance(), _apply_submit_refund_finance(), attachments_to_urls(), _audit_to_dict(), batch_submit_receipt_finance() (+62 more)
### Community 9 - "Community 9"
Cohesion: 0.07
Nodes (66): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+58 more)
### Community 10 - "Community 10"
Cohesion: 0.05
Nodes (26): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+18 more)
### Community 11 - "Community 11"
Cohesion: 0.09
Nodes (57): batch_delete_research_donations(), change_donation_status(), convert_from_storage(), convert_to_storage(), create_research_donation(), delete_research_donation(), _get_current_user_id(), get_research_donation_detail() (+49 more)
### Community 12 - "Community 12"
Cohesion: 0.32
Nodes (52): ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody, OutputTypeCreateBody, OutputTypeUpdateBody, PackItemInput, ProcessCategoryCreateBody (+44 more)
### Community 11 - "Community 11"
Cohesion: 0.14
Nodes (36): _audit_to_dict(), batch_delete_donations(), _collection_to_dict(), convert_to_storage(), _create_collection(), create_donation(), _create_party(), _create_samples() (+28 more)
### Community 13 - "Community 13"
Cohesion: 0.05
Nodes (40): BaseHTTPMiddleware, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+32 more)
### Community 12 - "Community 12"
### Community 14 - "Community 14"
Cohesion: 0.16
Nodes (32): _create_insurance_company(), _create_insurance_company_invoice(), _create_insurance_project_price(), delete_insurance_company(), delete_insurance_company_invoice(), delete_insurance_project_price(), _fmt_dt(), _generate_uuid() (+24 more)
### Community 13 - "Community 13"
### Community 15 - "Community 15"
Cohesion: 0.12
Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total, 申请退款 Args: out_trade_no: 商户订单号 total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
### Community 14 - "Community 14"
### Community 16 - "Community 16"
Cohesion: 0.11
Nodes (28): get_next_cell_product_code(), _build_facility_path_label(), build_split_product_codes(), create_cell_product(), create_cell_products_batch(), create_cell_products_by_quantity(), delete_cell_product(), _enrich_product_dicts() (+20 more)
### Community 15 - "Community 15"
Cohesion: 0.16
Nodes (26): add_step(), copy_step(), create_route(), _create_step_consumable(), _create_step_data_collection(), _create_step_output(), _create_step_reagent(), get_route() (+18 more)
### Community 16 - "Community 16"
Cohesion: 0.17
Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
### Community 17 - "Community 17"
Cohesion: 0.21
Nodes (19): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+11 more)
### Community 18 - "Community 18"
Cohesion: 0.18
Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
### Community 19 - "Community 19"
Cohesion: 0.2
Nodes (14): deduct_for_task(), _display_remark(), list_usages(), list_usages_by_task(), _parse_receiver_purpose_from_remark(), 查询质检任务关联的物料使用记录(task_complete)。, 撤销任务此前 task_complete 扣减,便于重新填报物料使用。, 兼容旧数据:领用人/用途曾拼接在 remark 中。 (+6 more)
### Community 20 - "Community 20"
Cohesion: 0.25
Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
### Community 21 - "Community 21"
Cohesion: 0.25
Nodes (13): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), 采集协议号池 CRUD(biz_collection_protocol_no) (+5 more)
### Community 22 - "Community 22"
Cohesion: 0.26
Nodes (12): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), 样本编码池 CRUD(biz_sample_code), _resolve_issued_user_display_name() (+4 more)
### Community 23 - "Community 23"
### Community 18 - "Community 18"
Cohesion: 0.24
Nodes (9): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import Wech, 发送模板消息 Args: openid: 用户openid templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+1 more)
### Community 24 - "Community 24"
### Community 19 - "Community 19"
Cohesion: 0.23
Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
### Community 25 - "Community 25"
### Community 20 - "Community 20"
Cohesion: 0.27
Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
### Community 26 - "Community 26"
### Community 21 - "Community 21"
Cohesion: 0.29
Nodes (9): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu() (+1 more)
### Community 27 - "Community 27"
### Community 22 - "Community 22"
Cohesion: 0.31
Nodes (8): consumable_code_exists(), create_consumable(), get_consumable(), list_consumables(), 耗材 CRUD(biz_consumable), _row_to_dict(), soft_delete_consumable(), update_consumable()
### Community 28 - "Community 28"
### Community 23 - "Community 23"
Cohesion: 0.31
Nodes (8): create_sop_document(), get_sop_document(), list_sop_documents(), SOP文档 CRUD(biz_sop_document), _row_to_dict(), soft_delete_sop_document(), sop_code_exists(), update_sop_document()
### Community 24 - "Community 24"
Cohesion: 0.31
Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
### Community 25 - "Community 25"
Cohesion: 0.38
Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
### Community 29 - "Community 29"
### Community 26 - "Community 26"
Cohesion: 0.27
Nodes (8): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data(), update_dict_data()
### Community 30 - "Community 30"
### Community 27 - "Community 27"
Cohesion: 0.31
Nodes (8): category_code_exists(), create_process_category(), get_process_category(), list_process_categories(), 工艺工序分类 CRUD(biz_process_category), _row_to_dict(), soft_delete_process_category(), update_process_category()
### Community 31 - "Community 31"
### Community 28 - "Community 28"
Cohesion: 0.31
Nodes (8): create_item_unit(), get_item_unit(), list_item_units(), 物品单位 CRUD(biz_item_unit), _row_to_dict(), soft_delete_item_unit(), unit_code_exists(), update_item_unit()
### Community 32 - "Community 32"
Cohesion: 0.31
Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
### Community 33 - "Community 33"
### Community 29 - "Community 29"
Cohesion: 0.31
Nodes (8): create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type(), type_code_exists(), update_cell_sample_type()
### Community 34 - "Community 34"
### Community 30 - "Community 30"
Cohesion: 0.31
Nodes (8): create_output_type(), get_output_type(), list_output_types(), 工艺产出物类型 CRUD(biz_output_type), _row_to_dict(), soft_delete_output_type(), type_code_exists(), update_output_type()
### Community 35 - "Community 35"
### Community 31 - "Community 31"
Cohesion: 0.36
Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
### Community 36 - "Community 36"
### Community 32 - "Community 32"
Cohesion: 0.36
Nodes (7): create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), _row_to_dict(), soft_delete_dict_type(), update_dict_type()
### Community 33 - "Community 33"
Cohesion: 0.36
Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
### Community 37 - "Community 37"
### Community 34 - "Community 34"
Cohesion: 0.36
Nodes (7): create_reagent(), get_reagent(), list_reagents(), reagent_code_exists(), _row_to_dict(), soft_delete_reagent(), update_reagent()
### Community 38 - "Community 38"
### Community 35 - "Community 35"
Cohesion: 0.39
Nodes (8): create_lab_item(), get_lab_item(), item_code_exists(), list_active_lab_items(), list_lab_items(), _row_to_dict(), soft_delete_lab_item(), update_lab_item()
### Community 36 - "Community 36"
Cohesion: 0.36
Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
### Community 39 - "Community 39"
### Community 37 - "Community 37"
Cohesion: 0.29
Nodes (2): BaseModel, SoftDeleteModel
### Community 40 - "Community 40"
### Community 38 - "Community 38"
Cohesion: 0.38
Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
### Community 41 - "Community 41"
Cohesion: 0.6
Nodes (5): _dec(), _fmt_dt(), list_stock_logs(), _row_to_dict(), _user_nick_name_map()
### Community 42 - "Community 42"
Cohesion: 0.4
Nodes (4): list_ethnics_for_select(), list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict()
### Community 43 - "Community 43"
### Community 39 - "Community 39"
Cohesion: 0.5
Nodes (3): BaseSettings, Config, Settings
### Community 44 - "Community 44"
### Community 40 - "Community 40"
Cohesion: 0.5
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
### Community 45 - "Community 45"
### Community 41 - "Community 41"
Cohesion: 0.67
Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)
### Community 46 - "Community 46"
### Community 42 - "Community 42"
Cohesion: 1.0
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
### Community 47 - "Community 47"
### Community 43 - "Community 43"
Cohesion: 1.0
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
### Community 50 - "Community 50"
### Community 46 - "Community 46"
Cohesion: 1.0
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
### Community 51 - "Community 51"
### Community 47 - "Community 47"
Cohesion: 1.0
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
### Community 52 - "Community 52"
### Community 48 - "Community 48"
Cohesion: 1.0
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
### Community 53 - "Community 53"
### Community 49 - "Community 49"
Cohesion: 1.0
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
### Community 54 - "Community 54"
### Community 50 - "Community 50"
Cohesion: 1.0
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
### Community 55 - "Community 55"
### Community 51 - "Community 51"
Cohesion: 1.0
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
## Knowledge Gaps
- **90 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+85 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **Thin community `Community 39`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
- **Thin community `Community 37`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 44`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
- **Thin community `Community 40`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 45`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
- **Thin community `Community 41`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 46`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
- **Thin community `Community 42`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 47`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
- **Thin community `Community 43`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 50`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
- **Thin community `Community 46`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 51`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
- **Thin community `Community 47`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 52`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
- **Thin community `Community 48`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 53`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
- **Thin community `Community 49`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 54`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
- **Thin community `Community 50`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 55`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
- **Thin community `Community 51`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `generate_uuid()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`?**
_High betweenness centrality (0.118) - this node is a cross-community bridge._
- **Why does `SysUser` connect `Community 0` to `Community 2`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 19`, `Community 21`, `Community 22`?**
_High betweenness centrality (0.080) - this node is a cross-community bridge._
- **Why does `success_response()` connect `Community 1` to `Community 0`, `Community 3`, `Community 8`, `Community 42`, `Community 12`, `Community 45`, `Community 14`?**
_High betweenness centrality (0.066) - this node is a cross-community bridge._
- **Are the 383 inferred relationships involving `success_response()` (e.g. with `web_login_rsa_public_key()` and `web_login()`) actually correct?**
_`success_response()` has 383 INFERRED edges - model-reasoned connections that need verification._
- **Are the 353 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
_`str` has 353 INFERRED edges - model-reasoned connections that need verification._
- **Are the 306 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `web_login()`) actually correct?**
_`error_response()` has 306 INFERRED edges - model-reasoned connections that need verification._
- **Why does `generate_uuid()` connect `Community 4` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 13`, `Community 16`, `Community 17`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`?**
_High betweenness centrality (0.111) - this node is a cross-community bridge._
- **Why does `SysUser` connect `Community 0` to `Community 2`, `Community 3`, `Community 5`, `Community 9`, `Community 11`, `Community 12`, `Community 14`?**
_High betweenness centrality (0.091) - this node is a cross-community bridge._
- **Why does `success_response()` connect `Community 1` to `Community 0`, `Community 3`, `Community 5`, `Community 6`, `Community 41`, `Community 11`, `Community 14`, `Community 16`?**
_High betweenness centrality (0.055) - this node is a cross-community bridge._
- **Are the 384 inferred relationships involving `success_response()` (e.g. with `web_login_rsa_public_key()` and `web_login()`) actually correct?**
_`success_response()` has 384 INFERRED edges - model-reasoned connections that need verification._
- **Are the 356 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
_`str` has 356 INFERRED edges - model-reasoned connections that need verification._
- **Are the 307 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `web_login()`) actually correct?**
_`error_response()` has 307 INFERRED edges - model-reasoned connections that need verification._
- **Are the 216 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
_`SysUser` has 216 INFERRED edges - model-reasoned connections that need verification._
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">2434 nodes &middot; 9565 edges &middot; 60 communities</div>
<div id="stats">2441 nodes &middot; 9642 edges &middot; 56 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "init_py", "label": "__init__.py", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 48, "community_name": "Community 48", "source_file": "__init__.py", "file_type": "code", "degree": 0}, {"id": "main_py", "label": "main.py", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.py", "community": 8, "community_name": "Community 8", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_lifespan", "label": "lifespan()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "lifespan()", "community": 8, "community_name": "Community 8", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_mask_secret", "label": "_mask_secret()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "_mask_secret()", "community": 8, "community_name": "Community 8", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_root", "label": "root()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "root()", "community": 8, "community_name": "Community 8", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_health_check", "label": "health_check()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "health_check()", "community": 8, "community_name": "Community 8", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "tasks_init_py", "label": "__init__.py", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 8, "community_name": "Community 8", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "tasks_init_init_scheduled_tasks", "label": "init_scheduled_tasks()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "init_scheduled_tasks()", "community": 8, "community_name": "Community 8", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "tasks_init_shutdown_scheduler", "label": "shutdown_scheduler()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "shutdown_scheduler()", "community": 8, "community_name": "Community 8", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "core_auth_py", "label": "auth.py", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 11.4, "font": {"size": 0, "color": "#ffffff"}, "title": "auth.py", "community": 8, "community_name": "Community 8", "source_file": "core/auth.py", "file_type": "code", "degree": 18}, {"id": "core_auth_redis_session_enabled", "label": "_redis_session_enabled()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "_redis_session_enabled()", "community": 8, "community_name": "Community 8", "source_file": "core/auth.py", "file_type": "code", "degree": 2}, {"id": "core_auth_get_redis_client", "label": "_get_redis_client()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "_get_redis_client()", "community": 8, "community_name": "Community 8", "source_file": "core/auth.py", "file_type": "code", "degree": 9}, {"id": "core_auth_session_key", "label": "_session_key()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "_session_key()", "community": 8, "community_name": "Community 8", "source_file": "core/auth.py", "file_type": "code", "degree": 5}, {"id": "core_auth_refresh_key", "label": "_refresh_key()", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "boLine truncated
const RAW_EDGES = [{"from": "main_py", "to": "app_main_lifespan", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_mask_secret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_root", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_health_check", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_lifespan", "to": "core_login_crypto_ensure_initialized", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_init_scheduled_tasks", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_shutdown_scheduler", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tasks_init_py", "to": "tasks_init_init_scheduled_tasks", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tasks_init_py", "to": "tasks_init_shutdown_scheduler", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_redis_session_enabled", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_redis_client", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_session_key", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_refresh_key", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_store_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_revoke_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_extend_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_is_session_valid", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_store_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_is_refresh_token_valid", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_revoke_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_create_access_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_create_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_decode_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_current_user_by_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_current_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "Line truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 427}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 419}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 271}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 244}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 124}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 88}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 86}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 74}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 72}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 70}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 59}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 37}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 33}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 31}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 31}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 28}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 21}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 21}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 20}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 15}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 15}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 15}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 14}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 13}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 13}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 12}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 11}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 10}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 10}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 10}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 10}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 10}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 10}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 10}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 10}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 9}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 9}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 9}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 9}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 8}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 7}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 6}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 6}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 4}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 4}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 2}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 2}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 1}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 1}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 1}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 1}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 1}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 1}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 1}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 1}];
const RAW_NODES = [{"id": "init_py", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 44, "community_name": "Community 44", "source_file": "__init__.py", "file_type": "code", "degree": 0}, {"id": "main_py", "label": "main.py", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.py", "community": 5, "community_name": "Community 5", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_lifespan", "label": "lifespan()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "lifespan()", "community": 5, "community_name": "Community 5", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_mask_secret", "label": "_mask_secret()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "_mask_secret()", "community": 5, "community_name": "Community 5", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_root", "label": "root()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "root()", "community": 5, "community_name": "Community 5", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_health_check", "label": "health_check()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "health_check()", "community": 5, "community_name": "Community 5", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "tasks_init_py", "label": "__init__.py", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 5, "community_name": "Community 5", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "tasks_init_init_scheduled_tasks", "label": "init_scheduled_tasks()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "init_scheduled_tasks()", "community": 5, "community_name": "Community 5", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "tasks_init_shutdown_scheduler", "label": "shutdown_scheduler()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "shutdown_scheduler()", "community": 5, "community_name": "Community 5", "source_file": "tasks/__init__.py", "file_type": "code", "degree": 2}, {"id": "core_auth_py", "label": "auth.py", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 11.4, "font": {"size": 0, "color": "#ffffff"}, "title": "auth.py", "community": 5, "community_name": "Community 5", "source_file": "core/auth.py", "file_type": "code", "degree": 18}, {"id": "core_auth_redis_session_enabled", "label": "_redis_session_enabled()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "_redis_session_enabled()", "community": 5, "community_name": "Community 5", "source_file": "core/auth.py", "file_type": "code", "degree": 2}, {"id": "core_auth_get_redis_client", "label": "_get_redis_client()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "_get_redis_client()", "community": 5, "community_name": "Community 5", "source_file": "core/auth.py", "file_type": "code", "degree": 9}, {"id": "core_auth_session_key", "label": "_session_key()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "_session_key()", "community": 5, "community_name": "Community 5", "source_file": "core/auth.py", "file_type": "code", "degree": 5}, {"id": "core_auth_refresh_key", "label": "_refresh_key()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "boLine truncated
const RAW_EDGES = [{"from": "main_py", "to": "app_main_lifespan", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_mask_secret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_root", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_health_check", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_lifespan", "to": "core_login_crypto_ensure_initialized", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_init_scheduled_tasks", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_shutdown_scheduler", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tasks_init_py", "to": "tasks_init_init_scheduled_tasks", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tasks_init_py", "to": "tasks_init_shutdown_scheduler", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_redis_session_enabled", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_redis_client", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_session_key", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_refresh_key", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_store_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_revoke_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_extend_session", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_is_session_valid", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_store_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_is_refresh_token_valid", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_revoke_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_create_access_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_create_refresh_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_decode_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_current_user_by_token", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_current_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "Line truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 423}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 383}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 282}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 191}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 138}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 101}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 100}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 88}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 72}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 70}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 69}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 60}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 59}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 55}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 33}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 31}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 31}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 20}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 13}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 13}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 12}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 11}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 10}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 10}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 10}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 10}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 10}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 10}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 10}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 10}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 10}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 9}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 9}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 9}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 9}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 9}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 9}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 8}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 7}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 4}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 4}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 3}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 2}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 2}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 1}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 1}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 1}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 1}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 1}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 1}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 1}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 1}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 1}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 1}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 0}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 0}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 0}];
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -41,7 +41,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter_value.py": 1778578258.0980055,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review_media.py": 1778578258.0986822,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter.py": 1778578258.0977957,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py": 1779418120.989224,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py": 1779430164.6962698,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_detail_media.py": 1778578258.0973988,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_banner.py": 1778578258.097102,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/region.py": 1778578258.099429,
@@ -76,7 +76,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/tenant.py": 1779265611.0532296,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable.py": 1779265707.1662717,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sop_document.py": 1779265721.9928973,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779415585.5702748,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779437598.5210466,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_item.py": 1779265661.482352,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent.py": 1779265718.524432,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1779329214.3583212,
@@ -130,7 +130,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/api/workflow.py": 1779266232.4568832,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1779304425.1104686,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1779281097.279193,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779363804.634908,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779435531.0507855,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_engine.py": 1778634664.6293883,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,348 @@
# 生产任务 I/O 与细胞产品库联动设计
> 日期:2026-05-22
> 状态:已确认,待实施
> 范围:生产任务执行弹窗 — 原材料/产出与细胞产品库 + 库位联动
---
## 1. 背景
### 当前问题
1. **产出脱节**:任务产出面板只有简单字段(名称、条码、数量、单位、备注),与细胞产品库完全脱节,产出的细胞产品不会自动入库。
2. **原材料信息不足**:选择细胞产品作为原材料时,只显示编码和名称,无法查看产品类型、代次、储物格等关键信息。
3. **库位不同步**:原材料消耗和产出创建不会联动库位状态(占用/释放)。
4. **先选后删除**:删除已选的原材料后恢复,当前无联动处理。
### 已有基础
- `BizProductionTaskOutput` 表已有 `cell_product_id`、`storage_slot_id` 字段(未使用)
- `BizCellProduct` 表已有 `production_task_id` 字段(未使用)
- `BizCellProduct` CRUD 已有完整的库位占用自动创建/释放逻辑
- `_refresh_slot_cache()` 已确保库位缓存一致性
---
## 2. 设计决策
| 维度 | 决策 |
|------|------|
| 产出联动模式 | **产出即入库** — 保存产出时自动在 `biz_cell_product` 创建记录 + 写入库位占用 |
| 删除恢复策略 | **删除即释放** — 删除产出/原材料后立即释放库位,重新添加需重新选位 |
| 产出表单范围 | **完整表单** — 与细胞产品库新增弹窗字段一致 |
| 原材料面板 | **同步增强** — 选择细胞产品时展示完整产品信息 |
| 产出交互方式 | **子弹窗 + 列表** — 添加/编辑弹出表单弹窗,列表展示核心字段 |
| 实现架构 | **后端一体化** — 一个事务内完成 diff + 产品 CRUD + 库位联动 |
---
## 3. 数据模型
### 3.1 无 DDL 变更
所有需要的字段已存在,只需激活:
- `biz_production_task_output.cell_product_id` — 开始写入实际产品 ID
- `biz_production_task_output.storage_slot_id` — 开始写入实际库位 ID
- `biz_cell_product.production_task_id` — 产出创建时写入任务 ID
### 3.2 数据流向
```
产出保存 (set_task_outputs)
├─ 新增 → create BizCellProduct (status=frozen, production_task_id=taskId)
│ + create BizStorageSlotOccupancy (item_source=production)
│ + _refresh_slot_cache()
│ + create BizProductionTaskOutput (关联 cell_product_id)
│
├─ 删除 → update BizCellProduct (status=voided)
│ + remove BizStorageSlotOccupancy
│ + _refresh_slot_cache()
│ + delete BizProductionTaskOutput
│
└─ 修改 → update BizCellProduct 字段
+ 如库位变更:迁移 occupancy + _refresh_slot_cache()
+ update BizProductionTaskOutput
原材料保存 (set_task_inputs)
├─ 新增 cell_product → update BizCellProduct (status=used)
│ + remove BizStorageSlotOccupancy
│ + _refresh_slot_cache()
│
└─ 删除 cell_product → update BizCellProduct (status=frozen)
+ create BizStorageSlotOccupancy (恢复原位)
+ _refresh_slot_cache()
```
---
## 4. 后端 API 改造
### 4.1 Schema 变更
**`backend/app/schemas/production_task.py`**
`TaskOutputItem` 扩展为包含完整细胞产品字段:
```python
class TaskOutputItem(BaseModel):
# 关联字段
cell_product_id: str | None = None # 编辑已有产出时传入
# 细胞产品完整字段
product_code: str
product_name: str
product_type_id: str | None = None
spec_id: str | None = None
passage: str | None = None # P1-P8
batch_no: str | None = None
storage_slot_id: str | None = None
expected_cell_count: float | None = None
viability: float | None = None # 0-100
freeze_date: str | None = None # YYYY-MM-DD
quantity: int = 1
unit: str | None = None
expiry_date: str | None = None # YYYY-MM-DD
product_images: str | None = None # 逗号分隔 URL
product_status: str = "frozen"
remark: str | None = None
```
`TaskInputItem` 不变更结构,但后端增加联动逻辑。
### 4.2 CRUD 改造
**`backend/app/crud/production_task.py`**
#### `set_task_outputs(db, task_id, items)`
伪代码:
```python
async def set_task_outputs(db: AsyncSession, task_id: str, items: list[TaskOutputItem]):
# 1. 加载旧的 outputs
old_outputs = await _get_task_outputs(db, task_id)
old_map = {o.cell_product_id: o for o in old_outputs if o.cell_product_id}
# 2. 构建 new map
new_map = {}
for item in items:
if item.cell_product_id:
new_map[item.cell_product_id] = item
else:
new_map[f"__new_{id(item)}"] = item
# 3. 处理删除
for cp_id, old in old_map.items():
if cp_id not in new_map:
# 作废细胞产品
await cell_product_svc.update_status(db, cp_id, "voided")
# 释放库位
await occupancy_svc.remove_by_item(db, "cell_product", cp_id)
# 删除关联
await db.delete(old)
# 4. 处理新增和修改
result_outputs = []
for key, item in new_map.items():
if key.startswith("__new_"):
# 新增:创建细胞产品 + 占位
product = await cell_product_svc.create(db, {
"product_code": item.product_code,
"product_name": item.product_name,
...全部字段,
"product_status": "frozen",
"production_task_id": task_id,
})
if item.storage_slot_id:
await occupancy_svc.create(db, {
"slot_id": item.storage_slot_id,
"item_type": "cell_product",
"item_id": product.id,
"item_source": "production",
})
# 创建关联
output = BizProductionTaskOutput(
task_id=task_id,
cell_product_id=product.id,
storage_slot_id=item.storage_slot_id,
output_name=item.product_name,
output_barcode=item.product_code,
quantity=item.quantity,
unit=item.unit,
remark=item.remark,
)
result_outputs.append(output)
else:
# 修改:更新细胞产品 + 处理库位变更
await cell_product_svc.update(db, key, {...更新字段})
old_output = old_map[key]
if old_output.storage_slot_id != item.storage_slot_id:
# 库位变更:释放旧位 + 占新位
if old_output.storage_slot_id:
await occupancy_svc.remove_by_item(db, "cell_product", key)
if item.storage_slot_id:
await occupancy_svc.create(db, {...新占位})
# 更新关联表
_update_output_fields(old_output, item)
result_outputs.append(old_output)
await db.flush()
return result_outputs
```
#### `set_task_inputs(db, task_id, items)`
伪代码:
```python
async def set_task_inputs(db: AsyncSession, task_id: str, items: list[TaskInputItem]):
old_inputs = await _get_task_inputs(db, task_id)
old_cp_ids = {i.input_id for i in old_inputs if i.input_type == "cell_product" and i.input_id}
new_cp_ids = {item.input_id for item in items if item.input_type == "cell_product" and item.input_id}
# 被移除的细胞产品 → 恢复 frozen + 恢复库位
for pid in old_cp_ids - new_cp_ids:
await cell_product_svc.update_status(db, pid, "frozen")
# 查找该产品原来的 slot,重新占位
product = await cell_product_svc.get(db, pid)
if product and product.storage_slot_id:
await occupancy_svc.create(db, {
"slot_id": product.storage_slot_id,
"item_type": "cell_product",
"item_id": pid,
"item_source": "production",
})
# 新增的细胞产品 → used + 释放库位
for pid in new_cp_ids - old_cp_ids:
await cell_product_svc.update_status(db, pid, "used")
await occupancy_svc.remove_by_item(db, "cell_product", pid)
# 替换 inputs
await _replace_inputs(db, task_id, items)
```
### 4.3 路由适配
**`backend/app/api/production.py`**
`set_task_outputs` 和 `set_task_inputs` 路由的请求体 schema 更新,其余不变。
---
## 5. 前端改造
### 5.1 新增组件
#### `TaskOutputProductDialog.vue`
- 位置:`web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue`
- Props:`visible`, `mode` (add/edit), `productData`, `dropdownOptions`
- 功能:
- 完整细胞产品表单(与 `cell-product/index.vue` 新增弹窗一致)
- 4列栅格布局
- 自动生成产品编码(调用 `fetchNextCellProductCode`)
- 下拉选项由父组件传入(产品类型、规格、储物格、单位)
- 产品图片卡片化上传
- Emit:`save(formData)` / `cancel`
#### `TaskOutputList.vue`
- 位置:`web-admin/src/views/production/production-work-order/TaskOutputList.vue`
- Props:`outputs`, `readonly`
- 展示列:序号、产品编码、产品名称、产品类型(tag)、代次、储物格位置、数量/单位、状态(tag)、操作
- Emit:`edit(row)` / `delete(index)` / `add()`
#### `TaskInputEnhancedTable.vue`
- 位置:`web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue`
- Props:`inputs`, `cellProductOptions`, `readonly`
- 功能:
- 类型切换(样本/细胞产品)
- 样本行:编码、名称、备注
- 细胞产品行:产品编码、产品名称、产品类型、代次、储物格位置、状态(tag)、备注
- 选择细胞产品时自动填充所有展示列
- Emit:`update(inputs)` / `add()` / `remove(index)`
### 5.2 现有组件修改
#### `useTaskStepEditor.ts`
**产出相关改造**:
- `addTaskOutputRow()` → 不再添加空行,改为设置 `outputDialogVisible = true, outputDialogMode = 'add'`
- `editTaskOutputRow(row)` → 设置 `outputDialogVisible = true, outputDialogMode = 'edit', editingOutputData = row`
- `removeTaskOutputRow(i)` → 确认后从列表移除(实际库位释放在 `saveTaskOutputs` 时后端处理)
- `saveTaskOutputs()` → 提交增强后的完整数据到后端
- 新增状态:`outputDialogVisible`, `outputDialogMode`, `editingOutputData`
- 新增方法:`handleOutputDialogSave(formData)` — 将表单数据合并到 `taskOutputs` 列表
**原材料相关改造**:
- `onCellProductInputSelect(row, val)` → 从 `cellProductMasterById` 填充完整信息(产品类型、代次、储物格等)
- 增加 `loadInputDropdownData()` — 加载细胞产品列表(含完整字段)
#### `TaskExecutionDialog.vue`
- 产出面板:替换为 `<TaskOutputList>` + `<TaskOutputProductDialog>`
- 原材料面板:替换为 `<TaskInputEnhancedTable>`
- 下拉数据加载:复用细胞产品库的 `loadDropdownData()` 逻辑
#### `api/production-task.ts`
- `fetchSetTaskOutputs` 参数适配新 schema(增加完整细胞产品字段)
---
## 6. 改造范围清单
### 后端
| 文件 | 操作 |
|------|------|
| `backend/app/schemas/production_task.py` | 修改 — `TaskOutputItem` 扩展完整细胞产品字段 |
| `backend/app/crud/production_task.py` | 修改 — `set_task_outputs` 增加 diff + 产品 CRUD + 库位联动 |
| `backend/app/crud/production_task.py` | 修改 — `set_task_inputs` 增加 diff + 产品状态变更 + 库位联动 |
| `backend/app/api/production.py` | 修改 — 路由适配新 schema |
| `backend/app/crud/cell_product.py` | 可能修改 — 提取 `create_single` / `update_fields` 供复用 |
### 前端
| 文件 | 操作 |
|------|------|
| `web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue` | **新建** |
| `web-admin/src/views/production/production-work-order/TaskOutputList.vue` | **新建** |
| `web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue` | **新建** |
| `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts` | 修改 — 产出/原材料逻辑重构 |
| `web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue` | 修改 — 替换产出/原材料面板 |
| `web-admin/src/api/production-task.ts` | 修改 — API 参数适配 |
### 不涉及
- 数据库 DDL(无新表、无新列)
- 工艺路线/工序步骤相关代码
- 细胞产品库独立页面
- 库位管理独立页面
---
## 7. 风险和约束
1. **事务大小**:如果一次产出数量很多(如50份),事务内会创建大量记录。建议限制单次产出 ≤ 20 条。
2. **库位冲突**:用户选择已被占用的库位时,后端应返回明确错误,前端应在下拉中标记已占位。
3. **并发安全**:同一库位同时被两个任务选中 → 后端需在 occupancy 表加唯一约束或乐观锁。
4. **原材料恢复**:删除原材料恢复细胞产品时,如果原库位已被其他产品占用 → 需要提示用户选择新库位或报错。
---
## 8. 验收标准
1. 产出面板能通过子弹窗新增完整的细胞产品(所有字段可填写)
2. 保存产出后,细胞产品库页面能看到新创建的产品
3. 产出产品在库位管理页面显示为已占用
4. 删除产出后,细胞产品状态变为已作废,库位释放
5. 原材料选择细胞产品后,展示完整信息(类型、代次、库位等)
6. 保存原材料后,被消耗的细胞产品状态变为已使用,库位释放
7. 删除原材料中的细胞产品后,产品状态恢复为已冻存,库位恢复占用
8. 查看详情(readonly模式)下所有信息正确展示
File diff suppressed because it is too large. Load diff
+293 -18
View File
@@ -6893,21 +6893,21 @@
"label": "fetchQcWorkOrderList()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L89"
"source_location": "L104"
},
{
"id": "api_qc_work_order_fetchqcworkorderdetail",
"label": "fetchQcWorkOrderDetail()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L101"
"source_location": "L116"
},
{
"id": "api_qc_work_order_fetchconfigureqcworkorder",
"label": "fetchConfigureQcWorkOrder()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L107"
"source_location": "L122"
},
{
"id": "views_auth_login_index_vue",
@@ -8300,21 +8300,21 @@
"label": "if()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1055"
"source_location": "L1068"
},
{
"id": "process_route_index_get",
"label": "get()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1256"
"source_location": "L1269"
},
{
"id": "process_route_index_set",
"label": "set()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1266"
"source_location": "L1279"
},
{
"id": "views_production_sop_index_vue",
@@ -8393,6 +8393,13 @@
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L1"
},
{
"id": "production_work_order_stepdetailpanel_for",
"label": "for()",
"file_type": "code",
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L362"
},
{
"id": "views_production_production_work_order_taskexecutiondialog_vue",
"label": "TaskExecutionDialog.vue",
@@ -8414,6 +8421,13 @@
"source_file": "views/production/production-work-order/DispatchTaskDialog.vue",
"source_location": "L203"
},
{
"id": "views_production_production_work_order_taskoutputproductdialog_vue",
"label": "TaskOutputProductDialog.vue",
"file_type": "code",
"source_file": "views/production/production-work-order/TaskOutputProductDialog.vue",
"source_location": "L1"
},
{
"id": "views_production_consumable_pack_index_vue",
"label": "index.vue",
@@ -8456,6 +8470,83 @@
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L1"
},
{
"id": "qc_work_order_index_handlereceptionattachmentpreview",
"label": "handleReceptionAttachmentPreview()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L278"
},
{
"id": "qc_work_order_index_handlereceptionattachmentdownload",
"label": "handleReceptionAttachmentDownload()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L283"
},
{
"id": "qc_work_order_index_taskattachmentfullurl",
"label": "taskAttachmentFullUrl()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L287"
},
{
"id": "qc_work_order_index_handletaskattachmentpreview",
"label": "handleTaskAttachmentPreview()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L294"
},
{
"id": "qc_work_order_index_handletaskattachmentdownload",
"label": "handleTaskAttachmentDownload()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L299"
},
{
"id": "qc_work_order_index_formatconfiguredlabitems",
"label": "formatConfiguredLabItems()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L315"
},
{
"id": "qc_work_order_index_formatconfiguredthirditems",
"label": "formatConfiguredThirdItems()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L321"
},
{
"id": "qc_work_order_index_buildtasktypesections",
"label": "buildTaskTypeSections()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L329"
},
{
"id": "qc_work_order_index_buildconfiguredonlysection",
"label": "buildConfiguredOnlySection()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L388"
},
{
"id": "qc_work_order_index_formatdetectmethods",
"label": "formatDetectMethods()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L406"
},
{
"id": "qc_work_order_index_formatmetric",
"label": "formatMetric()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L413"
},
{
"id": "views_quality_inspection_sample_reception_check_item_index_vue",
"label": "index.vue",
@@ -21269,7 +21360,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L89",
"source_location": "L104",
"weight": 1.0
},
{
@@ -21278,7 +21369,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L101",
"source_location": "L116",
"weight": 1.0
},
{
@@ -21287,7 +21378,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L107",
"source_location": "L122",
"weight": 1.0
},
{
@@ -23846,13 +23937,40 @@
"source_location": "L504",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "icons_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L729",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "views_cell_storage_protocol_protocolattachmentreadonly_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L730",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L731",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "vue_draggable_plus",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L719",
"source_location": "L732",
"weight": 1.0
},
{
@@ -23861,7 +23979,7 @@
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L720",
"source_location": "L733",
"weight": 1.0
},
{
@@ -23870,7 +23988,7 @@
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L743",
"source_location": "L756",
"weight": 1.0
},
{
@@ -23879,7 +23997,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1055",
"source_location": "L1068",
"weight": 1.0
},
{
@@ -23888,7 +24006,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1172",
"source_location": "L1185",
"weight": 1.0
},
{
@@ -23897,7 +24015,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1256",
"source_location": "L1269",
"weight": 1.0
},
{
@@ -23906,7 +24024,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1266",
"source_location": "L1279",
"weight": 1.0
},
{
@@ -23990,6 +24108,15 @@
"source_location": "L57",
"weight": 1.0
},
{
"source": "views_production_production_work_order_stepdetailpanel_vue",
"target": "production_work_order_stepdetailpanel_for",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L362",
"weight": 1.0
},
{
"source": "views_production_production_work_order_dispatchtaskdialog_vue",
"target": "production_work_order_dispatchtaskdialog_if",
@@ -23999,6 +24126,114 @@
"source_location": "L203",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handlereceptionattachmentpreview",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L278",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handlereceptionattachmentdownload",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L283",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_taskattachmentfullurl",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L287",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handletaskattachmentpreview",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L294",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handletaskattachmentdownload",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L299",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatconfiguredlabitems",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L315",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatconfiguredthirditems",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L321",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_buildtasktypesections",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L329",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_buildconfiguredonlysection",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L388",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatdetectmethods",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L406",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatmetric",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L413",
"weight": 1.0
},
{
"source": "qc_work_order_index_handletaskattachmentpreview",
"target": "qc_work_order_index_taskattachmentfullurl",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L295",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_material_index_vue",
"target": "element_plus",
@@ -29514,7 +29749,7 @@
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "api/qc-work-order.ts",
"source_location": "L95",
"source_location": "L110",
"weight": 1.0
},
{
@@ -29524,7 +29759,7 @@
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "api/qc-work-order.ts",
"source_location": "L102",
"source_location": "L117",
"weight": 1.0
},
{
@@ -30327,6 +30562,46 @@
"source_location": "L571",
"weight": 1.0
},
{
"source": "production_work_order_stepdetailpanel_for",
"target": "storage_facility_index_push",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L364",
"weight": 1.0
},
{
"source": "qc_work_order_index_handlereceptionattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L284",
"weight": 1.0
},
{
"source": "qc_work_order_index_handletaskattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L300",
"weight": 1.0
},
{
"source": "qc_work_order_index_formatdetectmethods",
"target": "storage_facility_index_push",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L408",
"weight": 1.0
},
{
"source": "qc_task_index_handlereceptionattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
+99 -87
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-05-22)
## Corpus Check
- Large corpus: 580 files · ~303,483 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 581 files · ~306,392 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 1416 nodes · 1764 edges · 42 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 386 edges (avg confidence: 0.8)
- 1429 nodes · 1782 edges · 44 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 390 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -34,10 +34,10 @@
- [[_COMMUNITY_Community 22|Community 22]]
- [[_COMMUNITY_Community 23|Community 23]]
- [[_COMMUNITY_Community 24|Community 24]]
- [[_COMMUNITY_Community 26|Community 26]]
- [[_COMMUNITY_Community 25|Community 25]]
- [[_COMMUNITY_Community 27|Community 27]]
- [[_COMMUNITY_Community 28|Community 28]]
- [[_COMMUNITY_Community 30|Community 30]]
- [[_COMMUNITY_Community 29|Community 29]]
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Community 32|Community 32]]
- [[_COMMUNITY_Community 33|Community 33]]
@@ -45,17 +45,19 @@
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 39|Community 39]]
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 47|Community 47]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 155 edges
2. `WebSocketClient` - 26 edges
3. `push()` - 18 edges
3. `push()` - 20 edges
4. `withFullscreenLoading()` - 16 edges
5. `MenuProcessor` - 16 edges
6. `VersionManager` - 15 edges
@@ -68,7 +70,7 @@
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/system/site/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
- `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()` [INFERRED]
utils/http/index.ts → api/auth.ts
- `fetchLeaveList()` --calls--> `get()` [INFERRED]
@@ -80,215 +82,225 @@
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (80): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+72 more)
Nodes (78): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+70 more)
### Community 1 - "Community 1"
Cohesion: 0.03
Nodes (57): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+49 more)
Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
### Community 2 - "Community 2"
Cohesion: 0.04
Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
### Community 3 - "Community 3"
Cohesion: 0.05
Nodes (32): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+24 more)
### Community 4 - "Community 4"
Cohesion: 0.04
Nodes (18): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllSopDocuments(), fetchConsumableList() (+10 more)
### Community 4 - "Community 4"
Cohesion: 0.05
Nodes (33): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+25 more)
### Community 5 - "Community 5"
Cohesion: 0.05
Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
### Community 6 - "Community 6"
Cohesion: 0.09
Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
Nodes (5): handleReset(), handleSearch(), WebSocketClient, TableCache, emit()
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (10): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), TableCache, themeAnimation() (+2 more)
Cohesion: 0.05
Nodes (18): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+10 more)
### Community 8 - "Community 8"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
### Community 9 - "Community 9"
Cohesion: 0.13
Nodes (4): handleReset(), handleSearch(), WebSocketClient, emit()
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 10 - "Community 10"
Cohesion: 0.07
Nodes (13): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+5 more)
Cohesion: 0.08
Nodes (10): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), themeAnimation() (+2 more)
### Community 11 - "Community 11"
Cohesion: 0.09
Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
Cohesion: 0.1
Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
### Community 12 - "Community 12"
Cohesion: 0.1
Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
### Community 13 - "Community 13"
Cohesion: 0.1
Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
### Community 14 - "Community 14"
Cohesion: 0.08
Nodes (14): fetchLeaveDetail(), fetchLeaveList(), fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances() (+6 more)
Cohesion: 0.1
Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
### Community 15 - "Community 15"
Cohesion: 0.1
Nodes (18): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+10 more)
### Community 16 - "Community 16"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
### Community 17 - "Community 17"
Cohesion: 0.13
Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
### Community 18 - "Community 18"
### Community 16 - "Community 16"
Cohesion: 0.14
Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
### Community 19 - "Community 19"
Cohesion: 0.16
### Community 17 - "Community 17"
Cohesion: 0.14
Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
### Community 18 - "Community 18"
Cohesion: 0.19
Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
### Community 19 - "Community 19"
Cohesion: 0.11
Nodes (8): fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchRefundConfirmedList(), fetchRefundConfirmList()
### Community 20 - "Community 20"
Cohesion: 0.12
Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
### Community 21 - "Community 21"
Cohesion: 0.16
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
### Community 21 - "Community 21"
### Community 22 - "Community 22"
Cohesion: 0.12
Nodes (2): fetchProductionTaskDetail(), fetchProductionTaskList()
### Community 22 - "Community 22"
### Community 23 - "Community 23"
Cohesion: 0.2
Nodes (2): ComponentLoader, RouteTransformer
### Community 23 - "Community 23"
### Community 24 - "Community 24"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 24 - "Community 24"
Cohesion: 0.17
Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
### Community 25 - "Community 25"
Cohesion: 0.18
Nodes (11): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleDelete(), handleReset(), handleSearch() (+3 more)
### Community 26 - "Community 26"
### Community 27 - "Community 27"
Cohesion: 0.17
Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
### Community 27 - "Community 27"
### Community 28 - "Community 28"
Cohesion: 0.18
Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
### Community 28 - "Community 28"
### Community 29 - "Community 29"
Cohesion: 0.2
Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
### Community 30 - "Community 30"
### Community 31 - "Community 31"
Cohesion: 0.39
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
### Community 31 - "Community 31"
### Community 32 - "Community 32"
Cohesion: 0.36
Nodes (1): TableHeightCalculator
### Community 32 - "Community 32"
### Community 33 - "Community 33"
Cohesion: 0.33
Nodes (2): logApiCryptoBootInfo(), maskSecret()
### Community 33 - "Community 33"
### Community 34 - "Community 34"
Cohesion: 0.29
Nodes (2): fetchLeaveDetail(), fetchLeaveList()
### Community 35 - "Community 35"
Cohesion: 0.47
Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
### Community 34 - "Community 34"
### Community 36 - "Community 36"
Cohesion: 0.33
Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
### Community 35 - "Community 35"
### Community 37 - "Community 37"
Cohesion: 0.33
Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
### Community 36 - "Community 36"
### Community 38 - "Community 38"
Cohesion: 0.33
Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
### Community 37 - "Community 37"
### Community 39 - "Community 39"
Cohesion: 0.33
Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
### Community 39 - "Community 39"
### Community 40 - "Community 40"
Cohesion: 0.33
Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
### Community 42 - "Community 42"
Cohesion: 0.53
Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
### Community 40 - "Community 40"
### Community 43 - "Community 43"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 41 - "Community 41"
### Community 44 - "Community 44"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 42 - "Community 42"
### Community 45 - "Community 45"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 44 - "Community 44"
### Community 47 - "Community 47"
Cohesion: 1.0
Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
### Community 52 - "Community 52"
Cohesion: 0.67
Nodes (2): fetchDeleteProjectSystemItem(), if()
## Knowledge Gaps
- **Thin community `Community 21`** (17 nodes): `fetchAddTaskStep()`, `fetchCopyTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReorderTaskSteps()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepOutputs()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
- **Thin community `Community 22`** (17 nodes): `fetchAddTaskStep()`, `fetchCopyTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReorderTaskSteps()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepOutputs()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 22`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
- **Thin community `Community 23`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 28`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
- **Thin community `Community 29`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
- **Thin community `Community 32`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 32`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
- **Thin community `Community 33`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 34`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
- **Thin community `Community 34`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 35`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
- **Thin community `Community 36`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 36`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
- **Thin community `Community 37`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 37`** (6 nodes): `fetchCompleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
- **Thin community `Community 38`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 40`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
- **Thin community `Community 39`** (6 nodes): `fetchCompleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
- **Thin community `Community 40`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 42`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
- **Thin community `Community 43`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 44`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
- **Thin community `Community 44`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 52`** (3 nodes): `fetchDeleteProjectSystemItem()`, `if()`, `index.vue`
- **Thin community `Community 45`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 47`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 21`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `Community 34`, `Community 35`, `Community 36`, `Community 37`?**
_High betweenness centrality (0.267) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 3` to `Community 0`, `Community 2`, `Community 7`, `Community 9`, `Community 11`, `Community 12`?**
_High betweenness centrality (0.075) - this node is a cross-community bridge._
- **Why does `buildTreePathLabelFromFlat()` connect `Community 3` to `Community 0`, `Community 7`?**
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 34`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 40`?**
_High betweenness centrality (0.266) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 4` to `Community 0`, `Community 2`, `Community 6`, `Community 7`, `Community 10`, `Community 13`, `Community 18`?**
_High betweenness centrality (0.091) - this node is a cross-community bridge._
- **Why does `fetchGetLatestUpgradeLog()` connect `Community 18` to `Community 0`?**
_High betweenness centrality (0.042) - this node is a cross-community bridge._
- **Are the 154 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
_`get()` has 154 INFERRED edges - model-reasoned connections that need verification._
- **Are the 17 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
_`push()` has 17 INFERRED edges - model-reasoned connections that need verification._
- **Are the 19 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
_`push()` has 19 INFERRED edges - model-reasoned connections that need verification._
- **Are the 15 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
_`withFullscreenLoading()` has 15 INFERRED edges - model-reasoned connections that need verification._
- **Should `Community 0` be split into smaller, more focused modules?**
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">1416 nodes &middot; 1764 edges &middot; 243 communities</div>
<div id="stats">1429 nodes &middot; 1782 edges &middot; 244 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 78, "community_name": "Community 78", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 79, "community_name": "Community 79", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 16, "community_name": "Community 16", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 16, "community_name": "Community 16", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 233}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 99}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 69}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 53}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 53}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 51}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 39}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 39}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 36}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 34}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 32}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 31}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 27}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 27}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 26}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 26}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 22}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 21}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 20}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 18}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 17}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 17}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 17}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 16}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 16}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 13}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 12}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 11}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 10}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 9}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 8}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 8}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 7}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 6}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 6}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 6}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 6}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 6}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 6}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 6}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 5}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 4}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 3}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 3}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 3}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 3}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 3}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 3}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 2}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 2}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2}, {"cLine truncated
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 80, "community_name": "Community 80", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 81, "community_name": "Community 81", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 15, "community_name": "Community 15", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 15, "community_name": "Community 15", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 223}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 87}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 69}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 53}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 53}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 51}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 48}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 44}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 39}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 36}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 30}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 28}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 27}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 27}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 27}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 22}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 20}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 20}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 19}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 19}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 19}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 17}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 17}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 17}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 16}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 15}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 13}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 12}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 11}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 10}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 9}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 8}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 8}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 7}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 7}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 6}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 6}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 6}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 6}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 6}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 6}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 6}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 6}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 5}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 4}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 3}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 3}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 3}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 3}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 3}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2}, {"Line truncated
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+19 -18
View File
@@ -17,8 +17,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779420283.3944924,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779420178.0237312,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779430756.3392074,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779425860.6226668,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -175,7 +175,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1778582866.2188683,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1779370638.1218963,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-payment-detail-readonly/index.vue": 1779006281.4220655,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779370582.0906003,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779431181.051139,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-dialog-bridge/index.vue": 1778582865.387537,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/index.ts": 1778582068.0037498,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/core/useFastEnter.ts": 1778582068.0014625,
@@ -215,14 +215,14 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material.ts": 1779283387.4196396,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception.ts": 1779255909.8553667,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1778582067.8610063,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779180968.908451,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779431182.2279022,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1778910234.3222458,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779269573.509064,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779329214.96505,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue": 1779161483.857836,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779370679.264584,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779424053.799563,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue": 1778938314.528914,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779371035.181769,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779423155.778682,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue": 1778978838.5026703,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1779313518.430975,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1779313530.5053055,
@@ -240,7 +240,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1779227504.1804457,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationAuditDialog.vue": 1779112125.8042517,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1779370624.9148946,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779371033.7577431,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779423240.754115,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue": 1779161484.445245,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/dashboard/console/index.vue": 1779227504.1947322,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/tenant/index.vue": 1779257191.2809517,
@@ -268,29 +268,30 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dict/index.vue": 1779259163.7560437,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/leave/index.vue": 1779257191.2768743,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/output-type/index.vue": 1779307917.5932443,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779341967.5417912,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779408337.9346838,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779423904.9098966,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779422386.4775605,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779309609.6630545,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779414898.4036016,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779307562.93293,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779361308.0826523,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779418710.3300052,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779408746.3297637,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779415778.1769066,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779414418.4825,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779426103.3002872,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779422456.542952,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779426096.2213817,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779422457.5458138,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue": 1779431206.6225035,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable-pack/index.vue": 1779307488.018346,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779307921.9798808,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779309524.0991938,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779423832.810978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779307716.2724373,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1778582068.070888,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779269560.7371643,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779431280.842699,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779269589.0865154,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779331328.5864854,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779423712.9489925,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779284232.1223023,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779333860.8573632,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779423489.553699,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779371036.8066387,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779285902.4727366,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779330250.9402645,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779423582.5553048,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/500/index.vue": 1778582068.0705533,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/403/index.vue": 1778582068.0697916,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/404/index.vue": 1778582068.0701847,
+4
View File
@@ -32,6 +32,10 @@ export function fetchUpdateProductionTaskStatus(id: string, data: Record<string,
return request.put({ url: `/web/admin/production-tasks/${id}/status`, params: data })
}
export function fetchDeleteProductionTask(id: string) {
return request.del({ url: `/web/admin/production-tasks/${id}` })
}
export function fetchAddTaskStep(taskId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/production-tasks/${taskId}/steps`, params: data })
}
+16 -1
View File
@@ -83,7 +83,22 @@ export type QcWorkOrderDetail = QcWorkOrder & {
itemName: string
feeAmount: number
}>
tasks?: Array<Record<string, unknown>>
tasks?: Array<{
id: string
taskNo: string
taskType: string
itemCode?: string
itemName?: string
companyId?: string
companyName?: string
feeAmount?: number
taskStatus: string
conclusion?: string
conclusionLabel?: string
remark?: string
completedTime?: string
attachmentFiles?: Array<{ id?: string; fileUrl?: string; fileName?: string }>
}>
}
export function fetchQcWorkOrderList(params?: {
@@ -120,7 +120,7 @@
/** 弹窗宽度,默认 720px */
width?: string
/** 字段栅格列数,默认 2 */
gridColumns?: 2 | 3
gridColumns?: 2 | 3 | 4
/** 挂到 body,用于嵌套在其它弹窗内时叠层展示 */
appendToBody?: boolean
/** 遮罩类名(可配合全局样式提高 z-index) */
@@ -240,6 +240,10 @@
&--cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
&--cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.view-kv {
@@ -301,15 +305,23 @@
margin-top: 12px;
}
@media (max-width: 1100px) {
.view-kv-grid--cols-4 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.view-kv-grid--cols-3 {
.view-kv-grid--cols-3,
.view-kv-grid--cols-4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.view-kv-grid,
.view-kv-grid--cols-3 {
.view-kv-grid--cols-3,
.view-kv-grid--cols-4 {
grid-template-columns: 1fr;
}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_stepdetailpanel_vue", "label": "StepDetailPanel.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L1"}, {"id": "production_work_order_stepdetailpanel_for", "label": "for()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L362"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_stepdetailpanel_vue", "target": "production_work_order_stepdetailpanel_for", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L362", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "push", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L364"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "decodeURIComponent", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "pop", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "split", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "label": "DispatchTaskDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L1"}, {"id": "production_work_order_dispatchtaskdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L203"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "target": "production_work_order_dispatchtaskdialog_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L203", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_dispatchtaskdialog_if", "callee": "initEditor", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L207"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_taskexecutiondialog_vue", "label": "TaskExecutionDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "label": "qc-work-order.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L1"}, {"id": "api_qc_work_order_fetchqcworkorderlist", "label": "fetchQcWorkOrderList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L104"}, {"id": "api_qc_work_order_fetchqcworkorderdetail", "label": "fetchQcWorkOrderDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L116"}, {"id": "api_qc_work_order_fetchconfigureqcworkorder", "label": "fetchConfigureQcWorkOrder()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L122"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchqcworkorderlist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L104", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchqcworkorderdetail", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L116", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchconfigureqcworkorder", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L122", "weight": 1.0}], "raw_calls": [{"caller_nid": "api_qc_work_order_fetchqcworkorderlist", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L110"}, {"caller_nid": "api_qc_work_order_fetchqcworkorderdetail", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L117"}, {"caller_nid": "api_qc_work_order_fetchconfigureqcworkorder", "callee": "post", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L131"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_system_view_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1"}, {"id": "process_route_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1068"}, {"id": "process_route_index_get", "label": "get()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1269"}, {"id": "process_route_index_set", "label": "set()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1279"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L729", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolattachmentreadonly_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L730", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L731", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "vue_draggable_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L732", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L733", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L756", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1068", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1185", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1269", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_set", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1279", "weight": 1.0}], "raw_calls": [{"caller_nid": "process_route_index_if", "callee": "some", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1069"}, {"caller_nid": "process_route_index_if", "callee": "mapReagentOptionLabel", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1071"}, {"caller_nid": "process_route_index_if", "callee": "some", "source_file": "/UsLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_third_party_company_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1"}, {"id": "qc_work_order_index_handlereceptionattachmentpreview", "label": "handleReceptionAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L278"}, {"id": "qc_work_order_index_handlereceptionattachmentdownload", "label": "handleReceptionAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L283"}, {"id": "qc_work_order_index_taskattachmentfullurl", "label": "taskAttachmentFullUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L287"}, {"id": "qc_work_order_index_handletaskattachmentpreview", "label": "handleTaskAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L294"}, {"id": "qc_work_order_index_handletaskattachmentdownload", "label": "handleTaskAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L299"}, {"id": "qc_work_order_index_formatconfiguredlabitems", "label": "formatConfiguredLabItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L315"}, {"id": "qc_work_order_index_formatconfiguredthirditems", "label": "formatConfiguredThirdItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L321"}, {"id": "qc_work_order_index_buildtasktypesections", "label": "buildTaskTypeSections()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L329"}, {"id": "qc_work_order_index_buildconfiguredonlysection", "label": "buildConfiguredOnlySection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L388"}, {"id": "qc_work_order_index_formatdetectmethods", "label": "formatDetectMethods()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L406"}, {"id": "qc_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L413"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L278", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L283", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_taskattachmentfullurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L287", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handletaskattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L294", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handletaskattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue",Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_lab_test_item_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_taskoutputproductdialog_vue", "label": "TaskOutputProductDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "label": "DonationFormDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1"}, {"id": "list_donationformdialog_createemptytransport", "label": "createEmptyTransport()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1019"}, {"id": "list_donationformdialog_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1039"}, {"id": "list_donationformdialog_loadethnicoptions", "label": "loadEthnicOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1046"}, {"id": "list_donationformdialog_ensureethnicoptioninlist", "label": "ensureEthnicOptionInList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1057"}, {"id": "list_donationformdialog_samplerowlabel", "label": "sampleRowLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1085"}, {"id": "list_donationformdialog_resetform", "label": "resetForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1169"}, {"id": "list_donationformdialog_scrolltosection", "label": "scrollToSection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1224"}, {"id": "list_donationformdialog_normalizesamplerow", "label": "normalizeSampleRow()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1232"}, {"id": "list_donationformdialog_openbatchtransportdialog", "label": "openBatchTransportDialog()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1255"}, {"id": "list_donationformdialog_confirmbatchtransportapply", "label": "confirmBatchTransportApply()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1263"}, {"id": "list_donationformdialog_addsample", "label": "addSample()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1277"}, {"id": "list_donationformdialog_removesample", "label": "removeSample()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1291"}, {"id": "list_donationformdialog_handleopenforadd", "label": "handleOpenForAdd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1295"}, {"id": "list_donationformdialog_applydonationbundletoform", "label": "applyDonationBundleToForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1343"}, {"id": "list_donationformdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1978"}, {"id": "list_donationformdialog_ensuredonationprotocolnoinoptions", "label": "ensureDonationProtocolNoInOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L2051"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L935", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "label": "useTaskStepEditor.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L1"}, {"id": "production_work_order_usetaskstepeditor_toapiflag", "label": "toApiFlag()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L28"}, {"id": "production_work_order_usetaskstepeditor_fromapiflag", "label": "fromApiFlag()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L32"}, {"id": "production_work_order_usetaskstepeditor_normalizestepsubtables", "label": "normalizeStepSubTables()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L35"}, {"id": "production_work_order_usetaskstepeditor_parseordernum", "label": "parseOrderNum()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L50"}, {"id": "production_work_order_usetaskstepeditor_usetaskstepeditor", "label": "useTaskStepEditor()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L57"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_task", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L16", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L24", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L25", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_toapiflag", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L28", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_fromapiflag", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L32", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_normalizestepsubtables", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L35", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_parseordernum", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L50", "weight": 1.0}, {"source": "users_wuzilong_woLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "label": "ProtocolFormDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolformdialog_createemptytransport", "label": "createEmptyTransport()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1287"}, {"id": "protocol_protocolformdialog_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1307"}, {"id": "protocol_protocolformdialog_loadethnicoptions", "label": "loadEthnicOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1314"}, {"id": "protocol_protocolformdialog_ensureethnicoptioninlist", "label": "ensureEthnicOptionInList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1325"}, {"id": "protocol_protocolformdialog_expressname", "label": "expressName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1353"}, {"id": "protocol_protocolformdialog_for", "label": "for()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L2021"}, {"id": "protocol_protocolformdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L2557"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1181", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1182", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_collection_protocol", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1194", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_business", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1200", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1201", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1202", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_ethnic", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1215", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_store_modules_user", "relation": "imports_from", "confidence": "EXTRACTED", "sLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L1"}, {"id": "sample_reception_index_parseetalocaldate", "label": "parseEtaLocalDate()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L683"}, {"id": "sample_reception_index_startoflocalday", "label": "startOfLocalDay()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L697"}, {"id": "sample_reception_index_isetatodayorearlier", "label": "isEtaTodayOrEarlier()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L701"}, {"id": "sample_reception_index_samplereceptionrowclassname", "label": "sampleReceptionRowClassName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L707"}, {"id": "sample_reception_index_pad", "label": "pad()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L832"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_parseetalocaldate", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L683", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_startoflocalday", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L697", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_isetatodayorearlier", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L701", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_samplereceptionrowclassname", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L707", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_pad", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L832", "weight": 1.0}, {"source": "sample_reception_index_isetatodayorearlier", "target": "sample_reception_index_parseetalocaldate", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L702", "weight": 1.0}, {"source": "sample_reception_index_isetatodayorearlier", "target": "sample_reception_index_startoflocalday", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L704", "weight": 1.0}, {"source": "sample_reception_index_samplereceptionrowclassname", "target": "sample_reception_index_isetatodayorearlier", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L709", "weight": 1.0}], "raw_calls": [{"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L684"}, {"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L684"}, {"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "slice", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/srLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "label": "ProtocolViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324"}, {"id": "protocol_protocolviewdialog_handleclosed", "label": "handleClosed()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1643"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_dash", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_handleclosed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1643", "weight": 1.0}], "raw_calls": [{"caller_nid": "protocol_protocolviewdialog_dash", "callee": "unitName", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324"}]}
@@ -1416,12 +1416,15 @@
activeSubTab.value = 'reagent'
processSearchKeyword.value = ''
configSuppressSave.value = true
stepList.value = []
stepListLoading.value = true
configDialogVisible.value = true
try {
await loadConfigReferenceData()
await loadRouteDetail()
} finally {
stepListLoading.value = false
await nextTick()
configSuppressSave.value = false
}
@@ -154,7 +154,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<div class="process-dialog-body min-h-[200px]">
<DialogFormBody :loading="dialogLoading" body-class="process-dialog-body min-h-[200px]">
<div class="detail-tabs">
<div
v-for="tab in formTabs"
@@ -482,7 +482,7 @@
</div>
</div>
</el-form>
</div>
</DialogFormBody>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
@@ -687,6 +687,7 @@
import { fetchItemUnitList } from '@/api/admin'
import { packTypeLabel, packTypeTagType } from '@/utils/status-tag-registry'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
defineOptions({ name: 'ProductionProcess' })
@@ -699,6 +700,7 @@
// 弹窗状态
const dialogVisible = ref(false)
const dialogLoading = ref(false)
const dialogTitle = ref('新增工艺')
const isEdit = ref(false)
const submitLoading = ref(false)
@@ -1018,13 +1020,15 @@
isEdit.value = false
dialogTitle.value = '新增工艺'
resetForm()
await loadReferenceData()
await searchItemUnitOptions('')
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
dialogLoading.value = true
nextTick(() => formRef.value?.clearValidate?.())
try {
await Promise.all([loadReferenceData(), searchItemUnitOptions('')])
} finally {
dialogLoading.value = false
nextTick(() => initDataCollectionSortable())
}
}
async function handleEdit(row: any) {
@@ -1032,9 +1036,14 @@
isEdit.value = true
dialogTitle.value = '编辑工艺'
editingId.value = row.id
await loadReferenceData()
dialogVisible.value = true
dialogLoading.value = true
nextTick(() => formRef.value?.clearValidate?.())
try {
const detail = await fetchProcessDetail(row.id)
const [, detail] = await Promise.all([
loadReferenceData(),
fetchProcessDetail(row.id)
])
formData.processCode = detail.processCode || ''
formData.processName = detail.processName || ''
formData.categoryId = detail.categoryId || ''
@@ -1067,12 +1076,10 @@
ensureDataCollectionUnitOptions()
} catch {
ElMessage.error('加载详情失败')
} finally {
dialogLoading.value = false
nextTick(() => initDataCollectionSortable())
}
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
}
async function handleSubmit() {
@@ -2,7 +2,9 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="phase === 'form' ? '派发生产任务' : `编辑生产任务 — ${createdTaskName || editTaskName || ''}`"
:title="
phase === 'form' ? '派发生产任务' : `编辑生产任务 — ${createdTaskName || editTaskName || ''}`
"
:width="phase === 'form' ? '560px' : '90vw'"
:class="phase === 'edit' ? 'dialog-form-layout dispatch-route-dialog' : 'dispatch-dialog'"
align-center
@@ -12,27 +14,60 @@
>
<!-- 阶段一:表单 -->
<div v-if="phase === 'form'" class="dispatch-dialog-work-order-info">
<span>工单号:<b>{{ workOrderRow?.workOrderNo }}</b></span>
<span
>工单号:<b>{{ workOrderRow?.workOrderNo }}</b></span
>
<span>协议号:{{ workOrderRow?.protocolNo || '—' }}</span>
<span>样本编码:{{ workOrderRow?.sampleCode || '—' }}</span>
</div>
<el-form v-if="phase === 'form'" ref="formRef" :model="form" :rules="rules" label-position="top" class="dialog-form-layout">
<el-form
v-if="phase === 'form'"
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="任务名称" prop="taskName" class="form-col-span-2">
<el-input v-model="form.taskName" placeholder="请输入任务名称" maxlength="200" />
</el-form-item>
<el-form-item label="工艺路线" class="form-col-span-2">
<template #label>工艺路线<span class="form-label-tip">可选,不选则从零创建工艺过程</span></template>
<el-select v-model="form.routeId" placeholder="请选择工艺路线(可选)" filterable clearable style="width: 100%">
<el-option v-for="route in routeOptions" :key="route.id"
:label="`${route.routeName}(${route.routeCode})`" :value="route.id" />
<template #label
>工艺路线<span class="form-label-tip">可选,不选则从零创建工艺过程</span></template
>
<el-select
v-model="form.routeId"
placeholder="请选择工艺路线(可选)"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="route in routeOptions"
:key="route.id"
:label="`${route.routeName}(${route.routeCode})`"
:value="route.id"
/>
</el-select>
</el-form-item>
<el-form-item label="描述" class="form-col-span-2">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="可选,填写任务描述" maxlength="500" />
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="可选,填写任务描述"
maxlength="500"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选,填写备注" maxlength="512" />
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="可选,填写备注"
maxlength="512"
/>
</el-form-item>
</div>
</el-form>
@@ -40,76 +75,121 @@
<template #footer>
<div v-if="phase === 'form'">
<el-button class="btn-ghost" @click="handleClose">取消</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask">生成任务</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask"
>生成任务</el-button
>
</div>
</template>
<!-- 阶段二:三栏编辑 -->
<div v-if="phase === 'edit'" class="route-config-body">
<div class="route-config-left">
<div class="route-config-left-header">
<h3>工艺库</h3>
<el-input v-model="editor.processSearchKeyword.value" placeholder="搜索工序" clearable size="small"
class="route-config-search" />
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item v-for="cat in editor.filteredProcessLibrary.value" :key="cat.categoryId"
:name="cat.categoryId">
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div v-for="(proc, procIndex) in cat.processes" :key="proc.id" class="route-config-process-card"
draggable="true" @dragstart="editor.onDragStart($event, proc)">
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty v-if="!editor.filteredProcessLibrary.value.length" description="暂无工艺数据" :image-size="48" />
<div class="route-config-left">
<div class="route-config-left-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
/>
</div>
<div class="route-config-center" v-loading="editor.stepListLoading.value" @dragover.prevent @drop="editor.onDropToCenter">
<div class="route-config-center-header">
<h3>工序步骤</h3>
<span class="route-config-step-count">共 {{ editor.stepList.value.length }} 步</span>
</div>
<div class="route-config-center-dropzone">
<draggable v-model="editor.stepList.value" handle=".route-config-step-handle" :animation="200"
@end="editor.onStepDragEnd">
<div v-for="(element, index) in editor.stepList.value" :key="element.id"
class="route-config-step-card" :class="{ 'is-active': editor.selectedStepId.value === element.id }"
@click="editor.selectStep(element)">
<div class="route-config-step-handle"><el-icon><Rank /></el-icon></div>
<div class="route-config-step-order">{{ index + 1 }}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-button link type="primary" size="small" title="复制步骤"
@click.stop="editor.handleCopyStep(element)">
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click.stop="editor.handleDeleteStep(element)">
<el-icon><Delete /></el-icon>
</el-button>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="route-config-center-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
</div>
<StepDetailPanel :editor="editor" />
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
<div
class="route-config-center"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<div class="route-config-center-header">
<h3>工序步骤</h3>
<span class="route-config-step-count">共 {{ editor.stepList.value.length }} 步</span>
</div>
<div class="route-config-center-dropzone">
<draggable
v-model="editor.stepList.value"
handle=".route-config-step-handle"
:animation="200"
@end="editor.onStepDragEnd"
>
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
class="route-config-step-card"
:class="{ 'is-active': editor.selectedStepId.value === element.id }"
@click="editor.selectStep(element)"
>
<div class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order">{{ index + 1 }}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="route-config-center-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
</div>
</div>
</div>
<StepDetailPanel :editor="editor" />
</div>
</el-dialog>
</template>
@@ -160,7 +240,9 @@
try {
const res = await fetchProcessRouteList({ page: 1, pageSize: 100 })
routeOptions.value = res.list || []
} catch { routeOptions.value = [] }
} catch {
routeOptions.value = []
}
}
function handleClose() {
@@ -193,25 +275,30 @@
await editor.initEditor(task.id)
ElMessage.success('任务已创建,可编辑步骤')
emit('success')
} catch { /* global error handling */ } finally {
} catch {
/* global error handling */
} finally {
submitting.value = false
}
}
watch(() => props.visible, async (val) => {
if (val) {
if (props.editTaskId) {
// 编辑已有任务:跳过阶段一,直接进入阶段二
createdTaskName.value = props.editTaskName || ''
phase.value = 'edit'
await editor.initEditor(props.editTaskId)
} else {
// 新建任务:走阶段一表单
phase.value = 'form'
loadRouteOptions()
watch(
() => props.visible,
async (val) => {
if (val) {
if (props.editTaskId) {
// 编辑已有任务:跳过阶段一,直接进入阶段二
createdTaskName.value = props.editTaskName || ''
phase.value = 'edit'
await editor.initEditor(props.editTaskId)
} else {
// 新建任务:走阶段一表单
phase.value = 'form'
loadRouteOptions()
}
}
}
})
)
</script>
<style scoped lang="scss">
@@ -0,0 +1,131 @@
<!-- 编辑生产任务基本信息弹窗 -->
<template>
<el-dialog
v-model="dialogVisible"
title="编辑任务基本信息"
width="560px"
destroy-on-close
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="任务名称" prop="taskName" class="form-col-span-2">
<el-input v-model="form.taskName" placeholder="请输入任务名称" maxlength="200" />
</el-form-item>
<el-form-item label="描述" class="form-col-span-2">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="可选,填写任务描述"
maxlength="500"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="可选,填写备注"
maxlength="512"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="handleClose">取消</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit"
>保存</el-button
>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
fetchProductionTaskDetail,
fetchUpdateProductionTask
} from '@/api/production-task'
import type { FormInstance, FormRules } from 'element-plus'
const props = defineProps<{
visible: boolean
taskId: string
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
(e: 'success'): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const formRef = ref<FormInstance>()
const submitting = ref(false)
const form = reactive({ taskName: '', description: '', remark: '' })
const rules: FormRules = {
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
}
async function loadTaskDetail() {
if (!props.taskId) return
try {
const res = await fetchProductionTaskDetail(props.taskId)
form.taskName = res.taskName || ''
form.description = res.description || ''
form.remark = res.remark || ''
} catch {
/* global error handling */
}
}
function handleClose() {
dialogVisible.value = false
form.taskName = ''
form.description = ''
form.remark = ''
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
submitting.value = true
try {
await fetchUpdateProductionTask(props.taskId, {
task_name: form.taskName,
description: form.description || undefined,
remark: form.remark || undefined
})
ElMessage.success('任务信息已更新')
emit('success')
handleClose()
} catch {
/* global error handling */
} finally {
submitting.value = false
}
}
watch(
() => props.visible,
async (val) => {
if (val && props.taskId) {
await loadTaskDetail()
}
}
)
</script>
@@ -6,13 +6,18 @@
<h3>步骤详情</h3>
<div v-if="!readonly" class="step-status-bar">
<template v-if="editor.mode.value === 'execution' && editor.selectedStep.value">
<el-tag :type="editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').type" size="small">
<el-tag
:type="editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').type"
size="small"
>
{{ editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').label }}
</el-tag>
<el-button
v-if="editor.selectedStep.value.stepStatus === 'dzx'"
class="btn-op-item primary"
size="small"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@click="editor.handleStartStep(editor.selectedStep.value)"
>
<el-icon><VideoPlay /></el-icon>执行此步骤
@@ -22,6 +27,8 @@
class="btn-op-item"
size="small"
type="success"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@click="editor.handleCompleteStep(editor.selectedStep.value)"
>
<el-icon><Check /></el-icon>完成步骤
@@ -32,6 +39,7 @@
type="primary"
size="small"
:loading="editor.manualSaveLoading.value"
:disabled="editor.stepActionLoading.value"
@click="editor.handleManualSave"
>
<el-icon><Check /></el-icon>保存
@@ -45,9 +53,17 @@
<el-input v-model="editor.selectedStep.value.processName" />
</el-form-item>
<el-form-item label="工序分类">
<el-select v-model="editor.selectedStep.value.categoryId" placeholder="选择分类" clearable>
<el-option v-for="cat in editor.categoryOptions.value" :key="cat.id"
:label="cat.categoryName || cat.name" :value="cat.id" />
<el-select
v-model="editor.selectedStep.value.categoryId"
placeholder="选择分类"
clearable
>
<el-option
v-for="cat in editor.categoryOptions.value"
:key="cat.id"
:label="cat.categoryName || cat.name"
:value="cat.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备名称">
@@ -57,14 +73,27 @@
<el-input v-model="editor.selectedStep.value.estimatedDuration" placeholder="如 2h" />
</el-form-item>
<el-form-item label="描述" class="step-form-col-half">
<el-input v-model="editor.selectedStep.value.description" type="textarea" :rows="2"
placeholder="工序描述" />
<el-input
v-model="editor.selectedStep.value.description"
type="textarea"
:rows="2"
placeholder="工序描述"
/>
</el-form-item>
<el-form-item label="关联SOP" class="step-form-col-half">
<el-select v-model="editor.sopIdsArr.value" multiple placeholder="选择SOP文档" filterable
@change="editor.onSopIdsChange">
<el-option v-for="sop in editor.sopOptions.value" :key="sop.id"
:label="sop.sopName || sop.name" :value="sop.id" />
<el-select
v-model="editor.sopIdsArr.value"
multiple
placeholder="选择SOP文档"
filterable
@change="editor.onSopIdsChange"
>
<el-option
v-for="sop in editor.sopOptions.value"
:key="sop.id"
:label="sop.sopName || sop.name"
:value="sop.id"
/>
</el-select>
</el-form-item>
<div
@@ -87,8 +116,19 @@
<el-icon><Plus /></el-icon>添加试剂
</el-button>
</div>
<el-table :data="editor.selectedStep.value.reagents" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.reagents"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="试剂编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" />
@@ -96,12 +136,25 @@
</el-table-column>
<el-table-column label="试剂名称" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.reagentId" placeholder="请选择试剂" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.reagentOptionsLoading.value"
<el-select
v-model="row.reagentId"
placeholder="请选择试剂"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.reagentOptionsLoading.value"
:remote-method="editor.searchReagentOptions"
@visible-change="(vis: boolean) => vis && editor.searchReagentOptions('')"
@change="(val: string) => editor.onReagentRowSelect(row, val)">
<el-option v-for="item in editor.reagentOptions.value" :key="item.id" :label="item.label" :value="item.id" />
@change="(val: string) => editor.onReagentRowSelect(row, val)"
>
<el-option
v-for="item in editor.reagentOptions.value"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
@@ -112,21 +165,48 @@
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="实际用量" min-width="110" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="实际用量"
min-width="110"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.actualUsage" placeholder="实际用量" class="inner-cell-input" @change="editor.saveReagents" />
<el-input
v-model="row.actualUsage"
placeholder="实际用量"
class="inner-cell-input"
@change="editor.saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
@@ -142,59 +222,133 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeReagentRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeReagentRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.reagents?.length" description="暂无试剂" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.reagents?.length"
description="暂无试剂"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="耗材" name="consumable">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addConsumableRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="editor.addConsumableRow"
>
<el-icon><Plus /></el-icon>添加耗材
</el-button>
</div>
<el-table :data="editor.selectedStep.value.consumables" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.consumables"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="耗材编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" />
<el-input
v-model="row.consumableCode"
placeholder="编码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="耗材名称" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.consumableId" placeholder="请选择耗材" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.consumableOptionsLoading.value"
<el-select
v-model="row.consumableId"
placeholder="请选择耗材"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.consumableOptionsLoading.value"
:remote-method="editor.searchConsumableOptions"
@visible-change="(vis: boolean) => vis && editor.searchConsumableOptions('')"
@change="(val: string) => editor.onConsumableRowSelect(row, val)">
<el-option v-for="item in editor.consumableOptions.value" :key="item.id" :label="item.label" :value="item.id" />
@change="(val: string) => editor.onConsumableRowSelect(row, val)"
>
<el-option
v-for="item in editor.consumableOptions.value"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.specModel" placeholder="规格型号" class="inner-cell-input" />
<el-input
v-model="row.specModel"
placeholder="规格型号"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="实际用量" min-width="110" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="实际用量"
min-width="110"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.actualUsage" placeholder="实际用量" class="inner-cell-input" @change="editor.saveConsumables" />
<el-input
v-model="row.actualUsage"
placeholder="实际用量"
class="inner-cell-input"
@change="editor.saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
@@ -210,11 +364,21 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeConsumableRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeConsumableRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.consumables?.length" description="暂无耗材" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.consumables?.length"
description="暂无耗材"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="产出物" name="output">
@@ -223,87 +387,182 @@
<el-icon><Plus /></el-icon>添加产出物
</el-button>
</div>
<el-table :data="editor.selectedStep.value.outputs" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.outputs"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="产出类型" min-width="150" align="center">
<template #default="{ row }">
<el-select v-model="row.outputTypeId" placeholder="选择类型" filterable class="inner-cell-select"
@change="editor.saveOutputs">
<el-option v-for="ot in editor.outputTypeOptions.value" :key="ot.id"
:label="ot.typeName || ot.name" :value="ot.id" />
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
@change="editor.saveOutputs"
>
<el-option
v-for="ot in editor.outputTypeOptions.value"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="名称" min-width="150" align="center">
<template #default="{ row }">
<el-input v-model="row.outputName" placeholder="名称" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="条码" min-width="150" align="center">
<template #default="{ row }">
<el-input v-model="row.outputBarcode" placeholder="条码" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeOutputRow($index)">删除</el-button>
<el-button link type="danger" size="small" @click="editor.removeOutputRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.outputs?.length" description="暂无产出物" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.outputs?.length"
description="暂无产出物"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="数据采集" name="dataCollection">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addDataCollectionRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="editor.addDataCollectionRow"
>
<el-icon><Plus /></el-icon>添加采集项
</el-button>
</div>
<el-table :data="editor.selectedStep.value.dataCollections" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.dataCollections"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="采集名称" min-width="180" align="center">
<template #default="{ row }">
<el-input v-model="row.collectionName" placeholder="采集项名称" class="inner-cell-input"
@change="editor.saveDataCollections" />
<el-input
v-model="row.collectionName"
placeholder="采集项名称"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
@change="editor.saveDataCollections">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@change="editor.saveDataCollections"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="采集值" min-width="140" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="采集值"
min-width="140"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.collectionValue" placeholder="输入采集值" class="inner-cell-input" @change="editor.saveDataCollections" />
<el-input
v-model="row.collectionValue"
placeholder="输入采集值"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input"
@change="editor.saveDataCollections" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeDataCollectionRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeDataCollectionRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.dataCollections?.length" description="暂无采集项" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.dataCollections?.length"
description="暂无采集项"
:image-size="48"
/>
</el-tab-pane>
</el-tabs>
</div>
@@ -364,8 +623,7 @@
files.push({
id: f.id || `${sop.id}_${f.fileUrl}`,
fileName:
f.fileName ||
decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
f.fileName || decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
fileUrl: f.fileUrl,
fileType: f.fileType || ''
})
@@ -12,9 +12,15 @@
<!-- 顶部:任务基本信息一行展示 -->
<div v-if="editor.taskDetail.value" class="execution-header">
<div class="execution-header-row">
<span class="execution-header-item">任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span>
<span class="execution-header-item">工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span>
<span class="execution-header-item">任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span>
<span class="execution-header-item"
>任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span
>
<span class="execution-header-item"
>工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span
>
<span class="execution-header-item"
>任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span
>
<el-tag :type="editor.taskStatusTag(editor.taskDetail.value.taskStatus).type">
{{ editor.taskStatusTag(editor.taskDetail.value.taskStatus).label }}
</el-tag>
@@ -24,6 +30,7 @@
class="btn-main"
type="primary"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleStartTask"
>
<el-icon><VideoPlay /></el-icon>开始执行
@@ -33,13 +40,16 @@
class="btn-main"
type="success"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleCompleteTask"
>
<el-icon><Check /></el-icon>完成任务
</el-button>
</div>
</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{ editor.taskDetail.value.remark }}</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{
editor.taskDetail.value.remark
}}</div>
</div>
<!-- 主体:可折叠工艺库 + 步骤列表 + 详情面板 -->
@@ -48,44 +58,79 @@
<template v-if="!readonly">
<transition name="slide-lib">
<div v-show="editor.processLibraryExpanded.value" class="execution-lib-panel">
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input v-model="editor.processSearchKeyword.value" placeholder="搜索工序" clearable size="small" class="route-config-search" />
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item v-for="cat in editor.filteredProcessLibrary.value" :key="cat.categoryId" :name="cat.categoryId">
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div v-for="(proc, procIndex) in cat.processes" :key="proc.id" class="route-config-process-card" draggable="true" @dragstart="editor.onDragStart($event, proc)">
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
/>
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty v-if="!editor.filteredProcessLibrary.value.length" description="暂无工艺数据" :image-size="48" />
</div>
</transition>
<el-empty
v-if="!cat.processes.length"
description="该分类暂无工序"
:image-size="32"
/>
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
</transition>
<!-- 工艺库折叠把手 -->
<div class="execution-lib-toggle" @click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value">
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
<!-- 工艺库折叠把手 -->
<div
class="execution-lib-toggle"
@click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value"
>
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
</template>
<!-- 步骤列表 -->
<div class="execution-step-list" v-loading="editor.stepListLoading.value" @dragover.prevent @drop="editor.onDropToCenter">
<div
class="execution-step-list"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<div class="execution-step-list-header">
<h3>工序步骤</h3>
<span class="execution-progress-badge">{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span>
<span class="execution-progress-badge"
>{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span
>
</div>
<div class="execution-step-list-body">
<!-- 虚拟步骤:任务输入 -->
@@ -96,9 +141,11 @@
>
<div class="virtual-step-icon virtual-step-icon--input">📥</div>
<div class="virtual-step-info">
<div class="virtual-step-name">任务输入</div>
<div class="virtual-step-name">原材料(样本/细胞产品)</div>
</div>
<el-tag v-if="editor.virtualInputStatus.value === 'filled'" type="success" size="small">已填写 {{ editor.taskInputs.value.length }} 项</el-tag>
<el-tag v-if="editor.virtualInputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskInputs.value.length }} 项</el-tag
>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
@@ -110,9 +157,11 @@
>
<div class="virtual-step-icon virtual-step-icon--output">📤</div>
<div class="virtual-step-info">
<div class="virtual-step-name">任务输出</div>
<div class="virtual-step-name">产出(细胞产品)</div>
</div>
<el-tag v-if="editor.virtualOutputStatus.value === 'filled'" type="success" size="small">已填写 {{ editor.taskOutputs.value.length }} 项</el-tag>
<el-tag v-if="editor.virtualOutputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskOutputs.value.length }} 项</el-tag
>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
@@ -120,7 +169,12 @@
<div class="step-divider"><span>工艺步骤</span></div>
<!-- 真实步骤列表 -->
<draggable v-model="editor.stepList.value" :handle="readonly ? undefined : '.route-config-step-handle'" :animation="200" @end="editor.onStepDragEnd">
<draggable
v-model="editor.stepList.value"
:handle="readonly ? undefined : '.route-config-step-handle'"
:animation="200"
@end="editor.onStepDragEnd"
>
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
@@ -131,18 +185,39 @@
}"
@click="editor.selectStep(element)"
>
<div v-if="!readonly" class="route-config-step-handle"><el-icon><Rank /></el-icon></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{ element.stepStatus === 'ywc' ? '✓' : index + 1 }}</div>
<div v-if="!readonly" class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{
element.stepStatus === 'ywc' ? '✓' : index + 1
}}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-tag v-if="element.stepStatus" :type="editor.stepStatusTag(element.stepStatus).type" size="small" style="margin-right:4px">{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag>
<el-tag
v-if="element.stepStatus"
:type="editor.stepStatusTag(element.stepStatus).type"
size="small"
style="margin-right: 4px"
>{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag
>
<template v-if="!readonly">
<el-button link type="primary" size="small" title="复制步骤" @click.stop="editor.handleCopyStep(element)">
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click.stop="editor.handleDeleteStep(element)">
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
@@ -161,116 +236,62 @@
<!-- 任务输入详情 -->
<template v-if="editor.selectedVirtualStep.value === 'input'">
<div class="execution-detail-header">
<h3>📥 任务输入</h3>
<h3>📥 原材料(样本/细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button class="btn-main" type="primary" size="small" @click="editor.addTaskInputRow"><el-icon><Plus /></el-icon>添加输入</el-button>
<el-button class="btn-main" type="primary" size="small" :loading="editor.inputSaving.value" @click="editor.saveTaskInputs"><el-icon><Check /></el-icon>保存</el-button>
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.inputSaving.value"
@click="editor.saveTaskInputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
<div class="execution-detail-body">
<el-table :data="editor.taskInputs.value" stripe class="inner-table">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table-column label="输入类型" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.inputType === 'sample' ? '样本' : row.inputType === 'cell_product' ? '细胞产品' : row.inputType }}</span>
<el-select v-else v-model="row.inputType" placeholder="选择类型" class="inner-cell-select" @change="() => { row.inputId = ''; row.inputCode = ''; row.inputName = '' }">
<el-option label="样本" value="sample" />
<el-option label="细胞产品" value="cell_product" />
</el-select>
</template>
</el-table-column>
<el-table-column label="输入编码" min-width="150" align="center">
<template #default="{ row }">
<template v-if="row.inputType === 'cell_product'">
<el-select
v-if="!readonly"
v-model="row.inputId"
placeholder="选择细胞产品"
filterable
class="inner-cell-select"
:loading="editor.cellProductOptionsLoading.value"
@change="(val: string) => editor.onCellProductInputSelect(row, val)"
>
<el-option v-for="opt in editor.cellProductOptions.value" :key="opt.id" :label="opt.label" :value="opt.id" />
</el-select>
<span v-else>{{ row.inputCode || '—' }}</span>
</template>
<template v-else>
<span>{{ row.inputCode || '—' }}</span>
</template>
</template>
</el-table-column>
<el-table-column label="输入名称" min-width="150" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.inputName || '—' }}</span>
<el-input v-else v-model="row.inputName" placeholder="输入名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.remark || '—' }}</span>
<el-input v-else v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeTaskInputRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.taskInputs.value.length" description="暂无输入项" :image-size="48" />
<TaskInputEnhancedTable
:items="editor.taskInputs.value"
:loading="false"
:disabled="readonly"
:sample-code="editor.taskDetail.value?.sampleCode || ''"
:cell-product-options="editor.cellProductOptions.value"
:cell-product-options-loading="editor.cellProductOptionsLoading.value"
@update:items="(items: Record<string, any>[]) => (editor.taskInputs.value = items)"
@search-cell-product="(q: string) => editor.searchCellProductOptions(q)"
@save="editor.saveTaskInputs"
/>
</div>
</template>
<!-- 任务输出详情 -->
<template v-else-if="editor.selectedVirtualStep.value === 'output'">
<div class="execution-detail-header">
<h3>📤 任务输出</h3>
<h3>📤 产出(细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button class="btn-main" type="primary" size="small" @click="editor.addTaskOutputRow"><el-icon><Plus /></el-icon>添加输出</el-button>
<el-button class="btn-main" type="primary" size="small" :loading="editor.outputSaving.value" @click="editor.saveTaskOutputs"><el-icon><Check /></el-icon>保存</el-button>
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.outputSaving.value"
@click="editor.saveTaskOutputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
<div class="execution-detail-body">
<el-table :data="editor.taskOutputs.value" stripe class="inner-table">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table-column label="输出名称" min-width="140" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.outputName || '—' }}</span>
<el-input v-else v-model="row.outputName" placeholder="输出名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="输出条码" min-width="140" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.outputBarcode || '—' }}</span>
<el-input v-else v-model="row.outputBarcode" placeholder="扫码或输入" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="数量" min-width="100" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.quantity ?? '—' }}</span>
<el-input-number v-else v-model="row.quantity" :min="0" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="单位" min-width="80" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.unit || '—' }}</span>
<el-input v-else v-model="row.unit" placeholder="单位" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.remark || '—' }}</span>
<el-input v-else v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeTaskOutputRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.taskOutputs.value.length" description="暂无输出项" :image-size="48" />
<TaskOutputList
:items="editor.taskOutputs.value"
:loading="false"
:product-type-options="editor.productTypeOptions.value"
:spec-options="editor.specOptions.value"
:storage-slot-options="editor.storageSlotOptions.value"
:unit-options="editor.itemUnitOptions.value"
:unit-options-loading="editor.itemUnitOptionsLoading.value"
:disabled="readonly"
@update:items="(items: Record<string, any>[]) => (editor.taskOutputs.value = items)"
@search-unit="(q: string) => editor.searchItemUnitOptions(q)"
@save="editor.saveTaskOutputs"
/>
</div>
</template>
@@ -288,6 +309,8 @@
import { VueDraggable as draggable } from 'vue-draggable-plus'
import { useTaskStepEditor } from './useTaskStepEditor'
import StepDetailPanel from './StepDetailPanel.vue'
import TaskInputEnhancedTable from './TaskInputEnhancedTable.vue'
import TaskOutputList from './TaskOutputList.vue'
const props = defineProps<{
visible: boolean
@@ -319,11 +342,14 @@
return 'seq-pending'
}
watch(() => props.visible, async (val) => {
if (val && props.taskId) {
await editor.initEditor(props.taskId, { mode: 'execution' })
watch(
() => props.visible,
async (val) => {
if (val && props.taskId) {
await editor.initEditor(props.taskId, { mode: 'execution' })
}
}
})
)
</script>
<style scoped lang="scss">
@@ -349,135 +375,280 @@
border: 1px solid #d1fae5;
}
.execution-header-row {
display: flex; align-items: center; gap: 16px;
font-size: 13px; color: #475569;
b { color: #1e293b; }
display: flex;
align-items: center;
gap: 16px;
font-size: 13px;
color: #475569;
b {
color: #1e293b;
}
}
.execution-header-item {
white-space: nowrap;
}
.execution-header-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.execution-header-remark {
margin-top: 4px;
font-size: 12px;
color: #64748b;
}
.execution-header-item { white-space: nowrap; }
.execution-header-actions { display: flex; gap: 8px; margin-left: auto; }
.execution-header-remark { margin-top: 4px; font-size: 12px; color: #64748b; }
/* 主体三区域 */
.execution-main {
display: flex; gap: 0;
display: flex;
gap: 0;
height: calc(96vh - 110px);
min-height: 400px;
}
/* 工艺库折叠侧栏 */
.execution-lib-panel {
width: 260px; flex-shrink: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.execution-lib-header {
padding: 10px 12px; border-bottom: 1px solid #e5e7eb; flex-shrink: 0;
h3 { margin: 0 0 6px; font-size: 13px; font-weight: 600; color: #1f2937; }
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
}
.slide-lib-enter-active, .slide-lib-leave-active {
transition: width 0.2s ease, opacity 0.2s ease;
.slide-lib-enter-active,
.slide-lib-leave-active {
transition:
width 0.2s ease,
opacity 0.2s ease;
overflow: hidden;
}
.slide-lib-enter-from, .slide-lib-leave-to {
width: 0 !important; opacity: 0;
.slide-lib-enter-from,
.slide-lib-leave-to {
width: 0 !important;
opacity: 0;
}
/* 工艺库折叠把手 */
.execution-lib-toggle {
width: 24px; flex-shrink: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 4px; cursor: pointer;
background: #f9fafb; border-left: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;
width: 24px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
background: #f9fafb;
border-left: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
transition: background 0.15s;
font-size: 14px; color: #9ca3af;
&:hover { background: #ecfdf5; color: #10b981; }
font-size: 14px;
color: #9ca3af;
&:hover {
background: #ecfdf5;
color: #10b981;
}
}
.execution-lib-toggle-text {
writing-mode: vertical-lr; font-size: 11px; letter-spacing: 2px;
writing-mode: vertical-lr;
font-size: 11px;
letter-spacing: 2px;
}
/* 步骤列表 */
.execution-step-list {
width: 340px; flex-shrink: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
overflow: hidden; margin: 0 8px;
width: 340px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin: 0 8px;
}
.execution-step-list-header {
padding: 10px 14px; border-bottom: 1px solid #e5e7eb;
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #1f2937; }
padding: 10px 14px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
}
.execution-progress-badge {
font-size: 12px; color: #6b7280; background: #f3f4f6;
padding: 2px 10px; border-radius: 10px;
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 2px 10px;
border-radius: 10px;
}
.execution-step-list-body {
flex: 1; overflow-y: auto; padding: 8px;
flex: 1;
overflow-y: auto;
padding: 8px;
}
.execution-step-list-empty {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 40px 0; color: #9ca3af;
p { margin-top: 8px; font-size: 13px; }
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #9ca3af;
p {
margin-top: 8px;
font-size: 13px;
}
}
/* 虚拟步骤卡片 */
/* 虚拟步骤卡片(原材料 / 产出 — 红色系强调必填) */
.virtual-step-card {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; margin-bottom: 4px;
border-radius: 8px; cursor: pointer;
border: 2px solid #bfdbfe; background: #eff6ff;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
border: 2px solid #fecaca;
background: #fef2f2;
transition: all 0.15s;
&:hover { border-color: #93c5fd; background: #dbeafe; }
&.is-active { border-color: #3b82f6; background: #dbeafe; box-shadow: 0 0 0 1px #3b82f6; }
&:hover {
border-color: #fca5a5;
background: #fee2e2;
}
&.is-active {
border-color: #ef4444;
background: #fee2e2;
box-shadow: 0 0 0 1px #ef4444;
}
}
.virtual-step-icon {
width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; flex-shrink: 0;
&--input { background: #dbeafe; }
&--output { background: #fef3c7; }
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
&--input,
&--output {
background: #fecaca;
}
}
.virtual-step-required {
margin-left: 8px;
font-size: 11px;
font-weight: 500;
color: #dc2626;
padding: 1px 6px;
border-radius: 4px;
background: #fee2e2;
border: 1px solid #fecaca;
}
.virtual-step-info {
flex: 1;
min-width: 0;
}
.virtual-step-name {
font-size: 13px;
font-weight: 500;
color: #1e293b;
}
.virtual-step-info { flex: 1; min-width: 0; }
.virtual-step-name { font-size: 13px; font-weight: 500; color: #1e293b; }
/* 分割线 */
.step-divider {
display: flex; align-items: center; gap: 8px;
margin: 8px 0; padding: 0 4px;
color: #9ca3af; font-size: 11px;
&::before, &::after { content: ''; flex: 1; height: 1px; background: #e5e7eb; }
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
padding: 0 4px;
color: #9ca3af;
font-size: 11px;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
}
/* 步骤序号状态 */
.seq-done { background: #d1fae5 !important; color: #059669 !important; }
.seq-running { background: #10b981 !important; color: #fff !important; }
.seq-pending { background: #f3f4f6 !important; color: #6b7280 !important; }
.route-config-step-card.is-done { opacity: 0.6; }
.seq-done {
background: #d1fae5 !important;
color: #059669 !important;
}
.seq-running {
background: #10b981 !important;
color: #fff !important;
}
.seq-pending {
background: #f3f4f6 !important;
color: #6b7280 !important;
}
.route-config-step-card.is-done {
opacity: 0.6;
}
/* 右侧详情面板 */
.execution-detail-panel {
flex: 1; min-width: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.execution-detail-header {
padding: 10px 16px; border-bottom: 1px solid #e5e7eb;
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #1e293b; }
padding: 10px 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
}
.execution-detail-actions {
display: flex;
gap: 8px;
}
.execution-detail-actions { display: flex; gap: 8px; }
.execution-detail-body {
flex: 1; overflow-y: auto; padding: 16px;
flex: 1;
overflow-y: auto;
padding: 16px;
}
.section-bar {
display: inline-block; width: 3px; height: 14px;
background: #10b981; border-radius: 2px; margin-right: 8px;
display: inline-block;
width: 3px;
height: 14px;
background: #10b981;
border-radius: 2px;
margin-right: 8px;
}
</style>
@@ -0,0 +1,361 @@
<template>
<div class="task-input-table">
<!-- 工具栏 -->
<div class="panel-top">
<h2 class="title-bar-heading">原材料(样本/细胞产品)</h2>
<div v-if="!disabled" class="panel-top-actions">
<el-button type="primary" size="small" @click="handleAddSample">
<el-icon><Plus /></el-icon>
添加样本
</el-button>
<el-button size="small" @click="handleAddCellProduct">
<el-icon><Plus /></el-icon>
添加细胞产品
</el-button>
</div>
</div>
<!-- 列表 -->
<el-table :data="items" size="small" stripe border v-loading="loading" empty-text="暂无原材料">
<el-table-column type="index" label="序号" width="56" align="center" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="inputTypeTagType(row.inputType)" size="small">
{{ row.inputType === 'cell_product' ? '细胞产品' : '样本' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="编码" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.inputType === 'sample' && !disabled">
<el-input v-model="row.inputCode" placeholder="请输入编码" />
</template>
<template v-else>{{ row.inputCode || row.productCode || '—' }}</template>
</template>
</el-table-column>
<el-table-column label="名称" min-width="120" align="center" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.inputType === 'sample' && !disabled">
<el-input v-model="row.inputName" placeholder="请输入名称" />
</template>
<template v-else>{{ row.inputName || row.productName || '—' }}</template>
</template>
</el-table-column>
<!-- 细胞产品特有字段 -->
<el-table-column label="产品类型" min-width="90" show-overflow-tooltip>
<template #default="{ row }">
{{ row.productTypeName || '—' }}
</template>
</el-table-column>
<el-table-column label="代次" width="70" align="center">
<template #default="{ row }">
{{ row.passage || '—' }}
</template>
</el-table-column>
<el-table-column label="储物格位置" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.storageSlotName || '—' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<template v-if="row.inputType === 'cell_product'">
<el-tag :type="inputStatusTagType(row.productStatus)" size="small">
{{ inputStatusLabel(row.productStatus) }}
</el-tag>
</template>
<template v-else>—</template>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="!disabled">
<el-input v-model="row.remark" placeholder="备注" />
</template>
<template v-else>{{ row.remark || '—' }}</template>
</template>
</el-table-column>
<el-table-column v-if="!disabled" label="操作" width="80" fixed="right" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="handleDelete($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 细胞产品选择弹窗 -->
<el-dialog
v-model="selectDialogVisible"
title="选择细胞产品"
width="1160px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div class="select-dialog-body">
<!-- 搜索框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索产品编码或名称"
clearable
style="margin-bottom: 12px"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<!-- 可选列表 -->
<div class="list-body list-wrap">
<el-table
ref="selectTableRef"
class="list-table"
:data="filteredCellProducts"
stripe
row-key="id"
height="380"
v-loading="cellProductOptionsLoading"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="45"
align="center"
:selectable="isRowSelectable"
/>
<el-table-column
label="产品编码"
prop="productCode"
min-width="210"
show-overflow-tooltip
/>
<el-table-column
label="产品名称"
prop="productName"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
label="产品类型"
prop="productTypeName"
min-width="90"
show-overflow-tooltip
/>
<el-table-column label="代次" prop="passage" width="70" align="center" />
<el-table-column
label="储物格位置"
prop="storageSlotName"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="inputStatusTagType(row.productStatus)" size="small">
{{ inputStatusLabel(row.productStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<el-button @click="selectDialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedRows.length" @click="confirmSelect">
确定(已选 {{ selectedRows.length }} 项)
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue'
defineOptions({ name: 'TaskInputEnhancedTable' })
const props = withDefaults(
defineProps<{
items: Record<string, any>[]
loading: boolean
disabled?: boolean
sampleCode?: string
cellProductOptions: {
id: string
label: string
productCode?: string
productName?: string
productTypeName?: string
passage?: string
storageSlotName?: string
productStatus?: string
}[]
cellProductOptionsLoading: boolean
}>(),
{ disabled: false, sampleCode: '' }
)
const emit = defineEmits<{
'update:items': [items: Record<string, any>[]]
searchCellProduct: [query: string]
save: []
}>()
const selectDialogVisible = ref(false)
const selectTableRef = ref()
const searchKeyword = ref('')
const selectedRows = ref<Record<string, any>[]>([])
/** 样本 success(绿),细胞产品 warning(琥珀),避免双绿相近 */
function inputTypeTagType(inputType: string) {
return inputType === 'cell_product' ? 'warning' : 'success'
}
function inputStatusTagType(status: string) {
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
voided: 'danger',
thawed: 'info'
}
return map[status] || 'info'
}
function inputStatusLabel(status: string) {
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
voided: '已作废',
thawed: '已复苏'
}
return map[status] || status || '—'
}
const filteredCellProducts = computed(() => {
const keyword = searchKeyword.value?.trim().toLowerCase() || ''
if (!keyword) return props.cellProductOptions
return props.cellProductOptions.filter(
(cp) =>
(cp.productCode || '').toLowerCase().includes(keyword) ||
(cp.productName || '').toLowerCase().includes(keyword) ||
(cp.label || '').toLowerCase().includes(keyword)
)
})
function isRowSelectable(row: Record<string, any>) {
const existingIds = new Set(
props.items.filter((i) => i.inputType === 'cell_product').map((i) => i.inputId)
)
return !existingIds.has(row.id)
}
function handleSearch() {
emit('searchCellProduct', searchKeyword.value)
}
function handleAddSample() {
const newItems = [...props.items]
// 自动填入样本编码:如果工单关联了样本编码,且尚未添加过,则自动填入
let autoCode = ''
const code = (props.sampleCode || '').trim()
if (code) {
const alreadyAdded = props.items.some(
(i) => i.inputType === 'sample' && (i.inputCode || '').trim() === code
)
if (!alreadyAdded) {
autoCode = code
}
}
newItems.push({
inputType: 'sample',
inputId: '',
inputCode: autoCode,
inputName: '',
remark: ''
})
emit('update:items', newItems)
}
function handleAddCellProduct() {
searchKeyword.value = ''
selectedRows.value = []
selectDialogVisible.value = true
emit('searchCellProduct', '')
}
function handleSelectionChange(rows: Record<string, any>[]) {
selectedRows.value = rows
}
function confirmSelect() {
if (!selectedRows.value.length) return
const existingIds = new Set(
props.items.filter((i) => i.inputType === 'cell_product').map((i) => i.inputId)
)
const newItems = [...props.items]
let addedCount = 0
for (const row of selectedRows.value) {
if (existingIds.has(row.id)) continue
newItems.push({
inputType: 'cell_product',
inputId: row.id,
inputCode: row.productCode || '',
inputName: row.productName || '',
productCode: row.productCode || '',
productName: row.productName || '',
productTypeName: row.productTypeName || '',
passage: row.passage || '',
storageSlotName: row.storageSlotName || '',
productStatus: row.productStatus || '',
remark: ''
})
addedCount++
}
emit('update:items', newItems)
selectDialogVisible.value = false
if (addedCount === 0) {
ElMessage.info('所选细胞产品已在列表中')
}
}
async function handleDelete(index: number) {
await ElMessageBox.confirm('确定要删除该原材料吗?', '提示', {
type: 'warning'
})
const newItems = [...props.items]
newItems.splice(index, 1)
emit('update:items', newItems)
}
</script>
<style lang="scss" scoped>
.task-input-table {
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.panel-top-actions {
display: flex;
gap: 10px;
}
}
.select-dialog-body {
padding: 0 4px;
}
// 样本行输入框高度适配
:deep(.el-table__cell) {
.el-input {
.el-input__wrapper {
min-height: 32px;
}
}
}
}
</style>
@@ -0,0 +1,177 @@
<template>
<div class="task-output-list">
<!-- 工具栏 -->
<div class="panel-top">
<h2 class="title-bar-heading">产出(细胞产品)</h2>
<el-button v-if="!disabled" type="primary" size="small" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增产出
</el-button>
</div>
<!-- 列表 -->
<el-table
:data="items"
size="small"
stripe
border
v-loading="loading"
empty-text="暂无产出产品"
>
<el-table-column type="index" label="序号" width="56" align="center" />
<el-table-column label="产品编码" prop="productCode" min-width="140" show-overflow-tooltip />
<el-table-column label="产品名称" prop="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="产品类型" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ row.productTypeName || '—' }}
</template>
</el-table-column>
<el-table-column label="规格" min-width="80" show-overflow-tooltip>
<template #default="{ row }">
{{ row.specName || '—' }}
</template>
</el-table-column>
<el-table-column label="代次" prop="passage" width="70" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="110" show-overflow-tooltip />
<el-table-column label="储物格位置" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.storageSlotName || '—' }}
</template>
</el-table-column>
<el-table-column label="数量" width="80" align="center">
<template #default="{ row }">
{{ row.quantity ?? '—' }}
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="70" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.productStatus)" size="small">
{{ statusLabel(row.productStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!disabled" label="操作" width="140" fixed="right" align="center">
<template #default="{ row, $index }">
<el-button type="primary" link size="small" @click="handleEdit(row, $index)">
编辑
</el-button>
<el-button type="danger" link size="small" @click="handleDelete($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑弹窗 -->
<TaskOutputProductDialog
v-model="dialogVisible"
:edit-item="editingItem"
:product-type-options="productTypeOptions"
:spec-options="specOptions"
:storage-slot-options="storageSlotOptions"
:unit-options="unitOptions"
:unit-options-loading="unitOptionsLoading"
@confirm="handleDialogConfirm"
@search-unit="(q: string) => emit('searchUnit', q)"
/>
</div>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import TaskOutputProductDialog from './TaskOutputProductDialog.vue'
defineOptions({ name: 'TaskOutputList' })
const props = withDefaults(
defineProps<{
items: Record<string, any>[]
loading: boolean
productTypeOptions: Array<{ id: string; productTypeName: string }>
specOptions: Array<{ id: string; specName: string }>
storageSlotOptions: Array<{ id: string; name: string }>
unitOptions: { label: string; value: string }[]
unitOptionsLoading: boolean
disabled?: boolean
}>(),
{ disabled: false }
)
const emit = defineEmits<{
'update:items': [items: Record<string, any>[]]
searchUnit: [query: string]
save: []
}>()
const dialogVisible = ref(false)
const editingItem = ref<Record<string, any> | null>(null)
const editingIndex = ref(-1)
function statusTagType(status: string) {
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
voided: 'danger',
thawed: 'info'
}
return map[status] || 'info'
}
function statusLabel(status: string) {
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
voided: '已作废',
thawed: '已复苏'
}
return map[status] || status || '—'
}
function handleAdd() {
editingItem.value = null
editingIndex.value = -1
dialogVisible.value = true
}
function handleEdit(row: Record<string, any>, index: number) {
editingItem.value = { ...row }
editingIndex.value = index
dialogVisible.value = true
}
async function handleDelete(index: number) {
await ElMessageBox.confirm('确定要删除该产出产品吗?删除后将在保存时生效。', '提示', {
type: 'warning'
})
const newItems = [...props.items]
newItems.splice(index, 1)
emit('update:items', newItems)
}
function handleDialogConfirm(data: Record<string, any>) {
const newItems = [...props.items]
if (editingIndex.value >= 0) {
// 编辑:替换原有项,保留 cellProductId
newItems[editingIndex.value] = {
...newItems[editingIndex.value],
...data
}
} else {
// 新增:添加到列表
newItems.push(data)
}
emit('update:items', newItems)
}
</script>
<style lang="scss" scoped>
.task-output-list {
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
}
</style>
@@ -0,0 +1,654 @@
<template>
<el-dialog
:model-value="modelValue"
:title="isEdit ? '编辑产出产品' : '新增产出产品'"
width="1000px"
class="dialog-form-layout task-output-product-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="task-output-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-3">
<!-- 产品编码 -->
<el-form-item label="产品编码" prop="productCode">
<el-input v-model="formData.productCode" placeholder="请输入产品编码" />
</el-form-item>
<!-- 产品名称 -->
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" />
</el-form-item>
<!-- 产品类型 -->
<el-form-item label="产品类型">
<el-select
v-model="formData.productTypeId"
placeholder="请选择产品类型"
clearable
style="width: 100%"
>
<el-option
v-for="item in productTypeOptions"
:key="item.id"
:label="item.productTypeName"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 规格 -->
<el-form-item label="规格">
<el-select
v-model="formData.specId"
placeholder="请选择规格"
clearable
style="width: 100%"
>
<el-option
v-for="item in specOptions"
:key="item.id"
:label="item.specName"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 代次 -->
<el-form-item label="代次">
<el-select
v-model="formData.passage"
placeholder="请选择代次"
clearable
style="width: 100%"
>
<el-option v-for="p in passageOptions" :key="p" :label="p" :value="p" />
</el-select>
</el-form-item>
<!-- 批次号 -->
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="formData.batchNo" placeholder="请输入批次号" />
</el-form-item>
<!-- 储物格位置 -->
<el-form-item label="储物格位置">
<el-select
v-model="formData.storageSlotId"
placeholder="请选择储物格"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in storageSlotOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 预计细胞数量 -->
<el-form-item label="预计细胞数量(亿)">
<el-input-number
v-model="formData.expectedCellCount"
:min="0"
:precision="4"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 活性 -->
<el-form-item label="活性(%)">
<el-input-number
v-model="formData.viability"
:min="0"
:max="100"
:precision="1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 冻存日期 -->
<el-form-item label="冻存日期">
<el-date-picker
v-model="formData.freezeDate"
type="date"
placeholder="请选择冻存日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<!-- 数量 -->
<el-form-item label="数量">
<el-input-number
v-model="formData.quantity"
:min="0"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 单位 -->
<el-form-item label="单位">
<el-select
v-model="formData.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
style="width: 100%"
:loading="unitOptionsLoading"
:remote-method="(q: string) => emit('searchUnit', q)"
@visible-change="(visible: boolean) => visible && emit('searchUnit', '')"
>
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 有效期 -->
<el-form-item label="有效期">
<el-date-picker
v-model="formData.expiryDate"
type="date"
placeholder="请选择有效期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<!-- 产品图片 (full width) -->
<el-form-item label="产品图片" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="productImageUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*"
@change="handleProductImageChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传图片</span>
</div>
</el-upload>
<div
v-for="(file, index) in productImageFiles"
:key="file.id || index"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="
file.fileUrl && !file._uploading ? handlePreviewProductImage(file) : undefined
"
>
<template v-if="file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else>
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click="handleDownloadProductImage(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="removeProductImage(index)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片上传,单文件不超过 10MB</span>
</div>
</el-form-item>
<!-- 备注 (full width) -->
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<!-- 图片预览弹窗 -->
<el-dialog v-model="previewVisible" title="图片预览" width="800px" align-center destroy-on-close>
<div class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { Plus, Close, Check, Delete, Download, Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'TaskOutputProductDialog' })
type ProductImageFile = {
id?: string
fileName: string
fileUrl: string
fileType?: string
_uploading?: boolean
}
const props = defineProps<{
modelValue: boolean
editItem?: Record<string, any> | null
productTypeOptions: Array<{ id: string; productTypeName: string }>
specOptions: Array<{ id: string; specName: string }>
storageSlotOptions: Array<{ id: string; name: string }>
unitOptions: { label: string; value: string }[]
unitOptionsLoading: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: [data: Record<string, any>]
searchUnit: [query: string]
}>()
const passageOptions = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8']
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
return suffix
return `${minioBaseUrl}/${suffix}`
}
const isEdit = computed(() => !!props.editItem?.cellProductId)
const formRef = ref()
const submitLoading = ref(false)
const productImageUploadRef = ref()
const previewVisible = ref(false)
const previewUrl = ref('')
const productImageFiles = ref<ProductImageFile[]>([])
const formData = reactive({
cellProductId: '',
productCode: '',
productName: '',
productTypeId: '',
specId: '',
passage: '',
batchNo: '',
storageSlotId: '',
expectedCellCount: undefined as number | undefined,
viability: undefined as number | undefined,
freezeDate: '',
quantity: 1 as number,
unit: '',
expiryDate: '',
productImages: '',
remark: ''
})
const formRules = {
productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
}
// Watch editItem to populate form
watch(
() => props.editItem,
(item) => {
if (item) {
formData.cellProductId = item.cellProductId || ''
formData.productCode = item.productCode || ''
formData.productName = item.productName || ''
formData.productTypeId = item.productTypeId || ''
formData.specId = item.specId || ''
formData.passage = item.passage || ''
formData.batchNo = item.batchNo || ''
formData.storageSlotId = item.storageSlotId || ''
formData.expectedCellCount = item.expectedCellCount ?? undefined
formData.viability = item.viability ?? undefined
formData.freezeDate = item.freezeDate || ''
formData.quantity = item.quantity ?? 1
formData.unit = item.unit || ''
formData.expiryDate = item.expiryDate || ''
formData.productImages = item.productImages || ''
formData.remark = item.remark || ''
syncFormToImages(item.productImages || '')
} else {
resetForm()
}
},
{ immediate: true }
)
function resetForm() {
formData.cellProductId = ''
formData.productCode = ''
formData.productName = ''
formData.productTypeId = ''
formData.specId = ''
formData.passage = ''
formData.batchNo = ''
formData.storageSlotId = ''
formData.expectedCellCount = undefined
formData.viability = undefined
formData.freezeDate = ''
formData.quantity = 1
formData.unit = ''
formData.expiryDate = ''
formData.productImages = ''
formData.remark = ''
productImageFiles.value = []
}
function fileNameFromUrl(url: string, index: number) {
const segment = url.split('/').pop() || ''
const base = decodeURIComponent(segment.split('?')[0] || '')
return base || `图片${index + 1}`
}
function syncImagesToForm() {
formData.productImages = productImageFiles.value
.filter((f) => f.fileUrl && !f._uploading)
.map((f) => f.fileUrl)
.join(',')
}
function syncFormToImages(imagesStr: string) {
productImageFiles.value = imagesStr
? imagesStr
.split(',')
.map((s, i) => {
const url = s.trim()
if (!url) return null
return {
id: `existing_${i}`,
fileName: fileNameFromUrl(url, i),
fileUrl: url,
fileType: 'image/'
} as ProductImageFile
})
.filter((f): f is ProductImageFile => Boolean(f))
: []
}
async function handleProductImageChange(uploadFile: any) {
const file = uploadFile.raw || uploadFile
if (!file) return
const maxSizeMb = 10
if (file.size / 1024 / 1024 > maxSizeMb) {
ElMessage.error(`图片大小不能超过 ${maxSizeMb}MB`)
productImageUploadRef.value?.clearFiles()
return
}
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
productImageFiles.value.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileType: file.type || 'image/',
_uploading: true
})
const idx = productImageFiles.value.length - 1
try {
const fd = new FormData()
fd.append('folder', 'cell-product')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
productImageFiles.value[idx] = {
...productImageFiles.value[idx],
id: res.id || tempId,
fileName: file.name || res.fileName || fileNameFromUrl(res.fileUrl, idx),
fileUrl: res.fileUrl,
fileType: res.fileType || file.type || 'image/',
_uploading: false
}
syncImagesToForm()
} catch {
ElMessage.error('图片上传失败')
productImageFiles.value.splice(idx, 1)
} finally {
nextTick(() => productImageUploadRef.value?.clearFiles())
}
}
function removeProductImage(index: number) {
productImageFiles.value.splice(index, 1)
syncImagesToForm()
}
function handlePreviewProductImage(file: ProductImageFile) {
if (file._uploading || !file.fileUrl) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewVisible.value = true
}
function handleDownloadProductImage(file: ProductImageFile) {
downloadAttachmentFile(file, { fullFileUrl })
}
async function handleSubmit() {
await formRef.value?.validate?.()
syncImagesToForm()
submitLoading.value = true
try {
const result: Record<string, any> = {
cellProductId: formData.cellProductId || undefined,
productCode: formData.productCode.trim(),
productName: formData.productName.trim(),
productTypeId: formData.productTypeId || undefined,
specId: formData.specId || undefined,
passage: formData.passage || undefined,
batchNo: formData.batchNo.trim() || undefined,
storageSlotId: formData.storageSlotId || undefined,
expectedCellCount: formData.expectedCellCount ?? undefined,
viability: formData.viability ?? undefined,
freezeDate: formData.freezeDate || undefined,
quantity: formData.quantity,
unit: formData.unit?.trim() || undefined,
expiryDate: formData.expiryDate || undefined,
productImages: formData.productImages?.trim() || undefined,
productStatus: 'frozen',
remark: formData.remark?.trim() || undefined
}
emit('confirm', result)
emit('update:modelValue', false)
} finally {
submitLoading.value = false
}
}
</script>
<style scoped lang="scss">
.task-output-product-dialog {
:deep(.el-dialog__body) {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
}
.attachment-upload {
width: 100%;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s;
&:hover {
border-color: #10b981;
border-style: solid;
background: #ecfdf5;
}
}
.upload-card-text {
font-size: 12px;
color: #10b981;
font-weight: 500;
line-height: 1;
}
.upload-hint {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1;
margin-top: 8px;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.attachment-item {
width: 140px;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #10b981;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
}
&.is-uploading {
opacity: 0.6;
pointer-events: none;
}
}
.attachment-preview {
width: 100%;
height: 96px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.attachment-footer {
display: flex;
flex-direction: column;
flex: 1;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
color: #334155;
line-height: 1.4;
word-break: break-all;
min-height: calc(11px * 1.4 * 2);
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 4px;
flex-shrink: 0;
}
.action-btn {
width: 22px;
height: 22px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #64748b;
cursor: pointer;
background: #f1f5f9;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #10b981;
color: #fff;
}
&--danger:hover {
background: #ef4444;
color: #fff;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.workOrderNo" placeholder="请输入工单号" clearable />
</el-form-item>
<el-form-item label="工单状态">
<el-select v-model="searchForm.workOrderStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.workOrderStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待配置" value="dpz" />
<el-option label="已完成" value="ywc" />
</el-select>
@@ -27,8 +32,12 @@
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
</div>
</el-form>
@@ -43,7 +52,13 @@
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
type="index"
label="序号"
width="68"
align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<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" />
@@ -71,10 +86,20 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<el-table-column label="操作" width="460" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
width="460"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看详情</el-button>
<el-button class="btn-op-item" @click="handleEditTask(row)"><el-icon><EditPen /></el-icon>编辑任务</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看详情</el-button
>
<el-button class="btn-op-item" @click="handleEditTask(row)"
><el-icon><EditPen /></el-icon>编辑任务</el-button
>
<el-button
v-if="row.workOrderStatus === 'dpz'"
class="btn-op-item primary"
@@ -144,11 +169,30 @@
@success="loadData"
/>
<!-- 编辑任务基本信息弹窗 -->
<EditTaskInfoDialog
v-model:visible="editTaskInfoVisible"
:task-id="editTaskInfoId"
@success="handleEditTaskInfoSuccess"
/>
<!-- 任务列表弹窗 -->
<el-dialog v-model="taskSelectVisible" title="生产任务列表" width="780px" destroy-on-close align-center>
<div v-loading="taskSelectLoading" style="max-height:60vh;overflow-y:auto">
<el-table :data="taskSelectList" stripe style="width:100%">
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => i + 1" />
<el-dialog
v-model="taskSelectVisible"
title="生产任务列表"
width="980px"
destroy-on-close
align-center
>
<div v-loading="taskSelectLoading" style="max-height: 60vh; overflow-y: auto">
<el-table :data="taskSelectList" stripe style="width: 100%">
<el-table-column
type="index"
label="序号"
width="68"
align="center"
:index="(i) => i + 1"
/>
<el-table-column prop="taskName" label="任务名称" min-width="140" />
<el-table-column prop="routeName" label="工艺路线" min-width="140">
<template #default="{ row }">{{ row.routeName || '—' }}</template>
@@ -160,14 +204,29 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openTaskEditor(row)">编辑</el-button>
<el-button link type="primary" size="small" @click="openEditTaskInfo(row)"
>编辑信息</el-button
>
<el-button link type="primary" size="small" @click="openTaskEditor(row)"
>编辑工艺</el-button
>
<el-button link type="danger" size="small" @click="handleDeleteTask(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!taskSelectLoading && !taskSelectList.length" description="暂无任务,请先派发任务" :image-size="48" />
<el-empty
v-if="!taskSelectLoading && !taskSelectList.length"
description="暂无任务,请先派发任务"
:image-size="48"
/>
</div>
</el-dialog>
</section>
@@ -178,7 +237,15 @@
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import type { TableInstance } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, RefreshLeft, View, Check, Document, Position, EditPen } from '@element-plus/icons-vue'
import {
Search,
RefreshLeft,
View,
Check,
Document,
Position,
EditPen
} from '@element-plus/icons-vue'
import {
fetchProductionWorkOrderList,
fetchProductionWorkOrderDetail,
@@ -195,7 +262,11 @@
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import DispatchTaskDialog from './DispatchTaskDialog.vue'
import { fetchProductionTaskList } from '@/api/production-task'
import EditTaskInfoDialog from './EditTaskInfoDialog.vue'
import {
fetchProductionTaskList,
fetchDeleteProductionTask
} from '@/api/production-task'
defineOptions({ name: 'ProductionProductionWorkOrder' })
@@ -248,8 +319,16 @@
await layoutWorkOrderTable()
}
}
function handleSearch() { pagination.page = 1; loadData() }
function handleReset() { searchForm.workOrderNo = ''; searchForm.workOrderStatus = ''; pagination.page = 1; loadData() }
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.workOrderNo = ''
searchForm.workOrderStatus = ''
pagination.page = 1
loadData()
}
const viewVisible = ref(false)
const detail = ref<ProductionWorkOrderDetail | null>(null)
@@ -262,12 +341,15 @@
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>>
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()
@@ -310,9 +392,10 @@
)
}
if (r.etaTime) transportFields.push({ label: '预计到达', value: r.etaTime })
const transportSection = transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
const transportSection =
transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
return [
{
title: '基本信息',
@@ -320,9 +403,20 @@
{ label: '工单号', value: d.workOrderNo },
{ label: '协议号', value: d.protocolNo },
{ label: '样本编码', value: d.sampleCode },
{ label: '来源类型', value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''), tagType: 'primary' },
{
label: '来源类型',
value:
r.sourceTypeLabel ||
d.receptionSourceLabel ||
receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary'
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' },
{ label: '工单状态', value: woStatusTag(d.workOrderStatus).label, tagType: woStatusTag(d.workOrderStatus).type as any },
{
label: '工单状态',
value: woStatusTag(d.workOrderStatus).label,
tagType: woStatusTag(d.workOrderStatus).type as any
},
{ label: '创建时间', value: d.createTime || '—' },
{ label: '备注', value: d.remark || '—', span: true }
]
@@ -337,7 +431,17 @@
{ 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:
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 }
]
@@ -374,8 +478,10 @@
const taskSelectVisible = ref(false)
const taskSelectList = ref<any[]>([])
const taskSelectLoading = ref(false)
const currentWorkOrderRow = ref<any>(null)
async function handleEditTask(row: any) {
currentWorkOrderRow.value = row
taskSelectLoading.value = true
taskSelectVisible.value = true
taskSelectList.value = []
@@ -397,6 +503,30 @@
taskSelectVisible.value = false
}
// ── 编辑任务基本信息 ──────────────────────────────────
const editTaskInfoVisible = ref(false)
const editTaskInfoId = ref('')
function openEditTaskInfo(task: any) {
editTaskInfoId.value = task.id
editTaskInfoVisible.value = true
}
function handleEditTaskInfoSuccess() {
if (currentWorkOrderRow.value) {
handleEditTask(currentWorkOrderRow.value)
}
}
async function handleDeleteTask(task: any) {
await ElMessageBox.confirm('确定要删除该任务吗?删除后不可恢复。', '提示', { type: 'warning' })
await fetchDeleteProductionTask(task.id)
ElMessage.success('任务已删除')
if (currentWorkOrderRow.value) {
handleEditTask(currentWorkOrderRow.value)
}
}
onMounted(() => loadData())
</script>
@@ -411,8 +541,15 @@
font-size: 13px;
align-items: start;
dt { margin: 0; color: #64748b; font-weight: 500; }
dd { margin: 0; color: #1e293b; }
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
}
}
}
</style>
@@ -23,6 +23,13 @@ import {
} from '@/api/production'
import { fetchItemUnitList } from '@/api/admin'
import { fetchCellProductList } from '@/api/cell-product'
import {
fetchCellProductTypeList,
fetchCellProductSpecList,
fetchStorageFacilityList,
fetchStorageSlotList
} from '@/api/admin'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
// ── helpers ──────────────────────────────────────────────────────────
function toApiFlag(val: boolean | number | string | undefined): string {
@@ -90,9 +97,23 @@ export function useTaskStepEditor() {
const consumableOptionsLoading = ref(false)
const consumableMasterById = ref<Record<string, Record<string, any>>>({})
const cellProductOptions = ref<{ id: string; label: string }[]>([])
const cellProductOptions = ref<
Array<{
id: string
label: string
productCode?: string
productName?: string
productTypeName?: string
passage?: string
storageSlotName?: string
productStatus?: string
}>
>([])
const cellProductOptionsLoading = ref(false)
const cellProductMasterById = ref<Record<string, Record<string, any>>>({})
const productTypeOptions = ref<Array<{ id: string; productTypeName: string }>>([])
const specOptions = ref<Array<{ id: string; specName: string }>>([])
const storageSlotOptions = ref<Array<{ id: string; name: string }>>([])
function mapItemUnitOption(u: { unitName?: string; unitCode?: string }) {
const name = (u.unitName || '').trim()
@@ -108,7 +129,11 @@ export function useTaskStepEditor() {
async function searchItemUnitOptions(query: string) {
itemUnitOptionsLoading.value = true
try {
const res = await fetchItemUnitList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchItemUnitList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
itemUnitOptions.value = (res.list || []).map(mapItemUnitOption).filter((o) => o.value)
} catch {
itemUnitOptions.value = []
@@ -141,7 +166,11 @@ export function useTaskStepEditor() {
async function searchReagentOptions(query: string) {
reagentOptionsLoading.value = true
try {
const res = await fetchReagentList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchReagentList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
cacheReagentMasters(list)
reagentOptions.value = list
@@ -158,7 +187,10 @@ export function useTaskStepEditor() {
function ensureReagentOption(row: Record<string, any>) {
if (row.reagentId && !String(row.reagentId).startsWith('legacy:')) {
if (!reagentOptions.value.some((o) => o.id === row.reagentId))
reagentOptions.value = [{ id: row.reagentId, label: mapReagentOptionLabel(row) }, ...reagentOptions.value]
reagentOptions.value = [
{ id: row.reagentId, label: mapReagentOptionLabel(row) },
...reagentOptions.value
]
return
}
// reagentId 为空(后端不存储该字段)— 通过 reagentCode 反查主表真实 ID
@@ -175,7 +207,10 @@ export function useTaskStepEditor() {
const legacyId = `legacy:${code || name}`
row.reagentId = legacyId
if (!reagentOptions.value.some((o) => o.id === legacyId))
reagentOptions.value = [{ id: legacyId, label: mapReagentOptionLabel(row) }, ...reagentOptions.value]
reagentOptions.value = [
{ id: legacyId, label: mapReagentOptionLabel(row) },
...reagentOptions.value
]
}
function ensureAllStepReagentOptions() {
for (const step of stepList.value) {
@@ -183,11 +218,17 @@ export function useTaskStepEditor() {
}
}
function onReagentRowSelect(row: Record<string, any>, reagentId: string) {
if (!reagentId) { row.reagentId = ''; return }
if (String(reagentId).startsWith('legacy:')) { return }
if (!reagentId) {
row.reagentId = ''
return
}
if (String(reagentId).startsWith('legacy:')) {
return
}
// 去重:同一试剂不允许出现多次(排除当前行和 legacy 占位符)
const dup = (selectedStep.value?.reagents || []).some(
(r: Record<string, any>) => r !== row && r.reagentId === reagentId && !String(r.reagentId).startsWith('legacy:')
(r: Record<string, any>) =>
r !== row && r.reagentId === reagentId && !String(r.reagentId).startsWith('legacy:')
)
if (dup) {
ElMessage.warning('该试剂已添加,请勿重复选择')
@@ -195,11 +236,16 @@ export function useTaskStepEditor() {
return
}
const master = reagentMasterById.value[reagentId]
if (!master) { return }
if (!master) {
return
}
Object.assign(row, {
reagentId: master.id, reagentCode: master.reagentCode || '',
reagentName: master.reagentName || '', spec: master.spec || '',
standardUsage: master.standardUsage ?? '', unit: master.unit || '',
reagentId: master.id,
reagentCode: master.reagentCode || '',
reagentName: master.reagentName || '',
spec: master.spec || '',
standardUsage: master.standardUsage ?? '',
unit: master.unit || '',
remark: master.remark ?? master.description ?? ''
})
ensureItemUnitOption(row.unit)
@@ -230,7 +276,11 @@ export function useTaskStepEditor() {
async function searchConsumableOptions(query: string) {
consumableOptionsLoading.value = true
try {
const res = await fetchConsumableList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchConsumableList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
cacheConsumableMasters(list)
consumableOptions.value = list
@@ -247,7 +297,10 @@ export function useTaskStepEditor() {
function ensureConsumableOption(row: Record<string, any>) {
if (row.consumableId && !String(row.consumableId).startsWith('legacy:')) {
if (!consumableOptions.value.some((o) => o.id === row.consumableId))
consumableOptions.value = [{ id: row.consumableId, label: mapConsumableOptionLabel(row) }, ...consumableOptions.value]
consumableOptions.value = [
{ id: row.consumableId, label: mapConsumableOptionLabel(row) },
...consumableOptions.value
]
return
}
// consumableId 为空(后端不存储该字段)— 通过 consumableCode 反查主表真实 ID
@@ -264,7 +317,10 @@ export function useTaskStepEditor() {
const legacyId = `legacy:${code || name}`
row.consumableId = legacyId
if (!consumableOptions.value.some((o) => o.id === legacyId))
consumableOptions.value = [{ id: legacyId, label: mapConsumableOptionLabel(row) }, ...consumableOptions.value]
consumableOptions.value = [
{ id: legacyId, label: mapConsumableOptionLabel(row) },
...consumableOptions.value
]
}
function ensureAllStepConsumableOptions() {
for (const step of stepList.value) {
@@ -272,11 +328,19 @@ export function useTaskStepEditor() {
}
}
function onConsumableRowSelect(row: Record<string, any>, consumableId: string) {
if (!consumableId) { row.consumableId = ''; return }
if (String(consumableId).startsWith('legacy:')) { return }
if (!consumableId) {
row.consumableId = ''
return
}
if (String(consumableId).startsWith('legacy:')) {
return
}
// 去重:同一耗材不允许出现多次(排除当前行和 legacy 占位符)
const dup = (selectedStep.value?.consumables || []).some(
(c: Record<string, any>) => c !== row && c.consumableId === consumableId && !String(c.consumableId).startsWith('legacy:')
(c: Record<string, any>) =>
c !== row &&
c.consumableId === consumableId &&
!String(c.consumableId).startsWith('legacy:')
)
if (dup) {
ElMessage.warning('该耗材已添加,请勿重复选择')
@@ -284,11 +348,16 @@ export function useTaskStepEditor() {
return
}
const master = consumableMasterById.value[consumableId]
if (!master) { return }
if (!master) {
return
}
Object.assign(row, {
consumableId: master.id, consumableCode: master.consumableCode || '',
consumableName: master.consumableName || '', specModel: master.specModel || '',
standardUsage: master.standardUsage ?? '', unit: master.unit || '',
consumableId: master.id,
consumableCode: master.consumableCode || '',
consumableName: master.consumableName || '',
specModel: master.specModel || '',
standardUsage: master.standardUsage ?? '',
unit: master.unit || '',
remark: master.remark ?? master.description ?? ''
})
ensureItemUnitOption(row.unit)
@@ -303,17 +372,46 @@ export function useTaskStepEditor() {
async function searchCellProductOptions(query: string) {
cellProductOptionsLoading.value = true
try {
const res = await fetchCellProductList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchCellProductList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
const next = { ...cellProductMasterById.value }
for (const p of list) if (p?.id) next[p.id] = p
cellProductMasterById.value = next
cellProductOptions.value = list
.filter((p) => (p.productName || '').trim() || (p.productCode || '').trim())
.map((p) => ({ id: p.id, label: mapCellProductOptionLabel(p) }))
.map((p) => ({
id: p.id,
label: mapCellProductOptionLabel(p),
productCode: p.productCode || '',
productName: p.productName || '',
productTypeName: p.productTypeName || '',
passage: p.passage || '',
storageSlotName: p.storageSlotName || '',
productStatus: p.productStatus || ''
}))
for (const row of taskInputs.value) {
if (row.inputType === 'cell_product' && row.inputId && !cellProductOptions.value.some((o) => o.id === row.inputId)) {
cellProductOptions.value = [{ id: row.inputId, label: row.inputName || row.inputCode || '' }, ...cellProductOptions.value]
if (
row.inputType === 'cell_product' &&
row.inputId &&
!cellProductOptions.value.some((o) => o.id === row.inputId)
) {
cellProductOptions.value = [
{
id: row.inputId,
label: row.inputName || row.inputCode || '',
productCode: row.inputCode || '',
productName: row.inputName || '',
productTypeName: '',
passage: '',
storageSlotName: '',
productStatus: ''
},
...cellProductOptions.value
]
}
}
} catch {
@@ -323,7 +421,10 @@ export function useTaskStepEditor() {
}
}
function onCellProductInputSelect(row: Record<string, any>, productId: string) {
if (!productId) { row.inputId = ''; return }
if (!productId) {
row.inputId = ''
return
}
const master = cellProductMasterById.value[productId]
if (!master) return
row.inputId = master.id
@@ -335,7 +436,9 @@ export function useTaskStepEditor() {
for (const step of stepList.value) {
;(step.reagents || []).forEach((r: Record<string, any>) => ensureItemUnitOption(r.unit))
;(step.consumables || []).forEach((c: Record<string, any>) => ensureItemUnitOption(c.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) => ensureItemUnitOption(d.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) =>
ensureItemUnitOption(d.unit)
)
}
}
@@ -343,16 +446,21 @@ export function useTaskStepEditor() {
if (!selectedStepId.value) return null
return stepList.value.find((s) => s.id === selectedStepId.value) || null
})
const virtualInputStatus = computed(() => taskInputs.value.length > 0 ? 'filled' : 'empty')
const virtualOutputStatus = computed(() => taskOutputs.value.length > 0 ? 'filled' : 'empty')
const completedStepCount = computed(() => stepList.value.filter((s) => s.stepStatus === 'ywc').length)
const virtualInputStatus = computed(() => (taskInputs.value.length > 0 ? 'filled' : 'empty'))
const virtualOutputStatus = computed(() => (taskOutputs.value.length > 0 ? 'filled' : 'empty'))
const completedStepCount = computed(
() => stepList.value.filter((s) => s.stepStatus === 'ywc').length
)
const sopIdsArr = computed({
get() {
if (!selectedStep.value) return []
const raw = selectedStep.value.sopIds
if (!raw) return []
if (Array.isArray(raw)) return raw.filter(Boolean)
return String(raw).split(',').map((s: string) => s.trim()).filter(Boolean)
return String(raw)
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
},
set(val: string[]) {
if (!selectedStep.value) return
@@ -370,34 +478,97 @@ export function useTaskStepEditor() {
(p.processCode || '').toLowerCase().includes(keyword)
)
: group.processes
return { categoryId: group.categoryId, categoryName: group.categoryName, processes: filtered }
return {
categoryId: group.categoryId,
categoryName: group.categoryName,
processes: filtered
}
})
.filter((group) => group.processes.length > 0)
})
async function loadProductTypeAndSpecOptions() {
try {
const [typeRes, specRes] = await Promise.all([
fetchCellProductTypeList({ page: 1, pageSize: 100 }),
fetchCellProductSpecList({ page: 1, pageSize: 100 })
])
productTypeOptions.value = typeRes?.list || typeRes || []
specOptions.value = specRes?.list || specRes || []
} catch {
/* dropdown data failure is non-blocking */
}
}
async function loadStorageSlotOptions() {
try {
const facilityRes = await fetchStorageFacilityList()
const facilities = facilityRes?.list || facilityRes || []
const leafNodes = facilities.filter((n: any) => n.isLeaf === 1)
if (!leafNodes.length) {
storageSlotOptions.value = []
return
}
const results = await Promise.allSettled(
leafNodes.map(async (node: any) => {
const res: any = await fetchStorageSlotList({ facilityNodeId: node.id })
const slots = res?.list || res || []
const nodePath = buildTreePathLabelFromFlat(node.id, facilities, { nameKey: 'nodeName' })
return slots.map((slot: any) => {
const code = slot.slotCode || ''
const name = nodePath && code ? `${nodePath} / ${code}` : code || nodePath
return { id: slot.id, name }
})
})
)
const options: Array<{ id: string; name: string }> = []
results.forEach((r) => {
if (r.status === 'fulfilled') options.push(...r.value)
})
storageSlotOptions.value = options
} catch {
storageSlotOptions.value = []
}
}
async function loadConfigReferenceData() {
const [processes, categories, sops, outputTypes] = await Promise.all([
fetchAllProcesses(), fetchAllProcessCategories(),
fetchAllSopDocuments(), fetchAllOutputTypes()
fetchAllProcesses(),
fetchAllProcessCategories(),
fetchAllSopDocuments(),
fetchAllOutputTypes()
])
categoryOptions.value = categories || []
sopOptions.value = sops || []
outputTypeOptions.value = outputTypes || []
const catMap: Record<string, { categoryId: string; categoryName: string; processes: Record<string, any>[] }> = {}
const catMap: Record<
string,
{ categoryId: string; categoryName: string; processes: Record<string, any>[] }
> = {}
const uKey = '__uncategorized__'
catMap[uKey] = { categoryId: uKey, categoryName: '未分类', processes: [] }
;(categories || []).forEach((cat: Record<string, any>) => {
catMap[cat.id] = { categoryId: cat.id, categoryName: cat.categoryName || cat.name || '未命名分类', processes: [] }
catMap[cat.id] = {
categoryId: cat.id,
categoryName: cat.categoryName || cat.name || '未命名分类',
processes: []
}
})
;(processes || []).forEach((proc: Record<string, any>) => {
const catId = proc.categoryId || uKey
if (!catMap[catId]) catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
if (!catMap[catId])
catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
catMap[catId].processes.push(proc)
})
expandedCategories.value = Object.keys(catMap)
processLibrary.value = Object.values(catMap)
await Promise.all([searchItemUnitOptions(''), searchReagentOptions(''), searchConsumableOptions(''), searchCellProductOptions('')])
await Promise.all([
searchItemUnitOptions(''),
searchReagentOptions(''),
searchConsumableOptions(''),
searchCellProductOptions(''),
loadProductTypeAndSpecOptions(),
loadStorageSlotOptions()
])
}
async function loadTaskDetail(overrideId?: string) {
const id = overrideId || taskId.value
@@ -417,8 +588,15 @@ export function useTaskStepEditor() {
taskInputs.value = (detail.inputs || []).map((i: Record<string, any>) => ({ ...i }))
taskOutputs.value = (detail.outputs || []).map((o: Record<string, any>) => ({ ...o }))
for (const row of taskInputs.value) {
if (row.inputType === 'cell_product' && row.inputId && !cellProductOptions.value.some((o) => o.id === row.inputId)) {
cellProductOptions.value = [{ id: row.inputId, label: row.inputName || row.inputCode || '' }, ...cellProductOptions.value]
if (
row.inputType === 'cell_product' &&
row.inputId &&
!cellProductOptions.value.some((o) => o.id === row.inputId)
) {
cellProductOptions.value = [
{ id: row.inputId, label: row.inputName || row.inputCode || '' },
...cellProductOptions.value
]
}
}
}
@@ -440,7 +618,9 @@ export function useTaskStepEditor() {
await loadTaskDetail()
if (newStep?.id) selectedStepId.value = newStep.id
ElMessage.success('步骤添加成功')
} catch { /* ignore */ } finally {
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -449,8 +629,13 @@ export function useTaskStepEditor() {
if (!taskId.value) return
stepListLoading.value = true
try {
await fetchReorderTaskSteps({ taskId: taskId.value, stepIds: stepList.value.map((s) => s.id) })
} catch { /* ignore */ } finally {
await fetchReorderTaskSteps({
taskId: taskId.value,
stepIds: stepList.value.map((s) => s.id)
})
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -469,7 +654,9 @@ export function useTaskStepEditor() {
await loadTaskDetail()
if (ns?.id) selectedStepId.value = ns.id
ElMessage.success('步骤已复制')
} catch (e: any) { ElMessage.error(e?.message || '复制失败') } finally {
} catch (e: any) {
ElMessage.error(e?.message || '复制失败')
} finally {
stepListLoading.value = false
}
}
@@ -482,7 +669,9 @@ export function useTaskStepEditor() {
if (selectedStepId.value === step.id)
selectedStepId.value = stepList.value.length > 0 ? stepList.value[0].id : ''
ElMessage.success('步骤已删除')
} catch { /* ignore */ } finally {
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -498,7 +687,9 @@ export function useTaskStepEditor() {
estimated_duration: selectedStep.value.estimatedDuration,
sop_ids: selectedStep.value.sopIds
})
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function onSopIdsChange(val: string[]) {
if (!selectedStep.value) return
@@ -509,84 +700,135 @@ export function useTaskStepEditor() {
if (!selectedStep.value) return
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
selectedStep.value.reagents.push({
reagentId: '', reagentCode: '', reagentName: '', spec: '',
standardUsage: '', unit: '', isKeyMaterial: false, remark: ''
reagentId: '',
reagentCode: '',
reagentName: '',
spec: '',
standardUsage: '',
unit: '',
isKeyMaterial: false,
remark: ''
})
}
function removeReagentRow(i: number) { selectedStep.value?.reagents?.splice(i, 1) }
function removeReagentRow(i: number) {
selectedStep.value?.reagents?.splice(i, 1)
}
async function saveReagents() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.reagents || []).map((r: Record<string, any>) => ({
material_code: r.reagentCode, material_name: r.reagentName, spec: r.spec,
standard_usage: r.standardUsage, actual_usage: r.actualUsage || '',
material_code: r.reagentCode,
material_name: r.reagentName,
spec: r.spec,
standard_usage: r.standardUsage,
actual_usage: r.actualUsage || '',
unit: r.unit,
is_key_material: toApiFlag(r.isKeyMaterial), remark: r.remark || ''
is_key_material: toApiFlag(r.isKeyMaterial),
remark: r.remark || ''
}))
await fetchReplaceStepReagents(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addConsumableRow() {
if (!selectedStep.value) return
if (!selectedStep.value.consumables) selectedStep.value.consumables = []
selectedStep.value.consumables.push({
consumableId: '', consumableCode: '', consumableName: '', specModel: '',
standardUsage: '', unit: '', isRequired: false, remark: ''
consumableId: '',
consumableCode: '',
consumableName: '',
specModel: '',
standardUsage: '',
unit: '',
isRequired: false,
remark: ''
})
}
function removeConsumableRow(i: number) { selectedStep.value?.consumables?.splice(i, 1) }
function removeConsumableRow(i: number) {
selectedStep.value?.consumables?.splice(i, 1)
}
async function saveConsumables() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.consumables || []).map((c: Record<string, any>) => ({
material_code: c.consumableCode, material_name: c.consumableName,
material_code: c.consumableCode,
material_name: c.consumableName,
spec_model: c.specModel,
standard_usage: c.standardUsage, actual_usage: c.actualUsage || '',
standard_usage: c.standardUsage,
actual_usage: c.actualUsage || '',
unit: c.unit,
is_required: toApiFlag(c.isRequired), remark: c.remark || ''
is_required: toApiFlag(c.isRequired),
remark: c.remark || ''
}))
await fetchReplaceStepConsumables(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addOutputRow() {
if (!selectedStep.value) return
if (!selectedStep.value.outputs) selectedStep.value.outputs = []
selectedStep.value.outputs.push({ outputTypeId: '', outputName: '', outputBarcode: '', remark: '' })
selectedStep.value.outputs.push({
outputTypeId: '',
outputName: '',
outputBarcode: '',
remark: ''
})
}
function removeOutputRow(i: number) {
selectedStep.value?.outputs?.splice(i, 1)
}
function removeOutputRow(i: number) { selectedStep.value?.outputs?.splice(i, 1) }
async function saveOutputs() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.outputs || []).map((o: Record<string, any>) => ({
output_type_id: o.outputTypeId, output_name: o.outputName,
output_barcode: o.outputBarcode, remark: o.remark || ''
output_type_id: o.outputTypeId,
output_name: o.outputName,
output_barcode: o.outputBarcode,
remark: o.remark || ''
}))
await fetchReplaceStepOutputs(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addDataCollectionRow() {
if (!selectedStep.value) return
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
selectedStep.value.dataCollections.push({ collectionName: '', unit: '', orderNum: 0, remark: '' })
selectedStep.value.dataCollections.push({
collectionName: '',
unit: '',
orderNum: 0,
remark: ''
})
}
function removeDataCollectionRow(i: number) {
selectedStep.value?.dataCollections?.splice(i, 1)
}
function removeDataCollectionRow(i: number) { selectedStep.value?.dataCollections?.splice(i, 1) }
async function saveDataCollections() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.dataCollections || []).map((d: Record<string, any>) => ({
collection_name: d.collectionName, unit: d.unit,
collection_value: d.collectionValue || '', remark: d.remark || ''
collection_name: d.collectionName,
unit: d.unit,
collection_value: d.collectionValue || '',
remark: d.remark || ''
}))
await fetchReplaceStepDataCollection(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
const manualSaveLoading = ref(false)
const stepActionLoading = ref(false)
const taskActionLoading = ref(false)
async function handleManualSave() {
if (!selectedStep.value) return
if (!selectedStep.value || manualSaveLoading.value) return
manualSaveLoading.value = true
try {
await Promise.all([
@@ -605,34 +847,105 @@ export function useTaskStepEditor() {
}
async function handleStartStep(step: Record<string, any>) {
if (!taskId.value || !step.id) return
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'jxz' })
ElMessage.success('步骤已开始执行')
await loadTaskDetail()
if (!taskId.value || !step.id || stepActionLoading.value) return
stepActionLoading.value = true
try {
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'jxz' })
ElMessage.success('步骤已开始执行')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
stepActionLoading.value = false
}
}
async function handleCompleteStep(step: Record<string, any>) {
if (!taskId.value || !step.id) return
await ElMessageBox.confirm('确定完成该步骤吗?', '提示', { type: 'warning' })
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'ywc' })
ElMessage.success('步骤已完成')
await loadTaskDetail()
if (!taskId.value || !step.id || stepActionLoading.value) return
try {
await ElMessageBox.confirm('确定完成该步骤吗?', '提示', { type: 'warning' })
} catch {
return
}
stepActionLoading.value = true
try {
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'ywc' })
ElMessage.success('步骤已完成')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
stepActionLoading.value = false
}
}
async function handleStartTask() {
if (!taskDetail.value?.id) return
await ElMessageBox.confirm('确定开始执行该任务吗?', '提示', { type: 'warning' })
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'jxz' })
ElMessage.success('任务已开始执行')
await loadTaskDetail(taskDetail.value.id)
if (!taskDetail.value?.id || taskActionLoading.value) return
try {
await ElMessageBox.confirm('确定开始执行该任务吗?', '提示', { type: 'warning' })
} catch {
return
}
taskActionLoading.value = true
try {
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'jxz' })
ElMessage.success('任务已开始执行')
await loadTaskDetail(taskDetail.value.id)
} catch {
/* global error handling */
} finally {
taskActionLoading.value = false
}
}
function validateTaskIoRequired(): string | null {
if (!taskInputs.value.length) {
return '请先填写原材料(样本/细胞产品),至少添加一项并保存'
}
if (!taskOutputs.value.length) {
return '请先填写产出(细胞产品),至少添加一项并保存'
}
const invalidInput = taskInputs.value.find(
(row) =>
!row.inputType ||
(!(row.inputId || '').trim() &&
!(row.inputCode || '').trim() &&
!(row.inputName || '').trim())
)
if (invalidInput) {
return '原材料存在未填完整的行,请补全类型与编码/名称后保存'
}
const invalidOutput = taskOutputs.value.find(
(row) => !(row.outputName || '').trim() || !(row.outputBarcode || '').trim()
)
if (invalidOutput) {
return '产出存在未填完整的行,请补全输出名称与条码后保存'
}
return null
}
async function handleCompleteTask() {
if (!taskDetail.value?.id) return
await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
ElMessage.success('任务已完成')
await loadTaskDetail(taskDetail.value.id)
if (!taskDetail.value?.id || taskActionLoading.value) return
const ioErr = validateTaskIoRequired()
if (ioErr) {
await ElMessageBox.alert(ioErr, '无法完成任务', { type: 'warning' })
return
}
try {
await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
} catch {
return
}
taskActionLoading.value = true
try {
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
ElMessage.success('任务已完成')
await loadTaskDetail(taskDetail.value.id)
} catch {
/* global error handling */
} finally {
taskActionLoading.value = false
}
}
function addTaskInputRow() {
@@ -646,27 +959,68 @@ export function useTaskStepEditor() {
remark: ''
})
}
function removeTaskInputRow(i: number) { taskInputs.value.splice(i, 1) }
function removeTaskInputRow(i: number) {
taskInputs.value.splice(i, 1)
}
async function saveTaskInputs() {
if (!taskDetail.value?.id) return
if (!taskInputs.value.length) {
ElMessage.warning('请至少添加一项原材料(样本/细胞产品)')
return
}
const invalid = taskInputs.value.find(
(row) =>
!row.inputType ||
(!(row.inputId || '').trim() &&
!(row.inputCode || '').trim() &&
!(row.inputName || '').trim())
)
if (invalid) {
ElMessage.warning('请补全每行原材料的类型与编码/名称')
return
}
inputSaving.value = true
try {
await fetchSetTaskInputs(taskDetail.value.id, taskInputs.value)
ElMessage.success('输入已保存')
} catch { /* global error handling */ } finally { inputSaving.value = false }
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
inputSaving.value = false
}
}
function addTaskOutputRow() {
taskOutputs.value.push({ cellProductId: '', storageSlotId: '', outputName: '', outputBarcode: '', quantity: 0, unit: '', remark: '' })
taskOutputs.value.push({
cellProductId: '',
storageSlotId: '',
outputName: '',
outputBarcode: '',
quantity: 0,
unit: '',
remark: ''
})
}
function removeTaskOutputRow(i: number) {
taskOutputs.value.splice(i, 1)
}
function removeTaskOutputRow(i: number) { taskOutputs.value.splice(i, 1) }
async function saveTaskOutputs() {
if (!taskDetail.value?.id) return
if (!taskOutputs.value.length) {
ElMessage.warning('请至少添加一项产出(细胞产品)')
return
}
outputSaving.value = true
try {
await fetchSetTaskOutputs(taskDetail.value.id, taskOutputs.value)
ElMessage.success('输出已保存')
} catch { /* global error handling */ } finally { outputSaving.value = false }
ElMessage.success('产出已保存')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
outputSaving.value = false
}
}
function taskStatusTag(status: string) {
@@ -693,43 +1047,100 @@ export function useTaskStepEditor() {
activeSubTab.value = 'reagent'
processSearchKeyword.value = ''
configSuppressSave.value = true
stepList.value = []
stepListLoading.value = true
try {
await loadConfigReferenceData()
await loadTaskDetail()
} finally {
stepListLoading.value = false
await nextTick()
configSuppressSave.value = false
}
}
return {
taskId, stepList, selectedStepId, selectedStep, stepListLoading,
activeSubTab, processSearchKeyword, configSuppressSave,
mode, taskDetail, taskInputs, taskOutputs, inputSaving, outputSaving,
selectedVirtualStep, processLibraryExpanded,
virtualInputStatus, virtualOutputStatus, completedStepCount,
processLibrary, filteredProcessLibrary, expandedCategories,
categoryOptions, sopOptions, outputTypeOptions,
itemUnitOptions, itemUnitOptionsLoading,
reagentOptions, reagentOptionsLoading,
consumableOptions, consumableOptionsLoading,
cellProductOptions, cellProductOptionsLoading,
taskId,
stepList,
selectedStepId,
selectedStep,
stepListLoading,
activeSubTab,
processSearchKeyword,
configSuppressSave,
mode,
taskDetail,
taskInputs,
taskOutputs,
inputSaving,
outputSaving,
selectedVirtualStep,
processLibraryExpanded,
virtualInputStatus,
virtualOutputStatus,
completedStepCount,
processLibrary,
filteredProcessLibrary,
expandedCategories,
categoryOptions,
sopOptions,
outputTypeOptions,
itemUnitOptions,
itemUnitOptionsLoading,
reagentOptions,
reagentOptionsLoading,
consumableOptions,
consumableOptionsLoading,
cellProductOptions,
cellProductOptionsLoading,
productTypeOptions,
specOptions,
storageSlotOptions,
sopIdsArr,
onDragStart, onDropToCenter, onStepDragEnd, selectStep, selectVirtualStep,
handleCopyStep, handleDeleteStep,
handleStartStep, handleCompleteStep,
handleStartTask, handleCompleteTask,
addTaskInputRow, removeTaskInputRow, saveTaskInputs,
addTaskOutputRow, removeTaskOutputRow, saveTaskOutputs,
taskStatusTag, stepStatusTag,
saveStepBasic, onSopIdsChange,
searchItemUnitOptions, searchReagentOptions, searchConsumableOptions, searchCellProductOptions,
onReagentRowSelect, onConsumableRowSelect, onCellProductInputSelect,
addReagentRow, removeReagentRow, saveReagents,
addConsumableRow, removeConsumableRow, saveConsumables,
addOutputRow, removeOutputRow, saveOutputs,
addDataCollectionRow, removeDataCollectionRow, saveDataCollections,
manualSaveLoading, handleManualSave,
onDragStart,
onDropToCenter,
onStepDragEnd,
selectStep,
selectVirtualStep,
handleCopyStep,
handleDeleteStep,
handleStartStep,
handleCompleteStep,
handleStartTask,
handleCompleteTask,
addTaskInputRow,
removeTaskInputRow,
saveTaskInputs,
addTaskOutputRow,
removeTaskOutputRow,
saveTaskOutputs,
taskStatusTag,
stepStatusTag,
saveStepBasic,
onSopIdsChange,
searchItemUnitOptions,
searchReagentOptions,
searchConsumableOptions,
searchCellProductOptions,
onReagentRowSelect,
onConsumableRowSelect,
onCellProductInputSelect,
addReagentRow,
removeReagentRow,
saveReagents,
addConsumableRow,
removeConsumableRow,
saveConsumables,
addOutputRow,
removeOutputRow,
saveOutputs,
addDataCollectionRow,
removeDataCollectionRow,
saveDataCollections,
manualSaveLoading,
stepActionLoading,
taskActionLoading,
handleManualSave,
initEditor
}
}
@@ -138,9 +138,14 @@
<SystemViewDialog
v-model:visible="viewVisible"
title="工单详情"
width="1120px"
:grid-columns="4"
compact
:loading="viewLoading"
:icon="Document"
:meta="detail?.workOrderNo || ''"
:sections="viewSections"
modal-class="qc-work-order-view-dialog-mask"
>
<template #reception-extra>
<div class="view-reception-attachments">
@@ -156,6 +161,15 @@
</div>
</div>
</template>
<template v-for="task in viewTasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
<div :key="task.id" class="qc-work-order-task-attachments">
<ProtocolAttachmentReadonly
:files="task.attachmentFiles || []"
@preview="handleTaskAttachmentPreview"
@download="handleTaskAttachmentDownload"
/>
</div>
</template>
</SystemViewDialog>
</section>
</div>
@@ -174,15 +188,19 @@
type QcWorkOrder,
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import type { QcTask } from '@/api/qc-task'
import {
workOrderStatusLabel,
receptionSourceLabel,
acceptConclusionLabel,
transportTypeLabel
transportTypeLabel,
taskStatusLabel,
qcTaskConclusionLabel
} 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 type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'QualityInspectionQcWorkOrder' })
@@ -217,9 +235,20 @@
})
const viewVisible = ref(false)
const viewLoading = ref(false)
const detail = ref<QcWorkOrderDetail | null>(null)
type QcWorkOrderTaskView = QcTask & { results?: QcTaskResultItem[] }
const viewTasks = ref<QcWorkOrderTaskView[]>([])
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewTasksWithAttachments = computed(() =>
viewTasks.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
)
function taskAttachmentSlotName(taskId: string) {
return `task-att-${taskId}`
}
const viewReceptionAttachmentFiles = computed(() => {
const r = detail.value?.reception
if (!r) return []
@@ -255,6 +284,125 @@
downloadAttachmentFile(file, { fullFileUrl: receptionAttachmentFullUrl })
}
function taskAttachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleTaskAttachmentPreview(file: Record<string, unknown>) {
const url = taskAttachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleTaskAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: taskAttachmentFullUrl })
}
function conclusionTagType(conclusion?: string) {
if (conclusion === 'tg') return 'success' as const
if (conclusion === 'btg') return 'danger' as const
return 'info' as const
}
function taskStatusTagType(status?: string) {
if (status === 'ywc') return 'success' as const
if (status === 'jxz') return 'warning' as const
return 'info' as const
}
function formatConfiguredLabItems(d: QcWorkOrderDetail) {
return (
d.labItems?.map((li) => `${li.itemName || ''}(${li.itemCode || ''})`).join('、') || '无'
)
}
function formatConfiguredThirdItems(d: QcWorkOrderDetail) {
return (
d.thirdPartyItems
?.map((li) => `${li.companyName || ''} — ${li.itemName || ''}(¥${li.feeAmount ?? '—'})`)
.join('、') || '无'
)
}
function buildTaskTypeSections(
tasks: QcWorkOrderTaskView[],
taskType: 'sysz' | 'dsfjc',
sectionTitle: string
): ViewSection[] {
const filtered = tasks.filter((t) => t.taskType === taskType)
if (!filtered.length) return []
return filtered.map((task, index) => {
const title =
filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
const fields: ViewField[] = [
{ label: '检测项目', value: task.itemName || '—' },
{ label: '项目编码', value: task.itemCode || '—' },
{
label: '任务状态',
value: taskStatusLabel(task.taskStatus || ''),
tagType: taskStatusTagType(task.taskStatus)
},
{
label: '检测结论',
value: task.conclusionLabel || qcTaskConclusionLabel(task.conclusion || '') || '—',
tagType: conclusionTagType(task.conclusion)
},
{ label: '任务号', value: task.taskNo || '—' },
{ label: '完成时间', value: task.completedTime || '—' }
]
if (taskType === 'dsfjc') {
fields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
if (task.feeAmount != null) {
fields.push({ label: '费用(元)', value: String(task.feeAmount) })
}
}
if (task.results?.length) {
for (const item of task.results) {
fields.push({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
})
}
} else if (taskType === 'sysz') {
fields.push({ label: '检测参数', value: '—' })
}
fields.push({ label: '备注', value: task.remark || '—', span: true })
const section: ViewSection = { title, fields }
if (task.attachmentFiles?.length) {
section.slot = taskAttachmentSlotName(task.id)
section.slotTitle = taskType === 'dsfjc' ? '检测材料' : '检测附件'
section.slotTitleStyle = 'field'
} else if (taskType === 'dsfjc') {
fields.push({ label: '检测材料', value: '—' })
} else {
fields.push({ label: '检测附件', value: '—' })
}
return section
})
}
function buildConfiguredOnlySection(
title: string,
configuredLabel: string,
configuredValue: string,
attachmentLabel: string
): ViewSection {
return {
title,
fields: [
{ label: configuredLabel, value: configuredValue, span: true },
{ label: '检测结论', value: '—' },
{ label: '检测参数', value: '—' },
{ label: '备注', value: '—' },
{ label: attachmentLabel, value: '—' }
]
}
}
function formatDetectMethods(row: QcWorkOrder) {
const parts: string[] = []
if (row.hasLab) parts.push('实验室')
@@ -342,28 +490,23 @@
{ label: '备注', value: r.remark || '—', span: true }
]
},
{
title: '实验室项目',
fields: [
{
label: '检测项目',
value: d.labItems?.map((li) => li.itemName + '(' + li.itemCode + ')').join('、') || '无',
span: true
}
]
},
{
title: '第三方项目',
fields: [
{
label: '检测项目',
value:
d.thirdPartyItems?.map((li) => li.companyName + ' — ' + li.itemName + '(¥' + li.feeAmount + ')').join('、') ||
'无',
span: true
}
]
}
...(viewTasks.value.some((t) => t.taskType === 'sysz')
? buildTaskTypeSections(viewTasks.value, 'sysz', '实验室项目')
: d.hasLab
? [buildConfiguredOnlySection('实验室项目', '已配置项目', formatConfiguredLabItems(d), '检测附件')]
: []),
...(viewTasks.value.some((t) => t.taskType === 'dsfjc')
? buildTaskTypeSections(viewTasks.value, 'dsfjc', '第三方项目')
: d.hasThirdParty
? [
buildConfiguredOnlySection(
'第三方项目',
'已配置项目',
formatConfiguredThirdItems(d),
'检测材料'
)
]
: [])
]
})
@@ -433,8 +576,30 @@
}
async function handleView(row: QcWorkOrder) {
detail.value = await fetchQcWorkOrderDetail(row.id)
viewVisible.value = true
viewLoading.value = true
viewTasks.value = []
try {
const d = await fetchQcWorkOrderDetail(row.id)
detail.value = d
const rawTasks = (d.tasks || []) as QcWorkOrderTaskView[]
const enriched = await Promise.all(
rawTasks.map(async (task) => {
if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
return { ...task, results: [] as QcTaskResultItem[] }
}
try {
const results = await fetchQcTaskResults(task.id)
return { ...task, results: results || [] }
} catch {
return { ...task, results: [] as QcTaskResultItem[] }
}
})
)
viewTasks.value = enriched
} finally {
viewLoading.value = false
}
}
onMounted(() => loadData())
@@ -475,6 +640,10 @@
color: var(--mint-text-muted, #64748b);
}
.qc-work-order-task-attachments {
margin-top: 0;
}
.view-reception-attachments {
margin-top: 4px;