This commit is contained in:
zlwu committed 2026-05-21 18:59:46 +08:00
1 parent a905d80623
commit 7541a167c0
33 files changed
+2715 -850

No files matched your search

+1 -1
View File
@@ -1 +1 @@
68778
81932
+34
View File
@@ -43,6 +43,7 @@ from app.schemas.production_task import (
TaskStepDataCollectionItem,
TaskInputItem,
TaskOutputItem,
TaskStepReorderBody,
)
from app.schemas.cell_product import CellProductCreate, CellProductUpdate
@@ -1435,6 +1436,39 @@ async def delete_production_task_step(
return error_response(400, str(e))
@router.put("/production-tasks/step/reorder")
async def reorder_production_task_steps(
body: TaskStepReorderBody,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
production_task_svc.reorder_task_steps(db, body.task_id, body.step_ids)
return success_response(None, message="排序成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/production-tasks/{task_id}/steps/{step_id}/copy")
async def copy_production_task_step(
task_id: str,
step_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
step_data = production_task_svc.copy_task_step(
db,
step_id=step_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(step_data, message="复制成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/production-tasks/{task_id}/steps/{step_id}/reagents")
async def replace_task_step_reagents(
task_id: str,
+1 -1
View File
@@ -304,7 +304,7 @@ def update_cell_product(
if hasattr(data, "viability") and data.viability is not None:
obj.viability = data.viability
if data.product_status is not None and data.product_status != obj.product_status:
allowed = {"frozen": {"sold", "used"}}
allowed = {"frozen": {"sold", "used", "voided"}}
from_status = obj.product_status
to_status = data.product_status
if from_status not in allowed or to_status not in allowed[from_status]:
+132 -1
View File
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import (
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepConsumable,
BizProcessRouteStepDataCollection,
@@ -348,7 +349,19 @@ def list_production_tasks(
.limit(page_size)
.all()
)
return [_task_to_dict(x) for x in rows], int(total)
items = [_task_to_dict(x) for x in rows]
# 批量补充路线名称
route_ids = [x.route_id for x in rows if x.route_id]
if route_ids:
route_map = {
r.id: r.route_name
for r in db.query(BizProcessRoute.id, BizProcessRoute.route_name)
.filter(BizProcessRoute.id.in_(route_ids))
.all()
}
for item, row in zip(items, rows):
item["routeName"] = route_map.get(row.route_id, "")
return items, int(total)
def get_production_task_detail(db: Session, task_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
@@ -690,6 +703,124 @@ def set_task_outputs(
db.commit()
def reorder_task_steps(db: Session, task_id: str, step_ids: List[str]) -> None:
now = datetime.now()
for idx, sid in enumerate(step_ids):
step = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == sid, BizProductionTaskStep.task_id == task_id,
BizProductionTaskStep.del_flag == 0)
).first()
if step:
step.step_order = idx + 1
step.update_time = now
db.add(step)
db.commit()
def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
user_id: Optional[str] = None) -> Dict[str, Any]:
"""复制生产任务步骤,插入到原步骤之后,名称追加(复制)"""
src = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
if not src:
raise ValueError("步骤不存在")
now = datetime.now()
new_order = src.step_order + 1
# 后续步骤 step_order +1
later_steps = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.task_id == src.task_id,
BizProductionTaskStep.del_flag == 0,
BizProductionTaskStep.step_order > src.step_order,
)
).order_by(BizProductionTaskStep.step_order.asc()).all()
for ls in later_steps:
ls.step_order += 1
ls.update_time = now
db.add(ls)
# 创建新步骤
new_name = (src.process_name or "").strip()
if not new_name.endswith("(复制)"):
new_name += "(复制)"
new_step = BizProductionTaskStep(
id=generate_uuid(), task_id=src.task_id,
step_order=new_order,
process_code=src.process_code, category_id=src.category_id,
process_name=new_name, description=src.description,
equipment_name=src.equipment_name, estimated_duration=src.estimated_duration,
actual_duration=src.actual_duration, sop_ids=src.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=src.remark, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(new_step)
db.flush()
# 复制子表:试剂(从生产任务步骤试剂表复制)
for r in db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
).all():
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
reagent_code=r.reagent_code, reagent_name=r.reagent_name,
spec=r.spec, standard_usage=r.standard_usage,
actual_usage=r.actual_usage,
unit=r.unit, is_key_material=r.is_key_material,
remark=r.remark,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:耗材
for c in db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
).all():
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
consumable_code=c.consumable_code, consumable_name=c.consumable_name,
spec_model=c.spec_model, standard_usage=c.standard_usage,
actual_usage=c.actual_usage,
unit=c.unit, is_required=c.is_required,
remark=c.remark,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:产出
for o in db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
).all():
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:数据采集
for d in db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
).all():
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
collection_value=d.collection_value,
remark=d.remark,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
return _step_to_dict(new_step)
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
+1 -1
View File
@@ -2135,7 +2135,7 @@ class BizCellProduct(Base):
freeze_date = Column(DateTime, nullable=True, comment="冻存日期")
expected_cell_count = Column(Numeric(18, 4), nullable=True, comment="预计细胞数量(单位:亿)")
viability = Column(Numeric(5, 2), nullable=True, comment="活性(百分比,0~100)")
product_status = Column(String(16), nullable=True, default="frozen", comment="产品状态 frozen已冻存/sold已售卖/used已使用")
product_status = Column(String(16), nullable=True, default="frozen", comment="产品状态 frozen已冻存/sold已售卖/used已使用/voided已作废")
source_type = Column(String(32), nullable=True, default="production", comment="来源类型 production生产 qc质检")
production_task_id = Column(String(64), nullable=True, comment="生产任务ID")
quantity = Column(Numeric(18, 4), nullable=True, comment="数量")
+2 -2
View File
@@ -118,8 +118,8 @@ class CellProductUpdate(BaseModel):
@field_validator("product_status")
@classmethod
def validate_status(cls, v):
if v is not None and v not in ("frozen", "sold", "used"):
raise ValueError("状态必须为 frozen/sold/used")
if v is not None and v not in ("frozen", "sold", "used", "voided"):
raise ValueError("状态必须为 frozen/sold/used/voided")
return v
+8 -2
View File
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, List
from datetime import datetime
@@ -6,7 +6,7 @@ from datetime import datetime
class ProductionTaskCreate(BaseModel):
work_order_id: str
task_name: Optional[str] = None
route_id: str
route_id: Optional[str] = None
description: Optional[str] = None
remark: Optional[str] = None
@@ -91,3 +91,9 @@ class TaskOutputItem(BaseModel):
unit: Optional[str] = None
remark: Optional[str] = None
attachments: Optional[str] = None
class TaskStepReorderBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
task_id: str = Field(..., alias="taskId")
step_ids: List[str] = Field(..., alias="stepIds")
@@ -36,6 +36,20 @@
- **WHEN** 增加新表或字段
- **THEN** 仓库 SHALL 包含可复现的 schema 片段或迁移,且与 OpenSpec `database-conventions` 中的公共字段规则一致
### Requirement: 单文件行数上限
前端(`web-admin`)与后端(`backend`)的单个源码文件 SHALL 不超过 500 行。当文件因功能增长接近或超过此限制时,SHALL 将其拆分为多个职责单一的小文件(如:前端提取子组件、composable 或工具函数;后端按领域拆分 service、crud 模块或辅助函数)。配置文件、类型声明文件、SQL 脚本与自动生成文件不在此限制范围内。
#### Scenario: 前端页面文件增长
- **WHEN** 一个 Vue 组件文件超过 500 行
- **THEN** SHALL 将弹窗、复杂表格、可复用面板等拆分为独立子组件或 composable 函数,确保主入口文件在 500 行以内
#### Scenario: 后端模块文件增长
- **WHEN** 一个 Python 模块文件(api route、service、crud 等)超过 500 行
- **THEN** SHALL 按领域或功能边界拆分为多个子模块(如 `crud/production_task.py` → `crud/production_task.py` + `crud/production_task_step.py`),确保单个文件在 500 行以内
### Requirement: 提交说明可读性
提交信息 SHALL 使用完整句子说明「改了什么、为何」,并符合仓库 Conventional Commits / commitlint 类型约束。
File diff suppressed because it is too large. Load diff
+232 -27
View File
@@ -8324,18 +8324,53 @@
"source_location": "L1"
},
{
"id": "production_work_order_index_handleview",
"label": "handleView()",
"id": "production_work_order_index_layoutworkordertable",
"label": "layoutWorkOrderTable()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L319"
"source_location": "L280"
},
{
"id": "production_work_order_index_handlecomplete",
"label": "handleComplete()",
"id": "production_work_order_index_loaddata",
"label": "loadData()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L325"
"source_location": "L285"
},
{
"id": "production_work_order_index_handlesearch",
"label": "handleSearch()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L302"
},
{
"id": "production_work_order_index_handlereset",
"label": "handleReset()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L307"
},
{
"id": "production_work_order_index_handlereceptionattachmentpreview",
"label": "handleReceptionAttachmentPreview()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L345"
},
{
"id": "production_work_order_index_handlereceptionattachmentdownload",
"label": "handleReceptionAttachmentDownload()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L350"
},
{
"id": "production_work_order_index_formatmetric",
"label": "formatMetric()",
"file_type": "code",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L354"
},
{
"id": "views_production_consumable_pack_index_vue",
@@ -23834,20 +23869,200 @@
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handleview",
"relation": "contains",
"target": "element_plus",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L319",
"source_location": "L227",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handlecomplete",
"target": "element_plus",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L228",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "icons_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L229",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_work_order",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L230",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_task",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L236",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L239",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_qc_labels",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L243",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "views_cell_storage_protocol_protocolattachmentreadonly_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L248",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "components_business_system_view_dialog_index_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L249",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "components_business_system_view_dialog_index_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L250",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "element_plus",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L251",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L252",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_layoutworkordertable",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L325",
"source_location": "L280",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_loaddata",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L285",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handlesearch",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L302",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handlereset",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L307",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handlereceptionattachmentpreview",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L345",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_handlereceptionattachmentdownload",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L350",
"weight": 1.0
},
{
"source": "views_production_production_work_order_index_vue",
"target": "production_work_order_index_formatmetric",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L354",
"weight": 1.0
},
{
"source": "production_work_order_index_loaddata",
"target": "production_work_order_index_layoutworkordertable",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L298",
"weight": 1.0
},
{
"source": "production_work_order_index_handlesearch",
"target": "production_work_order_index_loaddata",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L304",
"weight": 1.0
},
{
"source": "production_work_order_index_handlereset",
"target": "production_work_order_index_loaddata",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L311",
"weight": 1.0
},
{
@@ -30159,33 +30374,23 @@
"weight": 1.0
},
{
"source": "production_work_order_index_handleview",
"target": "api_production_work_order_fetchproductionworkorderdetail",
"source": "production_work_order_index_loaddata",
"target": "api_production_work_order_fetchproductionworkorderlist",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L320",
"source_location": "L288",
"weight": 1.0
},
{
"source": "production_work_order_index_handlecomplete",
"target": "api_production_work_order_fetchcompleteproductionworkorder",
"source": "production_work_order_index_handlereceptionattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L327",
"weight": 1.0
},
{
"source": "production_work_order_index_handlecomplete",
"target": "collection_region_index_loaddata",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/production/production-work-order/index.vue",
"source_location": "L329",
"source_location": "L351",
"weight": 1.0
},
{
+71 -64
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-05-21)
## Corpus Check
- Large corpus: 577 files · ~298,996 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 577 files · ~299,569 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 1405 nodes · 1759 edges · 42 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 387 edges (avg confidence: 0.8)
- 1410 nodes · 1768 edges · 43 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 386 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -50,7 +50,8 @@
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 45|Community 45]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 153 edges
@@ -65,8 +66,6 @@
10. `StorageConfig` - 10 edges
## Surprising Connections (you probably didn't know these)
- `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
- `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()` [INFERRED]
@@ -75,72 +74,74 @@
api/leave.ts → views/production/process-route/index.vue
- `fetchLeaveDetail()` --calls--> `get()` [INFERRED]
api/leave.ts → views/production/process-route/index.vue
- `fetchSampleReceptionCheckItemList()` --calls--> `get()` [INFERRED]
api/sample-reception-check-item.ts → views/production/process-route/index.vue
## Communities
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (83): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+75 more)
Nodes (74): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+66 more)
### Community 1 - "Community 1"
Cohesion: 0.03
Nodes (40): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), IframeRouteManager, MenuProcessor (+32 more)
Nodes (57): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+49 more)
### Community 2 - "Community 2"
Cohesion: 0.03
Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
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 (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
### Community 5 - "Community 5"
Cohesion: 0.06
Nodes (24): FireworkSystem, reload(), formatUserLabel(), onPersonSelectChange(), subscribeTokenRefresh(), handleMenuJump(), openExternalLink(), set() (+16 more)
Cohesion: 0.05
Nodes (39): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+31 more)
### Community 6 - "Community 6"
Cohesion: 0.06
Nodes (11): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), TableCache (+3 more)
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (28): fetchCreateCustomer(), fetchCreateMenu(), fetchCustomerList(), fetchDeleteCustomer(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateCustomer(), fetchUpdateMenu() (+20 more)
### Community 8 - "Community 8"
Cohesion: 0.09
Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
### Community 7 - "Community 7"
### Community 9 - "Community 9"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 8 - "Community 8"
### Community 10 - "Community 10"
Cohesion: 0.07
Nodes (15): fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName() (+7 more)
### Community 11 - "Community 11"
Cohesion: 0.13
Nodes (4): handleReset(), handleSearch(), WebSocketClient, emit()
### Community 9 - "Community 9"
Cohesion: 0.08
Nodes (19): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+11 more)
### Community 10 - "Community 10"
Cohesion: 0.08
Nodes (23): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+15 more)
### Community 11 - "Community 11"
Cohesion: 0.07
Nodes (13): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+5 more)
### Community 12 - "Community 12"
Cohesion: 0.09
Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
Cohesion: 0.06
Nodes (14): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+6 more)
### Community 13 - "Community 13"
Cohesion: 0.09
Nodes (3): collectIds(), RoutePermissionValidator, TableCache
Cohesion: 0.1
Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
### Community 14 - "Community 14"
Cohesion: 0.1
Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
### Community 15 - "Community 15"
Cohesion: 0.11
Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
Cohesion: 0.1
Nodes (18): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+10 more)
### Community 16 - "Community 16"
Cohesion: 0.12
@@ -151,29 +152,29 @@ Cohesion: 0.14
Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
### Community 18 - "Community 18"
Cohesion: 0.1
Nodes (7): handleTouchEnd(), setTransition(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme()
Cohesion: 0.19
Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
### Community 19 - "Community 19"
Cohesion: 0.12
Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
### Community 20 - "Community 20"
Cohesion: 0.11
Nodes (8): fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchRefundConfirmedList(), fetchRefundConfirmList()
### 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.2
Nodes (2): ComponentLoader, RouteTransformer
### Community 22 - "Community 22"
### Community 23 - "Community 23"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 23 - "Community 23"
Cohesion: 0.17
Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
### Community 24 - "Community 24"
Cohesion: 0.14
Nodes (2): fetchProductionTaskDetail(), fetchProductionTaskList()
@@ -216,38 +217,42 @@ Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsAc
### Community 36 - "Community 36"
Cohesion: 0.33
Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
### Community 37 - "Community 37"
Cohesion: 0.33
Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
### Community 38 - "Community 38"
Cohesion: 0.33
Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
### Community 39 - "Community 39"
Cohesion: 0.33
Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
### Community 40 - "Community 40"
Cohesion: 0.53
Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
### Community 40 - "Community 40"
### Community 41 - "Community 41"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 41 - "Community 41"
### Community 42 - "Community 42"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 42 - "Community 42"
### Community 43 - "Community 43"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 44 - "Community 44"
### Community 45 - "Community 45"
Cohesion: 1.0
Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
## Knowledge Gaps
- **Thin community `Community 21`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
- **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`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 24`** (14 nodes): `fetchAddTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
@@ -261,29 +266,31 @@ Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 35`** (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): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
- **Thin community `Community 36`** (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): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
- **Thin community `Community 37`** (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 38`** (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 41`** (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 42`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
- **Thin community `Community 42`** (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 44`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
- **Thin community `Community 43`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 45`** (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 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 17`, `Community 19`, `Community 23`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 38`?**
_High betweenness centrality (0.265) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 5` to `Community 0`, `Community 1`, `Community 8`, `Community 9`, `Community 12`, `Community 13`, `Community 14`?**
_High betweenness centrality (0.074) - this node is a cross-community bridge._
- **Why does `fetchGetLatestUpgradeLog()` connect `Community 12` to `Community 0`?**
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`?**
_High betweenness centrality (0.259) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 3` to `Community 0`, `Community 2`, `Community 6`, `Community 11`, `Community 14`, `Community 18`?**
_High betweenness centrality (0.085) - this node is a cross-community bridge._
- **Why does `fetchGetLatestUpgradeLog()` connect `Community 18` to `Community 0`?**
_High betweenness centrality (0.043) - this node is a cross-community bridge._
- **Are the 152 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
_`get()` has 152 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">1405 nodes &middot; 1759 edges &middot; 237 communities</div>
<div id="stats">1410 nodes &middot; 1768 edges &middot; 238 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.6, "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": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 75, "community_name": "Community 75", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 76, "community_name": "Community 76", "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": 91}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 87}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 53}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 51}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 42}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 39}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 36}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 34}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 34}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 33}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 32}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 31}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 29}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 27}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 24}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 22}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 20}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 20}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 19}, {"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": 16}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 16}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 14}, {"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": 7}, {"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": 2}, {"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": "#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.6, "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": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 76, "community_name": "Community 76", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 77, "community_name": "Community 77", "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": 215}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 88}, {"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": 50}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 44}, {"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": 35}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 34}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 33}, {"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": 26}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 22}, {"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": 16}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 14}, {"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": 7}, {"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": 6}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 5}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 4}, {"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
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+5 -5
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": 1779340992.2511122,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779338103.9480238,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779342029.1834326,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779342021.1408348,
"/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,
@@ -269,12 +269,12 @@
"/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": 1779340490.9725103,
"/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": 1779337462.617501,
"/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": 1779269660.0717294,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779341706.1749396,
"/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": 1779341270.8272316,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779342203.7289343,
"/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,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 176 KiB

+18
View File
@@ -72,3 +72,21 @@ export function fetchSetTaskInputs(taskId: string, data: Record<string, any>) {
export function fetchSetTaskOutputs(taskId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/production-tasks/${taskId}/outputs`, params: data })
}
export function fetchReorderTaskSteps(data: { taskId: string; stepIds: string[] }) {
return request.put({ url: '/web/admin/production-tasks/step/reorder', params: data })
}
export function fetchCopyTaskStep(data: { taskId: string; stepId: string }) {
return request.post({
url: `/web/admin/production-tasks/${data.taskId}/steps/${data.stepId}/copy`,
params: data
})
}
export function fetchReplaceStepOutputs(taskId: string, stepId: string, data: Record<string, any>) {
return request.put({
url: `/web/admin/production-tasks/${taskId}/steps/${stepId}/outputs`,
params: data
})
}
@@ -91,13 +91,7 @@
opacity: 1
}"
>
<template v-if="AppConfig.systemInfo.name === '中科兴皖'">
<span class="system-name-main">中科</span>
<span class="system-name-accent">兴皖</span>
</template>
<template v-else>
{{ AppConfig.systemInfo.name }}
</template>
{{ AppConfig.systemInfo.name }}
</p>
</div>
<ElScrollbar :style="scrollbarStyle">
@@ -170,24 +170,21 @@
top: 0;
bottom: 0;
left: 58px;
right: 12px;
box-sizing: border-box;
display: flex;
align-items: center;
margin-left: 10px;
font-size: 16px;
font-size: 15px;
font-weight: 700;
line-height: 1.35;
color: #111827 !important;
white-space: normal;
&.is-dual-menu-name {
left: 25px;
margin: auto;
}
.system-name-main {
color: #111827;
}
.system-name-accent {
color: #059669;
}
}
&::after {
+1 -1
View File
@@ -38,7 +38,7 @@ import { headerBarConfig } from './modules/headerBar'
const appConfig: SystemConfig = {
// 系统信息
systemInfo: {
name: '中科兴皖' // 系统名称
name: '生产运营一体化门户' // 系统名称
},
// 系统主题
systemThemeStyles: {
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L1"}, {"id": "production_work_order_index_loaddata", "label": "loadData()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L271"}, {"id": "production_work_order_index_handlesearch", "label": "handleSearch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L287"}, {"id": "production_work_order_index_handlereset", "label": "handleReset()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L292"}, {"id": "production_work_order_index_handlereceptionattachmentpreview", "label": "handleReceptionAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L330"}, {"id": "production_work_order_index_handlereceptionattachmentdownload", "label": "handleReceptionAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L335"}, {"id": "production_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L339"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L220", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L221", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_work_order", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L222", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "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/index.vue", "source_location": "L228", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_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/production-work-order/index.vue", "source_location": "L231", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_qc_labels", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L235", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_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/production-work-order/index.vue", "source_location": "L240", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_system_view_dialog_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L241", "weight": 1.0}, {"source": "useLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_task_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/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_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_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_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L1"}, {"id": "production_work_order_index_layoutworkordertable", "label": "layoutWorkOrderTable()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L280"}, {"id": "production_work_order_index_loaddata", "label": "loadData()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L285"}, {"id": "production_work_order_index_handlesearch", "label": "handleSearch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L302"}, {"id": "production_work_order_index_handlereset", "label": "handleReset()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L307"}, {"id": "production_work_order_index_handlereceptionattachmentpreview", "label": "handleReceptionAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L345"}, {"id": "production_work_order_index_handlereceptionattachmentdownload", "label": "handleReceptionAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L350"}, {"id": "production_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L354"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L227", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L228", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L229", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_work_order", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L230", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "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/index.vue", "source_location": "L236", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_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/production-work-order/index.vue", "source_location": "L239", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_qc_labels", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L243", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolattachmentreadonly_vue", "relation": "imports_from", "confidence": "EXTRACTEDLine 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": []}
+297
View File
@@ -0,0 +1,297 @@
/**
* 路线配置图 / 任务步骤编辑器 — 三栏布局共享样式
* 被 process-route 和 production-work-order 的弹窗组件引用
*/
.route-config-body {
display: flex;
gap: 16px;
height: calc(90vh - 120px);
min-height: 500px;
}
/* 左侧面板:工艺库 */
.route-config-left {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.route-config-left-header {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
h3 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
}
.route-config-search {
width: 100%;
}
.route-config-collapse {
flex: 1;
overflow-y: auto;
border: none;
:deep(.el-collapse-item__header) {
font-size: 13px;
font-weight: 600;
color: #374151;
background: #ecfdf5;
padding: 0 12px;
height: 36px;
line-height: 36px;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
:deep(.el-collapse-item__content) {
padding: 4px 8px 8px;
}
}
.route-config-cat-bar {
display: inline-block;
width: 3px;
height: 14px;
background: #10b981;
border-radius: 2px;
margin-right: 8px;
vertical-align: middle;
}
.route-config-process-card {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 8px 8px 10px;
margin-bottom: 4px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
cursor: grab;
transition: all 0.15s ease;
&:hover {
border-color: #10b981;
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.12);
}
&:active {
cursor: grabbing;
}
}
.route-config-process-name {
font-size: 13px;
font-weight: 500;
color: #1f2937;
margin-bottom: 2px;
}
.route-config-process-seq {
color: #1f2937;
font-size: 13px;
font-weight: 500;
margin-right: 2px;
}
.route-config-process-code {
font-size: 11px;
color: #9ca3af;
}
.route-config-process-info {
flex: 1;
min-width: 0;
}
.route-config-process-drag {
flex-shrink: 0;
color: #d1d5db;
font-size: 14px;
}
/* 中间面板:步骤列表 */
.route-config-center {
width: 640px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.route-config-center-header {
padding: 12px 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: #1f2937;
}
}
.route-config-step-count {
font-size: 12px;
color: #9ca3af;
}
.route-config-center-dropzone {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.route-config-center-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #9ca3af;
p {
margin-top: 8px;
font-size: 13px;
}
}
.route-config-step-card {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
margin-bottom: 6px;
background: #fff;
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: #6ee7b7;
}
&.is-active {
border-color: #10b981;
background: #ecfdf5;
}
}
.route-config-step-handle {
cursor: grab;
color: #9ca3af;
flex-shrink: 0;
font-size: 16px;
&:active {
cursor: grabbing;
}
}
.route-config-step-order {
width: 24px;
height: 24px;
border-radius: 50%;
background: #10b981;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.route-config-step-info {
flex: 1;
min-width: 0;
}
.route-config-step-name {
font-size: 13px;
font-weight: 500;
color: #1f2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.route-config-step-desc {
font-size: 11px;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 右侧面板:步骤详情 */
.route-config-right {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.route-config-right-header {
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
}
.route-config-right-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.route-config-right-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 16px;
color: #9ca3af;
p {
margin-top: 12px;
font-size: 13px;
}
}
.route-config-step-form {
margin-bottom: 12px;
:deep(.el-form-item) {
margin-bottom: 8px;
}
}
.route-config-tabs {
:deep(.el-tabs__header) {
margin-bottom: 8px;
}
}
.route-config-tab-toolbar {
margin-bottom: 8px;
display: flex;
justify-content: flex-end;
}
/* 内嵌表格单元格控件 */
.inner-table {
width: 100%;
:deep(.inner-cell-input) {
width: 100%;
}
:deep(.inner-cell-select) {
width: 100%;
.el-select__wrapper {
min-height: 36px;
}
}
}
@@ -83,23 +83,6 @@
<div class="data-item"><div class="data-label">登记日期</div><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div>
<div class="data-item"><div class="data-label">采集专员</div><div class="data-value">{{ collectorDisplay }}</div></div>
<div class="data-item"><div class="data-label">创建时间</div><div class="data-value">{{ dash(protocol?.createTime) }}</div></div>
<template v-if="showStorageCert">
<div class="data-item">
<div class="data-label">制作状态</div>
<div class="data-value">
<el-tag
:type="storageCertMakeStatusTagType(String(storageCertInfo?.makeStatus || '0'))"
size="small"
>
{{ storageCertMakeStatusLabel(String(storageCertInfo?.makeStatus || '0')) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">制作日期</div>
<div class="data-value">{{ formatYmd(storageCertInfo?.makeDate) }}</div>
</div>
</template>
</div>
<!-- 协议状态变更记录(非储存证查看:仍在基本信息内) -->
<div v-if="!showStorageCert && hasAnyStatusChange" class="status-change-section">
@@ -123,23 +106,53 @@
</template>
</div>
</div>
<!-- 储存证附件(储存证查看:在协议附件上方) -->
<div v-if="showStorageCert" class="attachment-row">
<div class="data-label">储存证附件</div>
<ProtocolAttachmentReadonly
:files="storageCertAttachments"
@preview="openPreview"
@download="downloadFile"
/>
<!-- 协议附件、备注同一行 -->
<div
v-if="protocolAttachments.length || (protocol?.remark && String(protocol.remark).trim())"
class="data-grid data-grid--2 protocol-attach-remark-row"
>
<div class="data-item protocol-attachments-item">
<div class="data-label">协议附件</div>
<div class="data-value protocol-attachments-value">
<ProtocolAttachmentReadonly
:files="protocolAttachments"
@preview="openPreview"
@download="downloadFile"
/>
</div>
</div>
<div class="data-item">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div>
</div>
</div>
<!-- 协议附件 -->
<div class="attachment-row" v-if="protocolAttachments.length">
<div class="data-label">协议附件</div>
<ProtocolAttachmentReadonly :files="protocolAttachments" @preview="openPreview" @download="downloadFile" />
</div>
<!-- 备注 -->
<div class="remark-row" v-if="protocol?.remark">
<div class="data-item data-item--full"><div class="data-label">备注</div><div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div></div>
<!-- 储存证:储存证附件、制作状态、制作日期同一行(在协议附件行下方) -->
<div v-if="showStorageCert" class="data-grid data-grid--3 storage-cert-info-row">
<div class="data-item storage-cert-attachments-item">
<div class="data-label">储存证附件</div>
<div class="data-value storage-cert-attachments-value">
<ProtocolAttachmentReadonly
:files="storageCertAttachments"
@preview="openPreview"
@download="downloadFile"
/>
</div>
</div>
<div class="data-item">
<div class="data-label">制作状态</div>
<div class="data-value">
<el-tag
:type="storageCertMakeStatusTagType(String(storageCertInfo?.makeStatus || '0'))"
size="small"
>
{{ storageCertMakeStatusLabel(String(storageCertInfo?.makeStatus || '0')) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">制作日期</div>
<div class="data-value">{{ formatYmd(storageCertInfo?.makeDate) }}</div>
</div>
</div>
</div>
@@ -1866,6 +1879,38 @@
}
}
/* 储存证:储存证附件 / 制作状态 / 制作日期同一行(在协议附件行下方) */
.storage-cert-info-row {
margin-top: 14px;
}
.storage-cert-attachments-item {
.storage-cert-attachments-value {
font-weight: 400;
}
:deep(.protocol-attachment-empty) {
font-size: 14px;
color: #64748b;
}
}
/* 协议附件、备注同一行 */
.protocol-attach-remark-row {
margin-top: 14px;
}
.protocol-attachments-item {
.protocol-attachments-value {
font-weight: 400;
}
:deep(.protocol-attachment-empty) {
font-size: 14px;
color: #64748b;
}
}
/* ---- Attachment / Remark Row ---- */
.attachment-row {
margin-top: 12px;
@@ -2786,6 +2831,18 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.storage-cert-info-row.data-grid--3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
.storage-cert-attachments-item {
grid-column: 1 / -1;
}
}
.protocol-attach-remark-row.data-grid--2 {
grid-template-columns: 1fr;
}
.payment-summary {
grid-template-columns: repeat(2, 1fr);
}
@@ -2812,6 +2869,10 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.storage-cert-info-row.data-grid--3 {
grid-template-columns: 1fr;
}
.payment-summary {
grid-template-columns: repeat(2, 1fr);
}
@@ -33,6 +33,7 @@
<el-option label="已冻存" value="frozen" />
<el-option label="已售卖" value="sold" />
<el-option label="已使用" value="used" />
<el-option label="已作废" value="voided" />
</el-select>
</el-form-item>
<el-form-item label="代次">
@@ -681,6 +682,7 @@
frozen: { label: '已冻存', type: 'success' },
sold: { label: '已售卖', type: 'warning' },
used: { label: '已使用', type: 'info' },
voided: { label: '已作废', type: 'danger' },
}
return map[status] || { label: status, type: 'info' }
}
@@ -724,6 +726,7 @@
{ label: '已冻存', value: 'frozen' },
{ label: '已售卖', value: 'sold' },
{ label: '已使用', value: 'used' },
{ label: '已作废', value: 'voided' },
]
function mapUnitOption(u: { unitName?: string; unitCode?: string }) {
@@ -480,6 +480,8 @@
defineOptions({ name: 'ProductionProductionTask' })
const route = useRoute()
// ---------------------------------------------------------------------------
// 状态标签映射
// ---------------------------------------------------------------------------
@@ -853,9 +855,15 @@
}
// ---------------------------------------------------------------------------
// 初始化
// 初始化(支持从工单页面跳转带 workOrderNo 筛选)
// ---------------------------------------------------------------------------
onMounted(() => loadData())
onMounted(() => {
const qWorkOrderNo = route.query.workOrderNo as string
if (qWorkOrderNo) {
searchForm.workOrderNo = qWorkOrderNo
}
loadData()
})
</script>
<style scoped lang="scss">
@@ -0,0 +1,246 @@
<!-- 派发生产任务两步式弹窗:阶段一表单 → 阶段二三栏编辑 -->
<template>
<el-dialog
v-model="dialogVisible"
:title="phase === 'form' ? '派发生产任务' : `编辑生产任务 — ${createdTaskName || editTaskName || ''}`"
:width="phase === 'form' ? '560px' : '90vw'"
:class="phase === 'edit' ? 'dialog-form-layout dispatch-route-dialog' : 'dispatch-dialog'"
align-center
destroy-on-close
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 阶段一:表单 -->
<div v-if="phase === 'form'" class="dispatch-dialog-work-order-info">
<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">
<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" />
</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-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>
<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>
</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>
<div class="route-config-center" @dragover.prevent @drop="editor.onDropToCenter">
<el-skeleton v-if="editor.stepListLoading.value" animated :rows="6" />
<template v-else>
<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>
</template>
</div>
<StepDetailPanel :editor="editor" />
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { Rank, Plus, CopyDocument, Delete } from '@element-plus/icons-vue'
import { VueDraggable as draggable } from 'vue-draggable-plus'
import { fetchCreateProductionTask } from '@/api/production-task'
import { fetchProcessRouteList } from '@/api/production'
import type { FormInstance, FormRules } from 'element-plus'
import { useTaskStepEditor } from './useTaskStepEditor'
import StepDetailPanel from './StepDetailPanel.vue'
const props = defineProps<{
visible: boolean
workOrderRow: Record<string, any> | null
/** 传入已有任务 ID 时跳过阶段一,直接进入阶段二编辑 */
editTaskId?: string
/** 编辑模式下显示的任务名称 */
editTaskName?: 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 phase = ref<'form' | 'edit'>('form')
const createdTaskName = ref('')
const submitting = ref(false)
const formRef = ref<FormInstance>()
const routeOptions = ref<any[]>([])
const form = reactive({ taskName: '', routeId: '', description: '', remark: '' })
const rules: FormRules = {
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
}
const editor = useTaskStepEditor()
async function loadRouteOptions() {
try {
const res = await fetchProcessRouteList({ page: 1, pageSize: 100 })
routeOptions.value = res.list || []
} catch { routeOptions.value = [] }
}
function handleClose() {
dialogVisible.value = false
phase.value = 'form'
createdTaskName.value = ''
form.taskName = ''
form.routeId = ''
form.description = ''
form.remark = ''
}
async function handleCreateTask() {
if (!formRef.value) return
await formRef.value.validate()
if (!props.workOrderRow) return
submitting.value = true
try {
const task = await fetchCreateProductionTask({
work_order_id: props.workOrderRow.id,
task_name: form.taskName,
route_id: form.routeId,
description: form.description || undefined,
remark: form.remark || undefined
})
createdTaskName.value = form.taskName
phase.value = 'edit'
await editor.initEditor(task.id)
ElMessage.success('任务已创建,可编辑步骤')
emit('success')
} 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()
}
}
})
</script>
<style scoped lang="scss">
@use '../../../styles/route-config' as *;
.dispatch-dialog-work-order-info {
display: flex;
gap: 20px;
flex-wrap: wrap;
padding: 12px 16px;
background: #f0fdf4;
border-radius: 6px;
margin-bottom: 16px;
font-size: 13px;
color: #475569;
border: 1px solid #d1fae5;
b {
color: #1e293b;
}
}
.dispatch-route-dialog {
:deep(.el-dialog__body) {
max-height: calc(90vh - 60px);
overflow: hidden;
padding: 16px;
}
}
</style>
@@ -0,0 +1,274 @@
<!-- 步骤详情右侧面板:表单 + 试剂/耗材/产出物/数据采集 4 个 Tab -->
<template>
<div class="route-config-right">
<template v-if="editor.selectedStep.value">
<div class="route-config-right-header"><h3>步骤详情</h3></div>
<div class="route-config-right-body">
<el-form label-position="top" class="route-config-step-form">
<div class="form-grid-2">
<el-form-item label="工序名称">
<el-input v-model="editor.selectedStep.value.processName" @change="editor.saveStepBasic" />
</el-form-item>
<el-form-item label="工序分类">
<el-select v-model="editor.selectedStep.value.categoryId" placeholder="选择分类" clearable @change="editor.saveStepBasic">
<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="描述" class="form-col-span-2">
<el-input v-model="editor.selectedStep.value.description" type="textarea" :rows="2"
placeholder="工序描述" @change="editor.saveStepBasic" />
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="editor.selectedStep.value.equipmentName" @change="editor.saveStepBasic" />
</el-form-item>
<el-form-item label="预计时长(min)">
<el-input v-model="editor.selectedStep.value.estimatedDuration" placeholder="如 2h" @change="editor.saveStepBasic" />
</el-form-item>
<el-form-item label="关联SOP" class="form-col-span-2">
<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>
</el-form>
<el-tabs v-model="editor.activeSubTab.value" class="route-config-tabs">
<el-tab-pane label="试剂" name="reagent">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addReagentRow">
<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-column label="试剂编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" @change="editor.saveReagents" />
</template>
</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"
: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" />
</el-select>
</template>
</el-table-column>
<el-table-column label="规格" min-width="100" align="center">
<template #default="{ row }">
<el-input v-model="row.spec" placeholder="规格" class="inner-cell-input" @change="editor.saveReagents" />
</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" @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"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
@change="editor.saveReagents">
<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 label="关键物料" width="90" align="center">
<template #default="{ row }">
<el-switch v-model="row.isKeyMaterial" @change="editor.saveReagents" />
</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.saveReagents" />
</template>
</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>
</template>
</el-table-column>
</el-table>
<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-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-column label="耗材编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" @change="editor.saveConsumables" />
</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"
: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" />
</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" @change="editor.saveConsumables" />
</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" @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"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
@change="editor.saveConsumables">
<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 label="必需" width="80" align="center">
<template #default="{ row }">
<el-switch v-model="row.isRequired" @change="editor.saveConsumables" />
</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.saveConsumables" />
</template>
</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>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.consumables?.length" description="暂无耗材" :image-size="48" />
</el-tab-pane>
<el-tab-pane label="产出物" name="output">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addOutputRow">
<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-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>
</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" />
</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" />
</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" />
</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>
</template>
</el-table-column>
</el-table>
<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-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-column label="采集名称" min-width="180" align="center">
<template #default="{ row }">
<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"
: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" />
</el-select>
</template>
</el-table-column>
<el-table-column label="排序号" min-width="100" align="center">
<template #default="{ row }">
<el-input v-model="row.orderNum" 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" />
</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>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.dataCollections?.length" description="暂无采集项" :image-size="48" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<template v-else>
<div class="route-config-right-empty">
<el-icon :size="48" color="#d1d5db"><Rank /></el-icon>
<p>请选择左侧步骤查看详情</p>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { Plus, Rank } from '@element-plus/icons-vue'
import { useTaskStepEditor } from './useTaskStepEditor'
defineProps<{
editor: ReturnType<typeof useTaskStepEditor>
}>()
</script>
<style scoped lang="scss">
@use '../../../styles/route-config' as *;
</style>
@@ -4,7 +4,7 @@
<div class="page-container">
<header class="page-head">
<h1>入库生产工单</h1>
<p class="page-desc">入库生产工单管理,查看工单详情并完成待配置工单。</p>
<p class="page-desc">入库生产工单管理,查看工单详情、派发生产任务并完成待配置工单。</p>
</header>
<section class="panel">
@@ -35,7 +35,14 @@
</div>
<div class="list-body list-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
<el-table
ref="workOrderListTableRef"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
@row-dblclick="handleView"
>
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" />
@@ -64,12 +71,20 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<el-table-column label="操作" min-width="280" fixed="right" 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
v-if="row.workOrderStatus === 'dpz'"
class="btn-op-item primary"
@click="handleOpenDispatch(row)"
>
<el-icon><Position /></el-icon>派发任务
</el-button>
<el-button
v-if="row.workOrderStatus === 'dpz'"
class="btn-op-item"
@click="handleComplete(row)"
>
<el-icon><Check /></el-icon>完成工单
@@ -89,6 +104,7 @@
</div>
</div>
<!-- 查看详情弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
title="工单详情"
@@ -111,14 +127,57 @@
</div>
</template>
</SystemViewDialog>
<!-- 派发任务弹窗 -->
<DispatchTaskDialog
v-model:visible="dispatchVisible"
:work-order-row="dispatchRow"
@success="loadData"
/>
<!-- 编辑已有任务弹窗 -->
<DispatchTaskDialog
v-model:visible="editTaskVisible"
:work-order-row="editTaskRow"
:edit-task-id="editTaskId"
:edit-task-name="editTaskName"
@success="loadData"
/>
<!-- 任务列表弹窗 -->
<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 prop="taskName" label="任务名称" min-width="140" />
<el-table-column prop="routeName" label="工艺路线" min-width="140">
<template #default="{ row }">{{ row.routeName || '—' }}</template>
</el-table-column>
<el-table-column prop="taskStatus" label="任务状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="taskStatusLabel(row.taskStatus).type" size="small">
{{ taskStatusLabel(row.taskStatus).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openTaskEditor(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!taskSelectLoading && !taskSelectList.length" description="暂无任务,请先派发任务" :image-size="48" />
</div>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
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 } from '@element-plus/icons-vue'
import { Search, RefreshLeft, View, Check, Document, Position, EditPen } from '@element-plus/icons-vue'
import {
fetchProductionWorkOrderList,
fetchProductionWorkOrderDetail,
@@ -134,10 +193,11 @@
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import DispatchTaskDialog from './DispatchTaskDialog.vue'
import { fetchProductionTaskList } from '@/api/production-task'
defineOptions({ name: 'ProductionProductionWorkOrder' })
/* ---- 标签映射 ---- */
const woStatusTag = (status: string) => {
const map: Record<string, { label: string; type: string }> = {
dpz: { label: '待配置', type: 'info' },
@@ -145,7 +205,14 @@
}
return map[status] || { label: status, type: 'info' }
}
const taskStatusLabel = (status: string) => {
const map: Record<string, { label: string; type: string }> = {
dpz: { label: '待配置', type: 'info' },
jxz: { label: '进行中', type: 'warning' },
ywc: { label: '已完成', type: 'success' }
}
return map[status] || { label: status, type: 'info' }
}
const sourceTag = (source: string) => {
const map: Record<string, { label: string; type: string }> = {
cell_storage: { label: '细胞存储', type: 'success' },
@@ -154,12 +221,16 @@
return map[source] || { label: source, type: 'info' }
}
/* ---- 搜索 & 列表 ---- */
const searchForm = reactive({ workOrderNo: '', workOrderStatus: '' })
const tableData = ref<any[]>([])
const workOrderListTableRef = ref<TableInstance>()
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
async function layoutWorkOrderTable() {
await nextTick()
workOrderListTableRef.value?.doLayout?.()
}
async function loadData() {
loading.value = true
try {
@@ -173,22 +244,12 @@
pagination.total = res.total ?? 0
} finally {
loading.value = false
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)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
@@ -196,46 +257,35 @@
const viewReceptionAttachmentFiles = computed(() => {
const r = detail.value?.reception
if (!r) return []
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length)
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
}
const urls = String(r.receiveAttachments || '')
if (!urls.trim()) return []
return urls
.split(',')
.map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
return urls.split(',').map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
}).filter(Boolean) as Array<Record<string, unknown>>
})
function receptionAttachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleReceptionAttachmentPreview(file: Record<string, unknown>) {
const url = receptionAttachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: receptionAttachmentFullUrl })
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
return String(v)
}
function formatCheckResults(
items?: Array<{ itemName?: string; checked?: boolean }>
) {
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
if (!items?.length) return '—'
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
}
@@ -245,10 +295,7 @@
const d = detail.value
const r = d.reception || {}
const transportFields: ViewSection['fields'] = [
{
label: '运输方式',
value: transportTypeLabel(String(r.transportType || ''))
}
{ label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
]
if (r.transportType === 'kdys') {
transportFields.push(
@@ -261,14 +308,10 @@
{ label: '携带人电话', value: r.carrierPhone || '—' }
)
}
if (r.etaTime) {
transportFields.push({ label: '预计到达', value: r.etaTime })
}
const transportSection =
transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
if (r.etaTime) transportFields.push({ label: '预计到达', value: r.etaTime })
const transportSection = transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
return [
{
title: '基本信息',
@@ -276,17 +319,9 @@
{ 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 }
]
@@ -298,17 +333,10 @@
fields: [
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
{
label: '接收温度(℃)',
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
},
{ 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 }
]
@@ -320,8 +348,6 @@
detail.value = await fetchProductionWorkOrderDetail(row.id)
viewVisible.value = true
}
/* ---- 完成工单 ---- */
async function handleComplete(row: any) {
await ElMessageBox.confirm('确定要完成该工单吗?', '提示', { type: 'warning' })
await fetchCompleteProductionWorkOrder(row.id, {})
@@ -329,6 +355,47 @@
loadData()
}
// ── 派发任务 ──────────────────────────────────────────
const dispatchVisible = ref(false)
const dispatchRow = ref<any>(null)
function handleOpenDispatch(row: any) {
dispatchRow.value = row
dispatchVisible.value = true
}
// ── 编辑已有任务 ──────────────────────────────────────
const editTaskVisible = ref(false)
const editTaskRow = ref<any>(null)
const editTaskId = ref('')
const editTaskName = ref('')
// 任务选择弹窗
const taskSelectVisible = ref(false)
const taskSelectList = ref<any[]>([])
const taskSelectLoading = ref(false)
async function handleEditTask(row: any) {
taskSelectLoading.value = true
taskSelectVisible.value = true
taskSelectList.value = []
try {
const res = await fetchProductionTaskList({ workOrderId: row.id, pageSize: 100 })
taskSelectList.value = res.list || []
} catch {
taskSelectList.value = []
} finally {
taskSelectLoading.value = false
}
}
function openTaskEditor(task: any) {
editTaskId.value = task.id
editTaskName.value = task.taskName || ''
editTaskRow.value = null
editTaskVisible.value = true
taskSelectVisible.value = false
}
onMounted(() => loadData())
</script>
@@ -343,16 +410,8 @@
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>
@@ -0,0 +1,496 @@
import {
fetchProductionTaskDetail,
fetchAddTaskStep,
fetchUpdateTaskStep,
fetchDeleteTaskStep,
fetchReorderTaskSteps,
fetchCopyTaskStep,
fetchReplaceStepReagents,
fetchReplaceStepConsumables,
fetchReplaceStepDataCollection,
fetchReplaceStepOutputs
} from '@/api/production-task'
import {
fetchAllProcesses,
fetchAllProcessCategories,
fetchAllSopDocuments,
fetchAllOutputTypes,
fetchReagentList,
fetchConsumableList
} from '@/api/production'
import { fetchItemUnitList } from '@/api/admin'
// ── helpers ──────────────────────────────────────────────────────────
function toApiFlag(val: boolean | number | string | undefined): string {
if (val === true || val === 1 || val === '1') return '1'
return '0'
}
function fromApiFlag(val: boolean | number | string | undefined): boolean {
return val === true || val === 1 || val === '1'
}
function normalizeStepSubTables(step: Record<string, any>) {
return {
...step,
reagents: (step.reagents || []).map((r: Record<string, any>) => ({
...r,
isKeyMaterial: fromApiFlag(r.isKeyMaterial)
})),
consumables: (step.consumables || []).map((c: Record<string, any>) => ({
...c,
isRequired: fromApiFlag(c.isRequired)
})),
outputs: step.outputs || [],
dataCollections: step.dataCollections || []
}
}
function parseOrderNum(value: unknown): number {
const n = Number.parseInt(String(value ?? '').trim(), 10)
if (!Number.isFinite(n) || n < 0) return 0
return Math.min(n, 9999)
}
// ── composable ───────────────────────────────────────────────────────
export function useTaskStepEditor() {
const taskId = ref('')
const stepList = ref<Record<string, any>[]>([])
const selectedStepId = ref('')
const stepListLoading = ref(false)
const activeSubTab = ref('reagent')
const processSearchKeyword = ref('')
const configSuppressSave = ref(false)
const processLibrary = ref<Record<string, any>[]>([])
const expandedCategories = ref<string[]>([])
const categoryOptions = ref<Record<string, any>[]>([])
const sopOptions = ref<Record<string, any>[]>([])
const outputTypeOptions = ref<Record<string, any>[]>([])
const itemUnitOptions = ref<{ label: string; value: string }[]>([])
const itemUnitOptionsLoading = ref(false)
const reagentOptions = ref<{ id: string; label: string }[]>([])
const reagentOptionsLoading = ref(false)
const reagentMasterById = ref<Record<string, Record<string, any>>>({})
const consumableOptions = ref<{ id: string; label: string }[]>([])
const consumableOptionsLoading = ref(false)
const consumableMasterById = ref<Record<string, Record<string, any>>>({})
function mapItemUnitOption(u: { unitName?: string; unitCode?: string }) {
const name = (u.unitName || '').trim()
const code = (u.unitCode || '').trim()
return { label: code ? `${name}(${code})` : name, value: name }
}
function ensureItemUnitOption(unitName: string) {
const v = unitName?.trim()
if (!v) return
if (!itemUnitOptions.value.some((o) => o.value === v))
itemUnitOptions.value = [{ label: v, value: v }, ...itemUnitOptions.value]
}
async function searchItemUnitOptions(query: string) {
itemUnitOptionsLoading.value = true
try {
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 = []
} finally {
itemUnitOptionsLoading.value = false
}
}
function mapReagentOptionLabel(r: { reagentName?: string; reagentCode?: string }) {
const name = (r.reagentName || '').trim()
const code = (r.reagentCode || '').trim()
return code ? `${name}(${code})` : name
}
function cacheReagentMasters(list: Record<string, any>[]) {
const next = { ...reagentMasterById.value }
for (const r of list) if (r?.id) next[r.id] = r
reagentMasterById.value = next
}
async function searchReagentOptions(query: string) {
reagentOptionsLoading.value = true
try {
const res = await fetchReagentList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const list = res.list || []
cacheReagentMasters(list)
reagentOptions.value = list
.filter((r) => (r.reagentName || '').trim() || (r.reagentCode || '').trim())
.map((r) => ({ id: r.id, label: mapReagentOptionLabel(r) }))
ensureAllStepReagentOptions()
} catch {
reagentOptions.value = []
ensureAllStepReagentOptions()
} finally {
reagentOptionsLoading.value = false
}
}
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]
return
}
const code = (row.reagentCode || '').trim()
const name = (row.reagentName || '').trim()
if (!code && !name) return
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]
}
function ensureAllStepReagentOptions() {
for (const step of stepList.value) {
;(step.reagents || []).forEach((r: Record<string, any>) => ensureReagentOption(r))
}
}
function onReagentRowSelect(row: Record<string, any>, reagentId: string) {
if (!reagentId) { row.reagentId = ''; saveReagents(); return }
if (String(reagentId).startsWith('legacy:')) { saveReagents(); return }
const master = reagentMasterById.value[reagentId]
if (!master) { saveReagents(); return }
Object.assign(row, {
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)
ensureReagentOption(row)
saveReagents()
}
function mapConsumableOptionLabel(c: { consumableName?: string; consumableCode?: string }) {
const name = (c.consumableName || '').trim()
const code = (c.consumableCode || '').trim()
return code ? `${name}(${code})` : name
}
function cacheConsumableMasters(list: Record<string, any>[]) {
const next = { ...consumableMasterById.value }
for (const c of list) if (c?.id) next[c.id] = c
consumableMasterById.value = next
}
async function searchConsumableOptions(query: string) {
consumableOptionsLoading.value = true
try {
const res = await fetchConsumableList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const list = res.list || []
cacheConsumableMasters(list)
consumableOptions.value = list
.filter((c) => (c.consumableName || '').trim() || (c.consumableCode || '').trim())
.map((c) => ({ id: c.id, label: mapConsumableOptionLabel(c) }))
ensureAllStepConsumableOptions()
} catch {
consumableOptions.value = []
ensureAllStepConsumableOptions()
} finally {
consumableOptionsLoading.value = false
}
}
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]
return
}
const code = (row.consumableCode || '').trim()
const name = (row.consumableName || '').trim()
if (!code && !name) return
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]
}
function ensureAllStepConsumableOptions() {
for (const step of stepList.value) {
;(step.consumables || []).forEach((c: Record<string, any>) => ensureConsumableOption(c))
}
}
function onConsumableRowSelect(row: Record<string, any>, consumableId: string) {
if (!consumableId) { row.consumableId = ''; saveConsumables(); return }
if (String(consumableId).startsWith('legacy:')) { saveConsumables(); return }
const master = consumableMasterById.value[consumableId]
if (!master) { saveConsumables(); return }
Object.assign(row, {
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)
ensureConsumableOption(row)
saveConsumables()
}
function ensureAllStepUnitOptions() {
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))
}
}
const selectedStep = computed(() => {
if (!selectedStepId.value) return null
return stepList.value.find((s) => s.id === selectedStepId.value) || null
})
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)
},
set(val: string[]) {
if (!selectedStep.value) return
selectedStep.value.sopIds = val.join(',')
}
})
const filteredProcessLibrary = computed(() => {
const keyword = processSearchKeyword.value?.trim().toLowerCase() || ''
return processLibrary.value
.map((group) => {
const filtered = keyword
? group.processes.filter(
(p: Record<string, any>) =>
(p.processName || '').toLowerCase().includes(keyword) ||
(p.processCode || '').toLowerCase().includes(keyword)
)
: group.processes
return { categoryId: group.categoryId, categoryName: group.categoryName, processes: filtered }
})
.filter((group) => group.processes.length > 0)
})
async function loadConfigReferenceData() {
const [processes, categories, sops, outputTypes] = await Promise.all([
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 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: [] }
})
;(processes || []).forEach((proc: Record<string, any>) => {
const catId = proc.categoryId || uKey
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('')])
}
async function loadTaskDetail() {
if (!taskId.value) return
try {
const detail = await fetchProductionTaskDetail(taskId.value)
stepList.value = (detail.steps || []).map(normalizeStepSubTables)
ensureAllStepReagentOptions()
ensureAllStepConsumableOptions()
ensureAllStepUnitOptions()
if (stepList.value.length > 0 && !selectedStepId.value)
selectedStepId.value = stepList.value[0].id
} catch {
stepList.value = []
}
}
function onDragStart(event: DragEvent, proc: Record<string, any>) {
event.dataTransfer?.setData('processId', proc.id)
event.dataTransfer!.effectAllowed = 'copy'
}
async function onDropToCenter(event: DragEvent) {
const processId = event.dataTransfer?.getData('processId')
if (!processId || !taskId.value) return
stepListLoading.value = true
try {
const newStep = await fetchAddTaskStep(taskId.value, { processId })
await loadTaskDetail()
if (newStep?.id) selectedStepId.value = newStep.id
ElMessage.success('步骤添加成功')
} catch { /* ignore */ } finally {
stepListLoading.value = false
}
}
async function onStepDragEnd() {
if (!taskId.value) return
stepListLoading.value = true
try {
await fetchReorderTaskSteps({ taskId: taskId.value, stepIds: stepList.value.map((s) => s.id) })
} catch { /* ignore */ } finally {
stepListLoading.value = false
}
}
function selectStep(step: Record<string, any>) { selectedStepId.value = step.id }
async function handleCopyStep(step: Record<string, any>) {
stepListLoading.value = true
try {
const ns = await fetchCopyTaskStep({ taskId: taskId.value, stepId: step.id })
await loadTaskDetail()
if (ns?.id) selectedStepId.value = ns.id
ElMessage.success('步骤已复制')
} catch (e: any) { ElMessage.error(e?.message || '复制失败') } finally {
stepListLoading.value = false
}
}
async function handleDeleteStep(step: Record<string, any>) {
await ElMessageBox.confirm('确定删除该步骤吗?', '提示', { type: 'warning' })
stepListLoading.value = true
try {
await fetchDeleteTaskStep(taskId.value, step.id)
await loadTaskDetail()
if (selectedStepId.value === step.id)
selectedStepId.value = stepList.value.length > 0 ? stepList.value[0].id : ''
ElMessage.success('步骤已删除')
} catch { /* ignore */ } finally {
stepListLoading.value = false
}
}
async function saveStepBasic() {
if (!selectedStep.value) return
try {
await fetchUpdateTaskStep(taskId.value, selectedStep.value.id, {
processName: selectedStep.value.processName,
categoryId: selectedStep.value.categoryId,
description: selectedStep.value.description,
equipmentName: selectedStep.value.equipmentName,
estimatedDuration: selectedStep.value.estimatedDuration,
sopIds: selectedStep.value.sopIds
})
} catch { /* ignore */ }
}
function onSopIdsChange(val: string[]) {
if (!selectedStep.value) return
selectedStep.value.sopIds = val.join(',')
saveStepBasic()
}
function addReagentRow() {
if (!selectedStep.value) return
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
selectedStep.value.reagents.push({
reagentId: '', reagentCode: '', reagentName: '', spec: '',
standardUsage: '', unit: '', isKeyMaterial: false, remark: ''
})
saveReagents()
}
function removeReagentRow(i: number) { selectedStep.value?.reagents?.splice(i, 1); saveReagents() }
async function saveReagents() {
if (!selectedStep.value || configSuppressSave.value) return
try {
await fetchReplaceStepReagents(taskId.value, selectedStep.value.id, {
items: (selectedStep.value.reagents || []).map((r: Record<string, any>) => ({
reagentCode: r.reagentCode, reagentName: r.reagentName, spec: r.spec,
standardUsage: r.standardUsage, unit: r.unit,
isKeyMaterial: toApiFlag(r.isKeyMaterial), remark: r.remark || ''
}))
})
} 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: ''
})
saveConsumables()
}
function removeConsumableRow(i: number) { selectedStep.value?.consumables?.splice(i, 1); saveConsumables() }
async function saveConsumables() {
if (!selectedStep.value || configSuppressSave.value) return
try {
await fetchReplaceStepConsumables(taskId.value, selectedStep.value.id, {
items: (selectedStep.value.consumables || []).map((c: Record<string, any>) => ({
consumableCode: c.consumableCode, consumableName: c.consumableName, specModel: c.specModel,
standardUsage: c.standardUsage, unit: c.unit,
isRequired: toApiFlag(c.isRequired), remark: c.remark || ''
}))
})
} catch { /* ignore */ }
}
function addOutputRow() {
if (!selectedStep.value) return
if (!selectedStep.value.outputs) selectedStep.value.outputs = []
selectedStep.value.outputs.push({ outputTypeId: '', outputName: '', outputBarcode: '', remark: '' })
saveOutputs()
}
function removeOutputRow(i: number) { selectedStep.value?.outputs?.splice(i, 1); saveOutputs() }
async function saveOutputs() {
if (!selectedStep.value || configSuppressSave.value) return
try {
await fetchReplaceStepOutputs(taskId.value, selectedStep.value.id, {
items: (selectedStep.value.outputs || []).map((o: Record<string, any>) => ({
outputTypeId: o.outputTypeId, outputName: o.outputName,
outputBarcode: o.outputBarcode, remark: o.remark || ''
}))
})
} catch { /* ignore */ }
}
function addDataCollectionRow() {
if (!selectedStep.value) return
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
selectedStep.value.dataCollections.push({ collectionName: '', unit: '', orderNum: 0, remark: '' })
saveDataCollections()
}
function removeDataCollectionRow(i: number) { selectedStep.value?.dataCollections?.splice(i, 1); saveDataCollections() }
async function saveDataCollections() {
if (!selectedStep.value || configSuppressSave.value) return
try {
await fetchReplaceStepDataCollection(taskId.value, selectedStep.value.id, {
items: (selectedStep.value.dataCollections || []).map((d: Record<string, any>) => ({
collectionName: d.collectionName, unit: d.unit,
orderNum: parseOrderNum(d.orderNum), remark: d.remark || ''
}))
})
} catch { /* ignore */ }
}
async function initEditor(id: string) {
taskId.value = id
selectedStepId.value = ''
activeSubTab.value = 'reagent'
processSearchKeyword.value = ''
configSuppressSave.value = true
try {
await loadConfigReferenceData()
await loadTaskDetail()
} finally {
await nextTick()
configSuppressSave.value = false
}
}
return {
taskId, stepList, selectedStepId, selectedStep, stepListLoading,
activeSubTab, processSearchKeyword, configSuppressSave,
processLibrary, filteredProcessLibrary, expandedCategories,
categoryOptions, sopOptions, outputTypeOptions,
itemUnitOptions, itemUnitOptionsLoading,
reagentOptions, reagentOptionsLoading,
consumableOptions, consumableOptionsLoading,
sopIdsArr,
onDragStart, onDropToCenter, onStepDragEnd, selectStep,
handleCopyStep, handleDeleteStep,
saveStepBasic, onSopIdsChange,
searchItemUnitOptions, searchReagentOptions, searchConsumableOptions,
onReagentRowSelect, onConsumableRowSelect,
addReagentRow, removeReagentRow, saveReagents,
addConsumableRow, removeConsumableRow, saveConsumables,
addOutputRow, removeOutputRow, saveOutputs,
addDataCollectionRow, removeDataCollectionRow, saveDataCollections,
initEditor
}
}