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

No files matched your search

File diff suppressed because it is too large. Load diff
+293 -18
View File
@@ -6893,21 +6893,21 @@
"label": "fetchQcWorkOrderList()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L89"
"source_location": "L104"
},
{
"id": "api_qc_work_order_fetchqcworkorderdetail",
"label": "fetchQcWorkOrderDetail()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L101"
"source_location": "L116"
},
{
"id": "api_qc_work_order_fetchconfigureqcworkorder",
"label": "fetchConfigureQcWorkOrder()",
"file_type": "code",
"source_file": "api/qc-work-order.ts",
"source_location": "L107"
"source_location": "L122"
},
{
"id": "views_auth_login_index_vue",
@@ -8300,21 +8300,21 @@
"label": "if()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1055"
"source_location": "L1068"
},
{
"id": "process_route_index_get",
"label": "get()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1256"
"source_location": "L1269"
},
{
"id": "process_route_index_set",
"label": "set()",
"file_type": "code",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1266"
"source_location": "L1279"
},
{
"id": "views_production_sop_index_vue",
@@ -8393,6 +8393,13 @@
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L1"
},
{
"id": "production_work_order_stepdetailpanel_for",
"label": "for()",
"file_type": "code",
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L362"
},
{
"id": "views_production_production_work_order_taskexecutiondialog_vue",
"label": "TaskExecutionDialog.vue",
@@ -8414,6 +8421,13 @@
"source_file": "views/production/production-work-order/DispatchTaskDialog.vue",
"source_location": "L203"
},
{
"id": "views_production_production_work_order_taskoutputproductdialog_vue",
"label": "TaskOutputProductDialog.vue",
"file_type": "code",
"source_file": "views/production/production-work-order/TaskOutputProductDialog.vue",
"source_location": "L1"
},
{
"id": "views_production_consumable_pack_index_vue",
"label": "index.vue",
@@ -8456,6 +8470,83 @@
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L1"
},
{
"id": "qc_work_order_index_handlereceptionattachmentpreview",
"label": "handleReceptionAttachmentPreview()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L278"
},
{
"id": "qc_work_order_index_handlereceptionattachmentdownload",
"label": "handleReceptionAttachmentDownload()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L283"
},
{
"id": "qc_work_order_index_taskattachmentfullurl",
"label": "taskAttachmentFullUrl()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L287"
},
{
"id": "qc_work_order_index_handletaskattachmentpreview",
"label": "handleTaskAttachmentPreview()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L294"
},
{
"id": "qc_work_order_index_handletaskattachmentdownload",
"label": "handleTaskAttachmentDownload()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L299"
},
{
"id": "qc_work_order_index_formatconfiguredlabitems",
"label": "formatConfiguredLabItems()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L315"
},
{
"id": "qc_work_order_index_formatconfiguredthirditems",
"label": "formatConfiguredThirdItems()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L321"
},
{
"id": "qc_work_order_index_buildtasktypesections",
"label": "buildTaskTypeSections()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L329"
},
{
"id": "qc_work_order_index_buildconfiguredonlysection",
"label": "buildConfiguredOnlySection()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L388"
},
{
"id": "qc_work_order_index_formatdetectmethods",
"label": "formatDetectMethods()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L406"
},
{
"id": "qc_work_order_index_formatmetric",
"label": "formatMetric()",
"file_type": "code",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L413"
},
{
"id": "views_quality_inspection_sample_reception_check_item_index_vue",
"label": "index.vue",
@@ -21269,7 +21360,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L89",
"source_location": "L104",
"weight": 1.0
},
{
@@ -21278,7 +21369,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L101",
"source_location": "L116",
"weight": 1.0
},
{
@@ -21287,7 +21378,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "api/qc-work-order.ts",
"source_location": "L107",
"source_location": "L122",
"weight": 1.0
},
{
@@ -23846,13 +23937,40 @@
"source_location": "L504",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "icons_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L729",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "views_cell_storage_protocol_protocolattachmentreadonly_vue",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L730",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L731",
"weight": 1.0
},
{
"source": "views_production_process_route_index_vue",
"target": "vue_draggable_plus",
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L719",
"source_location": "L732",
"weight": 1.0
},
{
@@ -23861,7 +23979,7 @@
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L720",
"source_location": "L733",
"weight": 1.0
},
{
@@ -23870,7 +23988,7 @@
"relation": "imports_from",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L743",
"source_location": "L756",
"weight": 1.0
},
{
@@ -23879,7 +23997,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1055",
"source_location": "L1068",
"weight": 1.0
},
{
@@ -23888,7 +24006,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1172",
"source_location": "L1185",
"weight": 1.0
},
{
@@ -23897,7 +24015,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1256",
"source_location": "L1269",
"weight": 1.0
},
{
@@ -23906,7 +24024,7 @@
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/process-route/index.vue",
"source_location": "L1266",
"source_location": "L1279",
"weight": 1.0
},
{
@@ -23990,6 +24108,15 @@
"source_location": "L57",
"weight": 1.0
},
{
"source": "views_production_production_work_order_stepdetailpanel_vue",
"target": "production_work_order_stepdetailpanel_for",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L362",
"weight": 1.0
},
{
"source": "views_production_production_work_order_dispatchtaskdialog_vue",
"target": "production_work_order_dispatchtaskdialog_if",
@@ -23999,6 +24126,114 @@
"source_location": "L203",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handlereceptionattachmentpreview",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L278",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handlereceptionattachmentdownload",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L283",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_taskattachmentfullurl",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L287",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handletaskattachmentpreview",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L294",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_handletaskattachmentdownload",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L299",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatconfiguredlabitems",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L315",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatconfiguredthirditems",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L321",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_buildtasktypesections",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L329",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_buildconfiguredonlysection",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L388",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatdetectmethods",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L406",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_work_order_index_vue",
"target": "qc_work_order_index_formatmetric",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L413",
"weight": 1.0
},
{
"source": "qc_work_order_index_handletaskattachmentpreview",
"target": "qc_work_order_index_taskattachmentfullurl",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L295",
"weight": 1.0
},
{
"source": "views_quality_inspection_qc_material_index_vue",
"target": "element_plus",
@@ -29514,7 +29749,7 @@
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "api/qc-work-order.ts",
"source_location": "L95",
"source_location": "L110",
"weight": 1.0
},
{
@@ -29524,7 +29759,7 @@
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "api/qc-work-order.ts",
"source_location": "L102",
"source_location": "L117",
"weight": 1.0
},
{
@@ -30327,6 +30562,46 @@
"source_location": "L571",
"weight": 1.0
},
{
"source": "production_work_order_stepdetailpanel_for",
"target": "storage_facility_index_push",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/production/production-work-order/StepDetailPanel.vue",
"source_location": "L364",
"weight": 1.0
},
{
"source": "qc_work_order_index_handlereceptionattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L284",
"weight": 1.0
},
{
"source": "qc_work_order_index_handletaskattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L300",
"weight": 1.0
},
{
"source": "qc_work_order_index_formatdetectmethods",
"target": "storage_facility_index_push",
"relation": "calls",
"confidence": "INFERRED",
"confidence_score": 0.8,
"source_file": "views/quality-inspection/qc-work-order/index.vue",
"source_location": "L408",
"weight": 1.0
},
{
"source": "qc_task_index_handlereceptionattachmentdownload",
"target": "utils_download_attachment_downloadattachmentfile",
+99 -87
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-05-22)
## Corpus Check
- Large corpus: 580 files · ~303,483 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 581 files · ~306,392 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 1416 nodes · 1764 edges · 42 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 386 edges (avg confidence: 0.8)
- 1429 nodes · 1782 edges · 44 communities detected
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 390 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -34,10 +34,10 @@
- [[_COMMUNITY_Community 22|Community 22]]
- [[_COMMUNITY_Community 23|Community 23]]
- [[_COMMUNITY_Community 24|Community 24]]
- [[_COMMUNITY_Community 26|Community 26]]
- [[_COMMUNITY_Community 25|Community 25]]
- [[_COMMUNITY_Community 27|Community 27]]
- [[_COMMUNITY_Community 28|Community 28]]
- [[_COMMUNITY_Community 30|Community 30]]
- [[_COMMUNITY_Community 29|Community 29]]
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Community 32|Community 32]]
- [[_COMMUNITY_Community 33|Community 33]]
@@ -45,17 +45,19 @@
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 39|Community 39]]
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 47|Community 47]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 155 edges
2. `WebSocketClient` - 26 edges
3. `push()` - 18 edges
3. `push()` - 20 edges
4. `withFullscreenLoading()` - 16 edges
5. `MenuProcessor` - 16 edges
6. `VersionManager` - 15 edges
@@ -68,7 +70,7 @@
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/system/site/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
- `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()` [INFERRED]
utils/http/index.ts → api/auth.ts
- `fetchLeaveList()` --calls--> `get()` [INFERRED]
@@ -80,215 +82,225 @@
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (80): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+72 more)
Nodes (78): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+70 more)
### Community 1 - "Community 1"
Cohesion: 0.03
Nodes (57): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+49 more)
Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
### Community 2 - "Community 2"
Cohesion: 0.04
Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
### Community 3 - "Community 3"
Cohesion: 0.05
Nodes (32): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+24 more)
### Community 4 - "Community 4"
Cohesion: 0.04
Nodes (18): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllSopDocuments(), fetchConsumableList() (+10 more)
### Community 4 - "Community 4"
Cohesion: 0.05
Nodes (33): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+25 more)
### Community 5 - "Community 5"
Cohesion: 0.05
Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
### Community 6 - "Community 6"
Cohesion: 0.09
Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
Nodes (5): handleReset(), handleSearch(), WebSocketClient, TableCache, emit()
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (10): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), TableCache, themeAnimation() (+2 more)
Cohesion: 0.05
Nodes (18): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+10 more)
### Community 8 - "Community 8"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
### Community 9 - "Community 9"
Cohesion: 0.13
Nodes (4): handleReset(), handleSearch(), WebSocketClient, emit()
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 10 - "Community 10"
Cohesion: 0.07
Nodes (13): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+5 more)
Cohesion: 0.08
Nodes (10): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), themeAnimation() (+2 more)
### Community 11 - "Community 11"
Cohesion: 0.09
Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
Cohesion: 0.1
Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
### Community 12 - "Community 12"
Cohesion: 0.1
Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
### Community 13 - "Community 13"
Cohesion: 0.1
Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
### Community 14 - "Community 14"
Cohesion: 0.08
Nodes (14): fetchLeaveDetail(), fetchLeaveList(), fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances() (+6 more)
Cohesion: 0.1
Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
### Community 15 - "Community 15"
Cohesion: 0.1
Nodes (18): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+10 more)
### Community 16 - "Community 16"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
### Community 17 - "Community 17"
Cohesion: 0.13
Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
### Community 18 - "Community 18"
### Community 16 - "Community 16"
Cohesion: 0.14
Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
### Community 19 - "Community 19"
Cohesion: 0.16
### Community 17 - "Community 17"
Cohesion: 0.14
Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
### Community 18 - "Community 18"
Cohesion: 0.19
Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
### Community 19 - "Community 19"
Cohesion: 0.11
Nodes (8): fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchRefundConfirmedList(), fetchRefundConfirmList()
### Community 20 - "Community 20"
Cohesion: 0.12
Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
### Community 21 - "Community 21"
Cohesion: 0.16
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
### Community 21 - "Community 21"
### Community 22 - "Community 22"
Cohesion: 0.12
Nodes (2): fetchProductionTaskDetail(), fetchProductionTaskList()
### Community 22 - "Community 22"
### Community 23 - "Community 23"
Cohesion: 0.2
Nodes (2): ComponentLoader, RouteTransformer
### Community 23 - "Community 23"
### Community 24 - "Community 24"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 24 - "Community 24"
Cohesion: 0.17
Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
### Community 25 - "Community 25"
Cohesion: 0.18
Nodes (11): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleDelete(), handleReset(), handleSearch() (+3 more)
### Community 26 - "Community 26"
### Community 27 - "Community 27"
Cohesion: 0.17
Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
### Community 27 - "Community 27"
### Community 28 - "Community 28"
Cohesion: 0.18
Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
### Community 28 - "Community 28"
### Community 29 - "Community 29"
Cohesion: 0.2
Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
### Community 30 - "Community 30"
### Community 31 - "Community 31"
Cohesion: 0.39
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
### Community 31 - "Community 31"
### Community 32 - "Community 32"
Cohesion: 0.36
Nodes (1): TableHeightCalculator
### Community 32 - "Community 32"
### Community 33 - "Community 33"
Cohesion: 0.33
Nodes (2): logApiCryptoBootInfo(), maskSecret()
### Community 33 - "Community 33"
### Community 34 - "Community 34"
Cohesion: 0.29
Nodes (2): fetchLeaveDetail(), fetchLeaveList()
### Community 35 - "Community 35"
Cohesion: 0.47
Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
### Community 34 - "Community 34"
### Community 36 - "Community 36"
Cohesion: 0.33
Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
### Community 35 - "Community 35"
### Community 37 - "Community 37"
Cohesion: 0.33
Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
### Community 36 - "Community 36"
### Community 38 - "Community 38"
Cohesion: 0.33
Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
### Community 37 - "Community 37"
### Community 39 - "Community 39"
Cohesion: 0.33
Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
### Community 39 - "Community 39"
### Community 40 - "Community 40"
Cohesion: 0.33
Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
### Community 42 - "Community 42"
Cohesion: 0.53
Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
### Community 40 - "Community 40"
### Community 43 - "Community 43"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 41 - "Community 41"
### Community 44 - "Community 44"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 42 - "Community 42"
### Community 45 - "Community 45"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 44 - "Community 44"
### Community 47 - "Community 47"
Cohesion: 1.0
Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
### Community 52 - "Community 52"
Cohesion: 0.67
Nodes (2): fetchDeleteProjectSystemItem(), if()
## Knowledge Gaps
- **Thin community `Community 21`** (17 nodes): `fetchAddTaskStep()`, `fetchCopyTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReorderTaskSteps()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepOutputs()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
- **Thin community `Community 22`** (17 nodes): `fetchAddTaskStep()`, `fetchCopyTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReorderTaskSteps()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepOutputs()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 22`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
- **Thin community `Community 23`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 28`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
- **Thin community `Community 29`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
- **Thin community `Community 32`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 32`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
- **Thin community `Community 33`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 34`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
- **Thin community `Community 34`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 35`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
- **Thin community `Community 36`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 36`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
- **Thin community `Community 37`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 37`** (6 nodes): `fetchCompleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
- **Thin community `Community 38`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 40`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
- **Thin community `Community 39`** (6 nodes): `fetchCompleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
- **Thin community `Community 40`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 42`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
- **Thin community `Community 43`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 44`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
- **Thin community `Community 44`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 52`** (3 nodes): `fetchDeleteProjectSystemItem()`, `if()`, `index.vue`
- **Thin community `Community 45`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 47`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 21`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `Community 34`, `Community 35`, `Community 36`, `Community 37`?**
_High betweenness centrality (0.267) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 3` to `Community 0`, `Community 2`, `Community 7`, `Community 9`, `Community 11`, `Community 12`?**
_High betweenness centrality (0.075) - this node is a cross-community bridge._
- **Why does `buildTreePathLabelFromFlat()` connect `Community 3` to `Community 0`, `Community 7`?**
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 34`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 40`?**
_High betweenness centrality (0.266) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 4` to `Community 0`, `Community 2`, `Community 6`, `Community 7`, `Community 10`, `Community 13`, `Community 18`?**
_High betweenness centrality (0.091) - this node is a cross-community bridge._
- **Why does `fetchGetLatestUpgradeLog()` connect `Community 18` to `Community 0`?**
_High betweenness centrality (0.042) - this node is a cross-community bridge._
- **Are the 154 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
_`get()` has 154 INFERRED edges - model-reasoned connections that need verification._
- **Are the 17 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
_`push()` has 17 INFERRED edges - model-reasoned connections that need verification._
- **Are the 19 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
_`push()` has 19 INFERRED edges - model-reasoned connections that need verification._
- **Are the 15 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
_`withFullscreenLoading()` has 15 INFERRED edges - model-reasoned connections that need verification._
- **Should `Community 0` be split into smaller, more focused modules?**
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">1416 nodes &middot; 1764 edges &middot; 243 communities</div>
<div id="stats">1429 nodes &middot; 1782 edges &middot; 244 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 32, "community_name": "Community 32", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 32, "community_name": "Community 32", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#9C755F", "border": "#9C755F", "highlight": {"background": "#ffffff", "border": "#9C755F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 78, "community_name": "Community 78", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 79, "community_name": "Community 79", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 16, "community_name": "Community 16", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 16, "community_name": "Community 16", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 16, "community_name": "Community 16", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 233}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 99}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 69}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 53}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 53}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 51}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 39}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 39}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 36}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 34}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 32}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 31}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 27}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 27}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 26}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 26}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 22}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 21}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 20}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 18}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 17}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 17}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 17}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 16}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 16}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 13}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 12}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 11}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 10}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 9}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 8}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 8}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 7}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 6}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 6}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 6}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 6}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 6}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 6}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 6}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 5}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 4}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 3}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 3}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 3}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 3}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 3}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 3}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 2}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 2}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2}, {"cLine truncated
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 33, "community_name": "Community 33", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 33, "community_name": "Community 33", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 80, "community_name": "Community 80", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 81, "community_name": "Community 81", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 15, "community_name": "Community 15", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.7, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 15, "community_name": "Community 15", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 15, "community_name": "Community 15", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 223}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 87}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 69}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 53}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 53}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 51}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 48}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 44}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 39}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 36}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 30}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 28}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 27}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 27}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 27}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 22}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 20}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 20}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 19}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 19}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 19}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 17}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 17}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 17}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 16}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 15}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 13}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 12}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 11}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 10}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 9}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 8}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 8}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 7}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 7}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 6}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 6}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 6}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 6}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 6}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 6}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 6}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 6}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 5}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 4}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 3}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 3}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 3}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 3}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 3}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2}, {"Line truncated
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+19 -18
View File
@@ -17,8 +17,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779420283.3944924,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779420178.0237312,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779430756.3392074,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779425860.6226668,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -175,7 +175,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1778582866.2188683,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1779370638.1218963,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-payment-detail-readonly/index.vue": 1779006281.4220655,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779370582.0906003,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779431181.051139,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-dialog-bridge/index.vue": 1778582865.387537,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/index.ts": 1778582068.0037498,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/core/useFastEnter.ts": 1778582068.0014625,
@@ -215,14 +215,14 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material.ts": 1779283387.4196396,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception.ts": 1779255909.8553667,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1778582067.8610063,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779180968.908451,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779431182.2279022,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1778910234.3222458,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779269573.509064,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779329214.96505,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue": 1779161483.857836,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779370679.264584,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779424053.799563,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue": 1778938314.528914,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779371035.181769,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779423155.778682,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue": 1778978838.5026703,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1779313518.430975,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1779313530.5053055,
@@ -240,7 +240,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1779227504.1804457,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationAuditDialog.vue": 1779112125.8042517,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1779370624.9148946,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779371033.7577431,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779423240.754115,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue": 1779161484.445245,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/dashboard/console/index.vue": 1779227504.1947322,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/tenant/index.vue": 1779257191.2809517,
@@ -268,29 +268,30 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dict/index.vue": 1779259163.7560437,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/leave/index.vue": 1779257191.2768743,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/output-type/index.vue": 1779307917.5932443,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779341967.5417912,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779408337.9346838,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779423904.9098966,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779422386.4775605,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779309609.6630545,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779414898.4036016,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779307562.93293,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779361308.0826523,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779418710.3300052,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779408746.3297637,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779415778.1769066,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779414418.4825,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779426103.3002872,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779422456.542952,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779426096.2213817,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779422457.5458138,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue": 1779431206.6225035,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable-pack/index.vue": 1779307488.018346,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779307921.9798808,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779309524.0991938,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779423832.810978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779307716.2724373,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1778582068.070888,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779269560.7371643,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779431280.842699,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779269589.0865154,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779331328.5864854,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779423712.9489925,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779284232.1223023,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779333860.8573632,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779423489.553699,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779371036.8066387,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779285902.4727366,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779330250.9402645,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779423582.5553048,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/500/index.vue": 1778582068.0705533,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/403/index.vue": 1778582068.0697916,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/404/index.vue": 1778582068.0701847,
+4
View File
@@ -32,6 +32,10 @@ export function fetchUpdateProductionTaskStatus(id: string, data: Record<string,
return request.put({ url: `/web/admin/production-tasks/${id}/status`, params: data })
}
export function fetchDeleteProductionTask(id: string) {
return request.del({ url: `/web/admin/production-tasks/${id}` })
}
export function fetchAddTaskStep(taskId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/production-tasks/${taskId}/steps`, params: data })
}
+16 -1
View File
@@ -83,7 +83,22 @@ export type QcWorkOrderDetail = QcWorkOrder & {
itemName: string
feeAmount: number
}>
tasks?: Array<Record<string, unknown>>
tasks?: Array<{
id: string
taskNo: string
taskType: string
itemCode?: string
itemName?: string
companyId?: string
companyName?: string
feeAmount?: number
taskStatus: string
conclusion?: string
conclusionLabel?: string
remark?: string
completedTime?: string
attachmentFiles?: Array<{ id?: string; fileUrl?: string; fileName?: string }>
}>
}
export function fetchQcWorkOrderList(params?: {
@@ -120,7 +120,7 @@
/** 弹窗宽度,默认 720px */
width?: string
/** 字段栅格列数,默认 2 */
gridColumns?: 2 | 3
gridColumns?: 2 | 3 | 4
/** 挂到 body,用于嵌套在其它弹窗内时叠层展示 */
appendToBody?: boolean
/** 遮罩类名(可配合全局样式提高 z-index) */
@@ -240,6 +240,10 @@
&--cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
&--cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.view-kv {
@@ -301,15 +305,23 @@
margin-top: 12px;
}
@media (max-width: 1100px) {
.view-kv-grid--cols-4 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.view-kv-grid--cols-3 {
.view-kv-grid--cols-3,
.view-kv-grid--cols-4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.view-kv-grid,
.view-kv-grid--cols-3 {
.view-kv-grid--cols-3,
.view-kv-grid--cols-4 {
grid-template-columns: 1fr;
}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_stepdetailpanel_vue", "label": "StepDetailPanel.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L1"}, {"id": "production_work_order_stepdetailpanel_for", "label": "for()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L362"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_stepdetailpanel_vue", "target": "production_work_order_stepdetailpanel_for", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L362", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "push", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L364"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "decodeURIComponent", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "pop", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "split", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}, {"caller_nid": "production_work_order_stepdetailpanel_for", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue", "source_location": "L368"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "label": "DispatchTaskDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L1"}, {"id": "production_work_order_dispatchtaskdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L203"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "target": "production_work_order_dispatchtaskdialog_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L203", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_dispatchtaskdialog_if", "callee": "initEditor", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L207"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_taskexecutiondialog_vue", "label": "TaskExecutionDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "label": "qc-work-order.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L1"}, {"id": "api_qc_work_order_fetchqcworkorderlist", "label": "fetchQcWorkOrderList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L104"}, {"id": "api_qc_work_order_fetchqcworkorderdetail", "label": "fetchQcWorkOrderDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L116"}, {"id": "api_qc_work_order_fetchconfigureqcworkorder", "label": "fetchConfigureQcWorkOrder()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L122"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchqcworkorderlist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L104", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchqcworkorderdetail", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L116", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order_ts", "target": "api_qc_work_order_fetchconfigureqcworkorder", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L122", "weight": 1.0}], "raw_calls": [{"caller_nid": "api_qc_work_order_fetchqcworkorderlist", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L110"}, {"caller_nid": "api_qc_work_order_fetchqcworkorderdetail", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L117"}, {"caller_nid": "api_qc_work_order_fetchconfigureqcworkorder", "callee": "post", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts", "source_location": "L131"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_system_view_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1"}, {"id": "process_route_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1068"}, {"id": "process_route_index_get", "label": "get()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1269"}, {"id": "process_route_index_set", "label": "set()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1279"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L729", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolattachmentreadonly_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L730", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L731", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "vue_draggable_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L732", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L733", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L756", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1068", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1185", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1269", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_set", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1279", "weight": 1.0}], "raw_calls": [{"caller_nid": "process_route_index_if", "callee": "some", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1069"}, {"caller_nid": "process_route_index_if", "callee": "mapReagentOptionLabel", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1071"}, {"caller_nid": "process_route_index_if", "callee": "some", "source_file": "/UsLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_third_party_company_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1"}, {"id": "qc_work_order_index_handlereceptionattachmentpreview", "label": "handleReceptionAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L278"}, {"id": "qc_work_order_index_handlereceptionattachmentdownload", "label": "handleReceptionAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L283"}, {"id": "qc_work_order_index_taskattachmentfullurl", "label": "taskAttachmentFullUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L287"}, {"id": "qc_work_order_index_handletaskattachmentpreview", "label": "handleTaskAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L294"}, {"id": "qc_work_order_index_handletaskattachmentdownload", "label": "handleTaskAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L299"}, {"id": "qc_work_order_index_formatconfiguredlabitems", "label": "formatConfiguredLabItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L315"}, {"id": "qc_work_order_index_formatconfiguredthirditems", "label": "formatConfiguredThirdItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L321"}, {"id": "qc_work_order_index_buildtasktypesections", "label": "buildTaskTypeSections()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L329"}, {"id": "qc_work_order_index_buildconfiguredonlysection", "label": "buildConfiguredOnlySection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L388"}, {"id": "qc_work_order_index_formatdetectmethods", "label": "formatDetectMethods()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L406"}, {"id": "qc_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L413"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L278", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L283", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_taskattachmentfullurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L287", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handletaskattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L294", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handletaskattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue",Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_lab_test_item_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_taskoutputproductdialog_vue", "label": "TaskOutputProductDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "label": "DonationFormDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1"}, {"id": "list_donationformdialog_createemptytransport", "label": "createEmptyTransport()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1019"}, {"id": "list_donationformdialog_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1039"}, {"id": "list_donationformdialog_loadethnicoptions", "label": "loadEthnicOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1046"}, {"id": "list_donationformdialog_ensureethnicoptioninlist", "label": "ensureEthnicOptionInList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1057"}, {"id": "list_donationformdialog_samplerowlabel", "label": "sampleRowLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1085"}, {"id": "list_donationformdialog_resetform", "label": "resetForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1169"}, {"id": "list_donationformdialog_scrolltosection", "label": "scrollToSection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1224"}, {"id": "list_donationformdialog_normalizesamplerow", "label": "normalizeSampleRow()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1232"}, {"id": "list_donationformdialog_openbatchtransportdialog", "label": "openBatchTransportDialog()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1255"}, {"id": "list_donationformdialog_confirmbatchtransportapply", "label": "confirmBatchTransportApply()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1263"}, {"id": "list_donationformdialog_addsample", "label": "addSample()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1277"}, {"id": "list_donationformdialog_removesample", "label": "removeSample()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1291"}, {"id": "list_donationformdialog_handleopenforadd", "label": "handleOpenForAdd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1295"}, {"id": "list_donationformdialog_applydonationbundletoform", "label": "applyDonationBundleToForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1343"}, {"id": "list_donationformdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L1978"}, {"id": "list_donationformdialog_ensuredonationprotocolnoinoptions", "label": "ensureDonationProtocolNoInOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L2051"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue", "source_location": "L935", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationformdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "label": "useTaskStepEditor.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L1"}, {"id": "production_work_order_usetaskstepeditor_toapiflag", "label": "toApiFlag()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L28"}, {"id": "production_work_order_usetaskstepeditor_fromapiflag", "label": "fromApiFlag()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L32"}, {"id": "production_work_order_usetaskstepeditor_normalizestepsubtables", "label": "normalizeStepSubTables()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L35"}, {"id": "production_work_order_usetaskstepeditor_parseordernum", "label": "parseOrderNum()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L50"}, {"id": "production_work_order_usetaskstepeditor_usetaskstepeditor", "label": "useTaskStepEditor()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L57"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production_task", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_production", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L16", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L24", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L25", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_toapiflag", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L28", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_fromapiflag", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L32", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_normalizestepsubtables", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L35", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_usetaskstepeditor_ts", "target": "production_work_order_usetaskstepeditor_parseordernum", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts", "source_location": "L50", "weight": 1.0}, {"source": "users_wuzilong_woLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "label": "ProtocolFormDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolformdialog_createemptytransport", "label": "createEmptyTransport()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1287"}, {"id": "protocol_protocolformdialog_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1307"}, {"id": "protocol_protocolformdialog_loadethnicoptions", "label": "loadEthnicOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1314"}, {"id": "protocol_protocolformdialog_ensureethnicoptioninlist", "label": "ensureEthnicOptionInList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1325"}, {"id": "protocol_protocolformdialog_expressname", "label": "expressName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1353"}, {"id": "protocol_protocolformdialog_for", "label": "for()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L2021"}, {"id": "protocol_protocolformdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L2557"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1181", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1182", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_collection_protocol", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1194", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_business", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1200", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1201", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1202", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_ethnic", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue", "source_location": "L1215", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolformdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_store_modules_user", "relation": "imports_from", "confidence": "EXTRACTED", "sLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L1"}, {"id": "sample_reception_index_parseetalocaldate", "label": "parseEtaLocalDate()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L683"}, {"id": "sample_reception_index_startoflocalday", "label": "startOfLocalDay()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L697"}, {"id": "sample_reception_index_isetatodayorearlier", "label": "isEtaTodayOrEarlier()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L701"}, {"id": "sample_reception_index_samplereceptionrowclassname", "label": "sampleReceptionRowClassName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L707"}, {"id": "sample_reception_index_pad", "label": "pad()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L832"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_parseetalocaldate", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L683", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_startoflocalday", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L697", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_isetatodayorearlier", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L701", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_samplereceptionrowclassname", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L707", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_sample_reception_index_vue", "target": "sample_reception_index_pad", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L832", "weight": 1.0}, {"source": "sample_reception_index_isetatodayorearlier", "target": "sample_reception_index_parseetalocaldate", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L702", "weight": 1.0}, {"source": "sample_reception_index_isetatodayorearlier", "target": "sample_reception_index_startoflocalday", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L704", "weight": 1.0}, {"source": "sample_reception_index_samplereceptionrowclassname", "target": "sample_reception_index_isetatodayorearlier", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L709", "weight": 1.0}], "raw_calls": [{"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L684"}, {"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue", "source_location": "L684"}, {"caller_nid": "sample_reception_index_parseetalocaldate", "callee": "slice", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/srLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "label": "ProtocolViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324"}, {"id": "protocol_protocolviewdialog_handleclosed", "label": "handleClosed()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1643"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_dash", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_handleclosed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1643", "weight": 1.0}], "raw_calls": [{"caller_nid": "protocol_protocolviewdialog_dash", "callee": "unitName", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L324"}]}
@@ -1416,12 +1416,15 @@
activeSubTab.value = 'reagent'
processSearchKeyword.value = ''
configSuppressSave.value = true
stepList.value = []
stepListLoading.value = true
configDialogVisible.value = true
try {
await loadConfigReferenceData()
await loadRouteDetail()
} finally {
stepListLoading.value = false
await nextTick()
configSuppressSave.value = false
}
@@ -154,7 +154,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<div class="process-dialog-body min-h-[200px]">
<DialogFormBody :loading="dialogLoading" body-class="process-dialog-body min-h-[200px]">
<div class="detail-tabs">
<div
v-for="tab in formTabs"
@@ -482,7 +482,7 @@
</div>
</div>
</el-form>
</div>
</DialogFormBody>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
@@ -687,6 +687,7 @@
import { fetchItemUnitList } from '@/api/admin'
import { packTypeLabel, packTypeTagType } from '@/utils/status-tag-registry'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
defineOptions({ name: 'ProductionProcess' })
@@ -699,6 +700,7 @@
// 弹窗状态
const dialogVisible = ref(false)
const dialogLoading = ref(false)
const dialogTitle = ref('新增工艺')
const isEdit = ref(false)
const submitLoading = ref(false)
@@ -1018,13 +1020,15 @@
isEdit.value = false
dialogTitle.value = '新增工艺'
resetForm()
await loadReferenceData()
await searchItemUnitOptions('')
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
dialogLoading.value = true
nextTick(() => formRef.value?.clearValidate?.())
try {
await Promise.all([loadReferenceData(), searchItemUnitOptions('')])
} finally {
dialogLoading.value = false
nextTick(() => initDataCollectionSortable())
}
}
async function handleEdit(row: any) {
@@ -1032,9 +1036,14 @@
isEdit.value = true
dialogTitle.value = '编辑工艺'
editingId.value = row.id
await loadReferenceData()
dialogVisible.value = true
dialogLoading.value = true
nextTick(() => formRef.value?.clearValidate?.())
try {
const detail = await fetchProcessDetail(row.id)
const [, detail] = await Promise.all([
loadReferenceData(),
fetchProcessDetail(row.id)
])
formData.processCode = detail.processCode || ''
formData.processName = detail.processName || ''
formData.categoryId = detail.categoryId || ''
@@ -1067,12 +1076,10 @@
ensureDataCollectionUnitOptions()
} catch {
ElMessage.error('加载详情失败')
} finally {
dialogLoading.value = false
nextTick(() => initDataCollectionSortable())
}
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate?.()
initDataCollectionSortable()
})
}
async function handleSubmit() {
@@ -2,7 +2,9 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="phase === 'form' ? '派发生产任务' : `编辑生产任务 — ${createdTaskName || editTaskName || ''}`"
:title="
phase === 'form' ? '派发生产任务' : `编辑生产任务 — ${createdTaskName || editTaskName || ''}`
"
:width="phase === 'form' ? '560px' : '90vw'"
:class="phase === 'edit' ? 'dialog-form-layout dispatch-route-dialog' : 'dispatch-dialog'"
align-center
@@ -12,27 +14,60 @@
>
<!-- 阶段一:表单 -->
<div v-if="phase === 'form'" class="dispatch-dialog-work-order-info">
<span>工单号:<b>{{ workOrderRow?.workOrderNo }}</b></span>
<span
>工单号:<b>{{ workOrderRow?.workOrderNo }}</b></span
>
<span>协议号:{{ workOrderRow?.protocolNo || '—' }}</span>
<span>样本编码:{{ workOrderRow?.sampleCode || '—' }}</span>
</div>
<el-form v-if="phase === 'form'" ref="formRef" :model="form" :rules="rules" label-position="top" class="dialog-form-layout">
<el-form
v-if="phase === 'form'"
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="任务名称" prop="taskName" class="form-col-span-2">
<el-input v-model="form.taskName" placeholder="请输入任务名称" maxlength="200" />
</el-form-item>
<el-form-item label="工艺路线" class="form-col-span-2">
<template #label>工艺路线<span class="form-label-tip">可选,不选则从零创建工艺过程</span></template>
<el-select v-model="form.routeId" placeholder="请选择工艺路线(可选)" filterable clearable style="width: 100%">
<el-option v-for="route in routeOptions" :key="route.id"
:label="`${route.routeName}(${route.routeCode})`" :value="route.id" />
<template #label
>工艺路线<span class="form-label-tip">可选,不选则从零创建工艺过程</span></template
>
<el-select
v-model="form.routeId"
placeholder="请选择工艺路线(可选)"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="route in routeOptions"
:key="route.id"
:label="`${route.routeName}(${route.routeCode})`"
:value="route.id"
/>
</el-select>
</el-form-item>
<el-form-item label="描述" class="form-col-span-2">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="可选,填写任务描述" maxlength="500" />
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="可选,填写任务描述"
maxlength="500"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选,填写备注" maxlength="512" />
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="可选,填写备注"
maxlength="512"
/>
</el-form-item>
</div>
</el-form>
@@ -40,76 +75,121 @@
<template #footer>
<div v-if="phase === 'form'">
<el-button class="btn-ghost" @click="handleClose">取消</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask">生成任务</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask"
>生成任务</el-button
>
</div>
</template>
<!-- 阶段二:三栏编辑 -->
<div v-if="phase === 'edit'" class="route-config-body">
<div class="route-config-left">
<div class="route-config-left-header">
<h3>工艺库</h3>
<el-input v-model="editor.processSearchKeyword.value" placeholder="搜索工序" clearable size="small"
class="route-config-search" />
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item v-for="cat in editor.filteredProcessLibrary.value" :key="cat.categoryId"
:name="cat.categoryId">
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div v-for="(proc, procIndex) in cat.processes" :key="proc.id" class="route-config-process-card"
draggable="true" @dragstart="editor.onDragStart($event, proc)">
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty v-if="!editor.filteredProcessLibrary.value.length" description="暂无工艺数据" :image-size="48" />
<div class="route-config-left">
<div class="route-config-left-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
/>
</div>
<div class="route-config-center" v-loading="editor.stepListLoading.value" @dragover.prevent @drop="editor.onDropToCenter">
<div class="route-config-center-header">
<h3>工序步骤</h3>
<span class="route-config-step-count">共 {{ editor.stepList.value.length }} 步</span>
</div>
<div class="route-config-center-dropzone">
<draggable v-model="editor.stepList.value" handle=".route-config-step-handle" :animation="200"
@end="editor.onStepDragEnd">
<div v-for="(element, index) in editor.stepList.value" :key="element.id"
class="route-config-step-card" :class="{ 'is-active': editor.selectedStepId.value === element.id }"
@click="editor.selectStep(element)">
<div class="route-config-step-handle"><el-icon><Rank /></el-icon></div>
<div class="route-config-step-order">{{ index + 1 }}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-button link type="primary" size="small" title="复制步骤"
@click.stop="editor.handleCopyStep(element)">
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click.stop="editor.handleDeleteStep(element)">
<el-icon><Delete /></el-icon>
</el-button>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="route-config-center-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
</div>
<StepDetailPanel :editor="editor" />
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
<div
class="route-config-center"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<div class="route-config-center-header">
<h3>工序步骤</h3>
<span class="route-config-step-count">共 {{ editor.stepList.value.length }} 步</span>
</div>
<div class="route-config-center-dropzone">
<draggable
v-model="editor.stepList.value"
handle=".route-config-step-handle"
:animation="200"
@end="editor.onStepDragEnd"
>
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
class="route-config-step-card"
:class="{ 'is-active': editor.selectedStepId.value === element.id }"
@click="editor.selectStep(element)"
>
<div class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order">{{ index + 1 }}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="route-config-center-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
</div>
</div>
</div>
<StepDetailPanel :editor="editor" />
</div>
</el-dialog>
</template>
@@ -160,7 +240,9 @@
try {
const res = await fetchProcessRouteList({ page: 1, pageSize: 100 })
routeOptions.value = res.list || []
} catch { routeOptions.value = [] }
} catch {
routeOptions.value = []
}
}
function handleClose() {
@@ -193,25 +275,30 @@
await editor.initEditor(task.id)
ElMessage.success('任务已创建,可编辑步骤')
emit('success')
} catch { /* global error handling */ } finally {
} catch {
/* global error handling */
} finally {
submitting.value = false
}
}
watch(() => props.visible, async (val) => {
if (val) {
if (props.editTaskId) {
// 编辑已有任务:跳过阶段一,直接进入阶段二
createdTaskName.value = props.editTaskName || ''
phase.value = 'edit'
await editor.initEditor(props.editTaskId)
} else {
// 新建任务:走阶段一表单
phase.value = 'form'
loadRouteOptions()
watch(
() => props.visible,
async (val) => {
if (val) {
if (props.editTaskId) {
// 编辑已有任务:跳过阶段一,直接进入阶段二
createdTaskName.value = props.editTaskName || ''
phase.value = 'edit'
await editor.initEditor(props.editTaskId)
} else {
// 新建任务:走阶段一表单
phase.value = 'form'
loadRouteOptions()
}
}
}
})
)
</script>
<style scoped lang="scss">
@@ -0,0 +1,131 @@
<!-- 编辑生产任务基本信息弹窗 -->
<template>
<el-dialog
v-model="dialogVisible"
title="编辑任务基本信息"
width="560px"
destroy-on-close
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="任务名称" prop="taskName" class="form-col-span-2">
<el-input v-model="form.taskName" placeholder="请输入任务名称" maxlength="200" />
</el-form-item>
<el-form-item label="描述" class="form-col-span-2">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="可选,填写任务描述"
maxlength="500"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="可选,填写备注"
maxlength="512"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="handleClose">取消</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit"
>保存</el-button
>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
fetchProductionTaskDetail,
fetchUpdateProductionTask
} from '@/api/production-task'
import type { FormInstance, FormRules } from 'element-plus'
const props = defineProps<{
visible: boolean
taskId: string
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
(e: 'success'): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const formRef = ref<FormInstance>()
const submitting = ref(false)
const form = reactive({ taskName: '', description: '', remark: '' })
const rules: FormRules = {
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
}
async function loadTaskDetail() {
if (!props.taskId) return
try {
const res = await fetchProductionTaskDetail(props.taskId)
form.taskName = res.taskName || ''
form.description = res.description || ''
form.remark = res.remark || ''
} catch {
/* global error handling */
}
}
function handleClose() {
dialogVisible.value = false
form.taskName = ''
form.description = ''
form.remark = ''
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
submitting.value = true
try {
await fetchUpdateProductionTask(props.taskId, {
task_name: form.taskName,
description: form.description || undefined,
remark: form.remark || undefined
})
ElMessage.success('任务信息已更新')
emit('success')
handleClose()
} catch {
/* global error handling */
} finally {
submitting.value = false
}
}
watch(
() => props.visible,
async (val) => {
if (val && props.taskId) {
await loadTaskDetail()
}
}
)
</script>
@@ -6,13 +6,18 @@
<h3>步骤详情</h3>
<div v-if="!readonly" class="step-status-bar">
<template v-if="editor.mode.value === 'execution' && editor.selectedStep.value">
<el-tag :type="editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').type" size="small">
<el-tag
:type="editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').type"
size="small"
>
{{ editor.stepStatusTag(editor.selectedStep.value.stepStatus || 'dzx').label }}
</el-tag>
<el-button
v-if="editor.selectedStep.value.stepStatus === 'dzx'"
class="btn-op-item primary"
size="small"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@click="editor.handleStartStep(editor.selectedStep.value)"
>
<el-icon><VideoPlay /></el-icon>执行此步骤
@@ -22,6 +27,8 @@
class="btn-op-item"
size="small"
type="success"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@click="editor.handleCompleteStep(editor.selectedStep.value)"
>
<el-icon><Check /></el-icon>完成步骤
@@ -32,6 +39,7 @@
type="primary"
size="small"
:loading="editor.manualSaveLoading.value"
:disabled="editor.stepActionLoading.value"
@click="editor.handleManualSave"
>
<el-icon><Check /></el-icon>保存
@@ -45,9 +53,17 @@
<el-input v-model="editor.selectedStep.value.processName" />
</el-form-item>
<el-form-item label="工序分类">
<el-select v-model="editor.selectedStep.value.categoryId" placeholder="选择分类" clearable>
<el-option v-for="cat in editor.categoryOptions.value" :key="cat.id"
:label="cat.categoryName || cat.name" :value="cat.id" />
<el-select
v-model="editor.selectedStep.value.categoryId"
placeholder="选择分类"
clearable
>
<el-option
v-for="cat in editor.categoryOptions.value"
:key="cat.id"
:label="cat.categoryName || cat.name"
:value="cat.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备名称">
@@ -57,14 +73,27 @@
<el-input v-model="editor.selectedStep.value.estimatedDuration" placeholder="如 2h" />
</el-form-item>
<el-form-item label="描述" class="step-form-col-half">
<el-input v-model="editor.selectedStep.value.description" type="textarea" :rows="2"
placeholder="工序描述" />
<el-input
v-model="editor.selectedStep.value.description"
type="textarea"
:rows="2"
placeholder="工序描述"
/>
</el-form-item>
<el-form-item label="关联SOP" class="step-form-col-half">
<el-select v-model="editor.sopIdsArr.value" multiple placeholder="选择SOP文档" filterable
@change="editor.onSopIdsChange">
<el-option v-for="sop in editor.sopOptions.value" :key="sop.id"
:label="sop.sopName || sop.name" :value="sop.id" />
<el-select
v-model="editor.sopIdsArr.value"
multiple
placeholder="选择SOP文档"
filterable
@change="editor.onSopIdsChange"
>
<el-option
v-for="sop in editor.sopOptions.value"
:key="sop.id"
:label="sop.sopName || sop.name"
:value="sop.id"
/>
</el-select>
</el-form-item>
<div
@@ -87,8 +116,19 @@
<el-icon><Plus /></el-icon>添加试剂
</el-button>
</div>
<el-table :data="editor.selectedStep.value.reagents" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.reagents"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="试剂编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" />
@@ -96,12 +136,25 @@
</el-table-column>
<el-table-column label="试剂名称" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.reagentId" placeholder="请选择试剂" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.reagentOptionsLoading.value"
<el-select
v-model="row.reagentId"
placeholder="请选择试剂"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.reagentOptionsLoading.value"
:remote-method="editor.searchReagentOptions"
@visible-change="(vis: boolean) => vis && editor.searchReagentOptions('')"
@change="(val: string) => editor.onReagentRowSelect(row, val)">
<el-option v-for="item in editor.reagentOptions.value" :key="item.id" :label="item.label" :value="item.id" />
@change="(val: string) => editor.onReagentRowSelect(row, val)"
>
<el-option
v-for="item in editor.reagentOptions.value"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
@@ -112,21 +165,48 @@
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="实际用量" min-width="110" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="实际用量"
min-width="110"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.actualUsage" placeholder="实际用量" class="inner-cell-input" @change="editor.saveReagents" />
<el-input
v-model="row.actualUsage"
placeholder="实际用量"
class="inner-cell-input"
@change="editor.saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
@@ -142,59 +222,133 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeReagentRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeReagentRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.reagents?.length" description="暂无试剂" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.reagents?.length"
description="暂无试剂"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="耗材" name="consumable">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addConsumableRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="editor.addConsumableRow"
>
<el-icon><Plus /></el-icon>添加耗材
</el-button>
</div>
<el-table :data="editor.selectedStep.value.consumables" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.consumables"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="耗材编码" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" />
<el-input
v-model="row.consumableCode"
placeholder="编码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="耗材名称" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.consumableId" placeholder="请选择耗材" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.consumableOptionsLoading.value"
<el-select
v-model="row.consumableId"
placeholder="请选择耗材"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.consumableOptionsLoading.value"
:remote-method="editor.searchConsumableOptions"
@visible-change="(vis: boolean) => vis && editor.searchConsumableOptions('')"
@change="(val: string) => editor.onConsumableRowSelect(row, val)">
<el-option v-for="item in editor.consumableOptions.value" :key="item.id" :label="item.label" :value="item.id" />
@change="(val: string) => editor.onConsumableRowSelect(row, val)"
>
<el-option
v-for="item in editor.consumableOptions.value"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.specModel" placeholder="规格型号" class="inner-cell-input" />
<el-input
v-model="row.specModel"
placeholder="规格型号"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center">
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="实际用量" min-width="110" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="实际用量"
min-width="110"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.actualUsage" placeholder="实际用量" class="inner-cell-input" @change="editor.saveConsumables" />
<el-input
v-model="row.actualUsage"
placeholder="实际用量"
class="inner-cell-input"
@change="editor.saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
@@ -210,11 +364,21 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeConsumableRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeConsumableRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.consumables?.length" description="暂无耗材" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.consumables?.length"
description="暂无耗材"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="产出物" name="output">
@@ -223,87 +387,182 @@
<el-icon><Plus /></el-icon>添加产出物
</el-button>
</div>
<el-table :data="editor.selectedStep.value.outputs" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.outputs"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="产出类型" min-width="150" align="center">
<template #default="{ row }">
<el-select v-model="row.outputTypeId" placeholder="选择类型" filterable class="inner-cell-select"
@change="editor.saveOutputs">
<el-option v-for="ot in editor.outputTypeOptions.value" :key="ot.id"
:label="ot.typeName || ot.name" :value="ot.id" />
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
@change="editor.saveOutputs"
>
<el-option
v-for="ot in editor.outputTypeOptions.value"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="名称" min-width="150" align="center">
<template #default="{ row }">
<el-input v-model="row.outputName" placeholder="名称" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="条码" min-width="150" align="center">
<template #default="{ row }">
<el-input v-model="row.outputBarcode" placeholder="条码" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input"
@change="editor.saveOutputs" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="editor.saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeOutputRow($index)">删除</el-button>
<el-button link type="danger" size="small" @click="editor.removeOutputRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.outputs?.length" description="暂无产出物" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.outputs?.length"
description="暂无产出物"
:image-size="48"
/>
</el-tab-pane>
<el-tab-pane label="数据采集" name="dataCollection">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="editor.addDataCollectionRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="editor.addDataCollectionRow"
>
<el-icon><Plus /></el-icon>添加采集项
</el-button>
</div>
<el-table :data="editor.selectedStep.value.dataCollections" stripe class="inner-table" max-height="300">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table
:data="editor.selectedStep.value.dataCollections"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
width="56"
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="采集名称" min-width="180" align="center">
<template #default="{ row }">
<el-input v-model="row.collectionName" placeholder="采集项名称" class="inner-cell-input"
@change="editor.saveDataCollections" />
<el-input
v-model="row.collectionName"
placeholder="采集项名称"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center">
<template #default="{ row }">
<el-select v-model="row.unit" placeholder="请选择单位" clearable filterable remote
reserve-keyword class="inner-cell-select" :loading="editor.itemUnitOptionsLoading.value"
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="editor.itemUnitOptionsLoading.value"
:remote-method="editor.searchItemUnitOptions"
@visible-change="(vis: boolean) => vis && editor.searchItemUnitOptions('')"
@change="editor.saveDataCollections">
<el-option v-for="item in editor.itemUnitOptions.value" :key="item.value" :label="item.label" :value="item.value" />
@change="editor.saveDataCollections"
>
<el-option
v-for="item in editor.itemUnitOptions.value"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column v-if="editor.mode.value === 'execution'" label="采集值" min-width="140" align="center">
<el-table-column
v-if="editor.mode.value === 'execution'"
label="采集值"
min-width="140"
align="center"
>
<template #default="{ row }">
<el-input v-model="row.collectionValue" placeholder="输入采集值" class="inner-cell-input" @change="editor.saveDataCollections" />
<el-input
v-model="row.collectionValue"
placeholder="输入采集值"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input"
@change="editor.saveDataCollections" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="editor.saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeDataCollectionRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="editor.removeDataCollectionRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.selectedStep.value.dataCollections?.length" description="暂无采集项" :image-size="48" />
<el-empty
v-if="!editor.selectedStep.value.dataCollections?.length"
description="暂无采集项"
:image-size="48"
/>
</el-tab-pane>
</el-tabs>
</div>
@@ -364,8 +623,7 @@
files.push({
id: f.id || `${sop.id}_${f.fileUrl}`,
fileName:
f.fileName ||
decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
f.fileName || decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
fileUrl: f.fileUrl,
fileType: f.fileType || ''
})
@@ -12,9 +12,15 @@
<!-- 顶部:任务基本信息一行展示 -->
<div v-if="editor.taskDetail.value" class="execution-header">
<div class="execution-header-row">
<span class="execution-header-item">任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span>
<span class="execution-header-item">工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span>
<span class="execution-header-item">任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span>
<span class="execution-header-item"
>任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span
>
<span class="execution-header-item"
>工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span
>
<span class="execution-header-item"
>任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span
>
<el-tag :type="editor.taskStatusTag(editor.taskDetail.value.taskStatus).type">
{{ editor.taskStatusTag(editor.taskDetail.value.taskStatus).label }}
</el-tag>
@@ -24,6 +30,7 @@
class="btn-main"
type="primary"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleStartTask"
>
<el-icon><VideoPlay /></el-icon>开始执行
@@ -33,13 +40,16 @@
class="btn-main"
type="success"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleCompleteTask"
>
<el-icon><Check /></el-icon>完成任务
</el-button>
</div>
</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{ editor.taskDetail.value.remark }}</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{
editor.taskDetail.value.remark
}}</div>
</div>
<!-- 主体:可折叠工艺库 + 步骤列表 + 详情面板 -->
@@ -48,44 +58,79 @@
<template v-if="!readonly">
<transition name="slide-lib">
<div v-show="editor.processLibraryExpanded.value" class="execution-lib-panel">
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input v-model="editor.processSearchKeyword.value" placeholder="搜索工序" clearable size="small" class="route-config-search" />
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item v-for="cat in editor.filteredProcessLibrary.value" :key="cat.categoryId" :name="cat.categoryId">
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div v-for="(proc, procIndex) in cat.processes" :key="proc.id" class="route-config-process-card" draggable="true" @dragstart="editor.onDragStart($event, proc)">
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
/>
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty v-if="!cat.processes.length" description="该分类暂无工序" :image-size="32" />
</el-collapse-item>
</el-collapse>
<el-empty v-if="!editor.filteredProcessLibrary.value.length" description="暂无工艺数据" :image-size="48" />
</div>
</transition>
<el-empty
v-if="!cat.processes.length"
description="该分类暂无工序"
:image-size="32"
/>
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
</transition>
<!-- 工艺库折叠把手 -->
<div class="execution-lib-toggle" @click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value">
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
<!-- 工艺库折叠把手 -->
<div
class="execution-lib-toggle"
@click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value"
>
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
</template>
<!-- 步骤列表 -->
<div class="execution-step-list" v-loading="editor.stepListLoading.value" @dragover.prevent @drop="editor.onDropToCenter">
<div
class="execution-step-list"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<div class="execution-step-list-header">
<h3>工序步骤</h3>
<span class="execution-progress-badge">{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span>
<span class="execution-progress-badge"
>{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span
>
</div>
<div class="execution-step-list-body">
<!-- 虚拟步骤:任务输入 -->
@@ -96,9 +141,11 @@
>
<div class="virtual-step-icon virtual-step-icon--input">📥</div>
<div class="virtual-step-info">
<div class="virtual-step-name">任务输入</div>
<div class="virtual-step-name">原材料(样本/细胞产品)</div>
</div>
<el-tag v-if="editor.virtualInputStatus.value === 'filled'" type="success" size="small">已填写 {{ editor.taskInputs.value.length }} 项</el-tag>
<el-tag v-if="editor.virtualInputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskInputs.value.length }} 项</el-tag
>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
@@ -110,9 +157,11 @@
>
<div class="virtual-step-icon virtual-step-icon--output">📤</div>
<div class="virtual-step-info">
<div class="virtual-step-name">任务输出</div>
<div class="virtual-step-name">产出(细胞产品)</div>
</div>
<el-tag v-if="editor.virtualOutputStatus.value === 'filled'" type="success" size="small">已填写 {{ editor.taskOutputs.value.length }} 项</el-tag>
<el-tag v-if="editor.virtualOutputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskOutputs.value.length }} 项</el-tag
>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
@@ -120,7 +169,12 @@
<div class="step-divider"><span>工艺步骤</span></div>
<!-- 真实步骤列表 -->
<draggable v-model="editor.stepList.value" :handle="readonly ? undefined : '.route-config-step-handle'" :animation="200" @end="editor.onStepDragEnd">
<draggable
v-model="editor.stepList.value"
:handle="readonly ? undefined : '.route-config-step-handle'"
:animation="200"
@end="editor.onStepDragEnd"
>
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
@@ -131,18 +185,39 @@
}"
@click="editor.selectStep(element)"
>
<div v-if="!readonly" class="route-config-step-handle"><el-icon><Rank /></el-icon></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{ element.stepStatus === 'ywc' ? '✓' : index + 1 }}</div>
<div v-if="!readonly" class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{
element.stepStatus === 'ywc' ? '✓' : index + 1
}}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-tag v-if="element.stepStatus" :type="editor.stepStatusTag(element.stepStatus).type" size="small" style="margin-right:4px">{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag>
<el-tag
v-if="element.stepStatus"
:type="editor.stepStatusTag(element.stepStatus).type"
size="small"
style="margin-right: 4px"
>{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag
>
<template v-if="!readonly">
<el-button link type="primary" size="small" title="复制步骤" @click.stop="editor.handleCopyStep(element)">
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click.stop="editor.handleDeleteStep(element)">
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
@@ -161,116 +236,62 @@
<!-- 任务输入详情 -->
<template v-if="editor.selectedVirtualStep.value === 'input'">
<div class="execution-detail-header">
<h3>📥 任务输入</h3>
<h3>📥 原材料(样本/细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button class="btn-main" type="primary" size="small" @click="editor.addTaskInputRow"><el-icon><Plus /></el-icon>添加输入</el-button>
<el-button class="btn-main" type="primary" size="small" :loading="editor.inputSaving.value" @click="editor.saveTaskInputs"><el-icon><Check /></el-icon>保存</el-button>
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.inputSaving.value"
@click="editor.saveTaskInputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
<div class="execution-detail-body">
<el-table :data="editor.taskInputs.value" stripe class="inner-table">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table-column label="输入类型" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.inputType === 'sample' ? '样本' : row.inputType === 'cell_product' ? '细胞产品' : row.inputType }}</span>
<el-select v-else v-model="row.inputType" placeholder="选择类型" class="inner-cell-select" @change="() => { row.inputId = ''; row.inputCode = ''; row.inputName = '' }">
<el-option label="样本" value="sample" />
<el-option label="细胞产品" value="cell_product" />
</el-select>
</template>
</el-table-column>
<el-table-column label="输入编码" min-width="150" align="center">
<template #default="{ row }">
<template v-if="row.inputType === 'cell_product'">
<el-select
v-if="!readonly"
v-model="row.inputId"
placeholder="选择细胞产品"
filterable
class="inner-cell-select"
:loading="editor.cellProductOptionsLoading.value"
@change="(val: string) => editor.onCellProductInputSelect(row, val)"
>
<el-option v-for="opt in editor.cellProductOptions.value" :key="opt.id" :label="opt.label" :value="opt.id" />
</el-select>
<span v-else>{{ row.inputCode || '—' }}</span>
</template>
<template v-else>
<span>{{ row.inputCode || '—' }}</span>
</template>
</template>
</el-table-column>
<el-table-column label="输入名称" min-width="150" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.inputName || '—' }}</span>
<el-input v-else v-model="row.inputName" placeholder="输入名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.remark || '—' }}</span>
<el-input v-else v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeTaskInputRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.taskInputs.value.length" description="暂无输入项" :image-size="48" />
<TaskInputEnhancedTable
:items="editor.taskInputs.value"
:loading="false"
:disabled="readonly"
:sample-code="editor.taskDetail.value?.sampleCode || ''"
:cell-product-options="editor.cellProductOptions.value"
:cell-product-options-loading="editor.cellProductOptionsLoading.value"
@update:items="(items: Record<string, any>[]) => (editor.taskInputs.value = items)"
@search-cell-product="(q: string) => editor.searchCellProductOptions(q)"
@save="editor.saveTaskInputs"
/>
</div>
</template>
<!-- 任务输出详情 -->
<template v-else-if="editor.selectedVirtualStep.value === 'output'">
<div class="execution-detail-header">
<h3>📤 任务输出</h3>
<h3>📤 产出(细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button class="btn-main" type="primary" size="small" @click="editor.addTaskOutputRow"><el-icon><Plus /></el-icon>添加输出</el-button>
<el-button class="btn-main" type="primary" size="small" :loading="editor.outputSaving.value" @click="editor.saveTaskOutputs"><el-icon><Check /></el-icon>保存</el-button>
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.outputSaving.value"
@click="editor.saveTaskOutputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
<div class="execution-detail-body">
<el-table :data="editor.taskOutputs.value" stripe class="inner-table">
<el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
<el-table-column label="输出名称" min-width="140" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.outputName || '—' }}</span>
<el-input v-else v-model="row.outputName" placeholder="输出名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="输出条码" min-width="140" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.outputBarcode || '—' }}</span>
<el-input v-else v-model="row.outputBarcode" placeholder="扫码或输入" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="数量" min-width="100" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.quantity ?? '—' }}</span>
<el-input-number v-else v-model="row.quantity" :min="0" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="单位" min-width="80" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.unit || '—' }}</span>
<el-input v-else v-model="row.unit" placeholder="单位" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center">
<template #default="{ row }">
<span v-if="readonly">{{ row.remark || '—' }}</span>
<el-input v-else v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="editor.removeTaskOutputRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!editor.taskOutputs.value.length" description="暂无输出项" :image-size="48" />
<TaskOutputList
:items="editor.taskOutputs.value"
:loading="false"
:product-type-options="editor.productTypeOptions.value"
:spec-options="editor.specOptions.value"
:storage-slot-options="editor.storageSlotOptions.value"
:unit-options="editor.itemUnitOptions.value"
:unit-options-loading="editor.itemUnitOptionsLoading.value"
:disabled="readonly"
@update:items="(items: Record<string, any>[]) => (editor.taskOutputs.value = items)"
@search-unit="(q: string) => editor.searchItemUnitOptions(q)"
@save="editor.saveTaskOutputs"
/>
</div>
</template>
@@ -288,6 +309,8 @@
import { VueDraggable as draggable } from 'vue-draggable-plus'
import { useTaskStepEditor } from './useTaskStepEditor'
import StepDetailPanel from './StepDetailPanel.vue'
import TaskInputEnhancedTable from './TaskInputEnhancedTable.vue'
import TaskOutputList from './TaskOutputList.vue'
const props = defineProps<{
visible: boolean
@@ -319,11 +342,14 @@
return 'seq-pending'
}
watch(() => props.visible, async (val) => {
if (val && props.taskId) {
await editor.initEditor(props.taskId, { mode: 'execution' })
watch(
() => props.visible,
async (val) => {
if (val && props.taskId) {
await editor.initEditor(props.taskId, { mode: 'execution' })
}
}
})
)
</script>
<style scoped lang="scss">
@@ -349,135 +375,280 @@
border: 1px solid #d1fae5;
}
.execution-header-row {
display: flex; align-items: center; gap: 16px;
font-size: 13px; color: #475569;
b { color: #1e293b; }
display: flex;
align-items: center;
gap: 16px;
font-size: 13px;
color: #475569;
b {
color: #1e293b;
}
}
.execution-header-item {
white-space: nowrap;
}
.execution-header-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.execution-header-remark {
margin-top: 4px;
font-size: 12px;
color: #64748b;
}
.execution-header-item { white-space: nowrap; }
.execution-header-actions { display: flex; gap: 8px; margin-left: auto; }
.execution-header-remark { margin-top: 4px; font-size: 12px; color: #64748b; }
/* 主体三区域 */
.execution-main {
display: flex; gap: 0;
display: flex;
gap: 0;
height: calc(96vh - 110px);
min-height: 400px;
}
/* 工艺库折叠侧栏 */
.execution-lib-panel {
width: 260px; flex-shrink: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.execution-lib-header {
padding: 10px 12px; border-bottom: 1px solid #e5e7eb; flex-shrink: 0;
h3 { margin: 0 0 6px; font-size: 13px; font-weight: 600; color: #1f2937; }
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
}
.slide-lib-enter-active, .slide-lib-leave-active {
transition: width 0.2s ease, opacity 0.2s ease;
.slide-lib-enter-active,
.slide-lib-leave-active {
transition:
width 0.2s ease,
opacity 0.2s ease;
overflow: hidden;
}
.slide-lib-enter-from, .slide-lib-leave-to {
width: 0 !important; opacity: 0;
.slide-lib-enter-from,
.slide-lib-leave-to {
width: 0 !important;
opacity: 0;
}
/* 工艺库折叠把手 */
.execution-lib-toggle {
width: 24px; flex-shrink: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 4px; cursor: pointer;
background: #f9fafb; border-left: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;
width: 24px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
background: #f9fafb;
border-left: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
transition: background 0.15s;
font-size: 14px; color: #9ca3af;
&:hover { background: #ecfdf5; color: #10b981; }
font-size: 14px;
color: #9ca3af;
&:hover {
background: #ecfdf5;
color: #10b981;
}
}
.execution-lib-toggle-text {
writing-mode: vertical-lr; font-size: 11px; letter-spacing: 2px;
writing-mode: vertical-lr;
font-size: 11px;
letter-spacing: 2px;
}
/* 步骤列表 */
.execution-step-list {
width: 340px; flex-shrink: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
overflow: hidden; margin: 0 8px;
width: 340px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin: 0 8px;
}
.execution-step-list-header {
padding: 10px 14px; border-bottom: 1px solid #e5e7eb;
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #1f2937; }
padding: 10px 14px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
}
.execution-progress-badge {
font-size: 12px; color: #6b7280; background: #f3f4f6;
padding: 2px 10px; border-radius: 10px;
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 2px 10px;
border-radius: 10px;
}
.execution-step-list-body {
flex: 1; overflow-y: auto; padding: 8px;
flex: 1;
overflow-y: auto;
padding: 8px;
}
.execution-step-list-empty {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 40px 0; color: #9ca3af;
p { margin-top: 8px; font-size: 13px; }
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #9ca3af;
p {
margin-top: 8px;
font-size: 13px;
}
}
/* 虚拟步骤卡片 */
/* 虚拟步骤卡片(原材料 / 产出 — 红色系强调必填) */
.virtual-step-card {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; margin-bottom: 4px;
border-radius: 8px; cursor: pointer;
border: 2px solid #bfdbfe; background: #eff6ff;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
border: 2px solid #fecaca;
background: #fef2f2;
transition: all 0.15s;
&:hover { border-color: #93c5fd; background: #dbeafe; }
&.is-active { border-color: #3b82f6; background: #dbeafe; box-shadow: 0 0 0 1px #3b82f6; }
&:hover {
border-color: #fca5a5;
background: #fee2e2;
}
&.is-active {
border-color: #ef4444;
background: #fee2e2;
box-shadow: 0 0 0 1px #ef4444;
}
}
.virtual-step-icon {
width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; flex-shrink: 0;
&--input { background: #dbeafe; }
&--output { background: #fef3c7; }
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
&--input,
&--output {
background: #fecaca;
}
}
.virtual-step-required {
margin-left: 8px;
font-size: 11px;
font-weight: 500;
color: #dc2626;
padding: 1px 6px;
border-radius: 4px;
background: #fee2e2;
border: 1px solid #fecaca;
}
.virtual-step-info {
flex: 1;
min-width: 0;
}
.virtual-step-name {
font-size: 13px;
font-weight: 500;
color: #1e293b;
}
.virtual-step-info { flex: 1; min-width: 0; }
.virtual-step-name { font-size: 13px; font-weight: 500; color: #1e293b; }
/* 分割线 */
.step-divider {
display: flex; align-items: center; gap: 8px;
margin: 8px 0; padding: 0 4px;
color: #9ca3af; font-size: 11px;
&::before, &::after { content: ''; flex: 1; height: 1px; background: #e5e7eb; }
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
padding: 0 4px;
color: #9ca3af;
font-size: 11px;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
}
/* 步骤序号状态 */
.seq-done { background: #d1fae5 !important; color: #059669 !important; }
.seq-running { background: #10b981 !important; color: #fff !important; }
.seq-pending { background: #f3f4f6 !important; color: #6b7280 !important; }
.route-config-step-card.is-done { opacity: 0.6; }
.seq-done {
background: #d1fae5 !important;
color: #059669 !important;
}
.seq-running {
background: #10b981 !important;
color: #fff !important;
}
.seq-pending {
background: #f3f4f6 !important;
color: #6b7280 !important;
}
.route-config-step-card.is-done {
opacity: 0.6;
}
/* 右侧详情面板 */
.execution-detail-panel {
flex: 1; min-width: 0;
display: flex; flex-direction: column;
border: 1px solid #e5e7eb; border-radius: 8px;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.execution-detail-header {
padding: 10px 16px; border-bottom: 1px solid #e5e7eb;
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #1e293b; }
padding: 10px 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
}
.execution-detail-actions {
display: flex;
gap: 8px;
}
.execution-detail-actions { display: flex; gap: 8px; }
.execution-detail-body {
flex: 1; overflow-y: auto; padding: 16px;
flex: 1;
overflow-y: auto;
padding: 16px;
}
.section-bar {
display: inline-block; width: 3px; height: 14px;
background: #10b981; border-radius: 2px; margin-right: 8px;
display: inline-block;
width: 3px;
height: 14px;
background: #10b981;
border-radius: 2px;
margin-right: 8px;
}
</style>
@@ -0,0 +1,361 @@
<template>
<div class="task-input-table">
<!-- 工具栏 -->
<div class="panel-top">
<h2 class="title-bar-heading">原材料(样本/细胞产品)</h2>
<div v-if="!disabled" class="panel-top-actions">
<el-button type="primary" size="small" @click="handleAddSample">
<el-icon><Plus /></el-icon>
添加样本
</el-button>
<el-button size="small" @click="handleAddCellProduct">
<el-icon><Plus /></el-icon>
添加细胞产品
</el-button>
</div>
</div>
<!-- 列表 -->
<el-table :data="items" size="small" stripe border v-loading="loading" empty-text="暂无原材料">
<el-table-column type="index" label="序号" width="56" align="center" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="inputTypeTagType(row.inputType)" size="small">
{{ row.inputType === 'cell_product' ? '细胞产品' : '样本' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="编码" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.inputType === 'sample' && !disabled">
<el-input v-model="row.inputCode" placeholder="请输入编码" />
</template>
<template v-else>{{ row.inputCode || row.productCode || '—' }}</template>
</template>
</el-table-column>
<el-table-column label="名称" min-width="120" align="center" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.inputType === 'sample' && !disabled">
<el-input v-model="row.inputName" placeholder="请输入名称" />
</template>
<template v-else>{{ row.inputName || row.productName || '—' }}</template>
</template>
</el-table-column>
<!-- 细胞产品特有字段 -->
<el-table-column label="产品类型" min-width="90" show-overflow-tooltip>
<template #default="{ row }">
{{ row.productTypeName || '—' }}
</template>
</el-table-column>
<el-table-column label="代次" width="70" align="center">
<template #default="{ row }">
{{ row.passage || '—' }}
</template>
</el-table-column>
<el-table-column label="储物格位置" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.storageSlotName || '—' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<template v-if="row.inputType === 'cell_product'">
<el-tag :type="inputStatusTagType(row.productStatus)" size="small">
{{ inputStatusLabel(row.productStatus) }}
</el-tag>
</template>
<template v-else>—</template>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="!disabled">
<el-input v-model="row.remark" placeholder="备注" />
</template>
<template v-else>{{ row.remark || '—' }}</template>
</template>
</el-table-column>
<el-table-column v-if="!disabled" label="操作" width="80" fixed="right" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="handleDelete($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 细胞产品选择弹窗 -->
<el-dialog
v-model="selectDialogVisible"
title="选择细胞产品"
width="1160px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div class="select-dialog-body">
<!-- 搜索框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索产品编码或名称"
clearable
style="margin-bottom: 12px"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<!-- 可选列表 -->
<div class="list-body list-wrap">
<el-table
ref="selectTableRef"
class="list-table"
:data="filteredCellProducts"
stripe
row-key="id"
height="380"
v-loading="cellProductOptionsLoading"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="45"
align="center"
:selectable="isRowSelectable"
/>
<el-table-column
label="产品编码"
prop="productCode"
min-width="210"
show-overflow-tooltip
/>
<el-table-column
label="产品名称"
prop="productName"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
label="产品类型"
prop="productTypeName"
min-width="90"
show-overflow-tooltip
/>
<el-table-column label="代次" prop="passage" width="70" align="center" />
<el-table-column
label="储物格位置"
prop="storageSlotName"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="inputStatusTagType(row.productStatus)" size="small">
{{ inputStatusLabel(row.productStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<el-button @click="selectDialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="!selectedRows.length" @click="confirmSelect">
确定(已选 {{ selectedRows.length }} 项)
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue'
defineOptions({ name: 'TaskInputEnhancedTable' })
const props = withDefaults(
defineProps<{
items: Record<string, any>[]
loading: boolean
disabled?: boolean
sampleCode?: string
cellProductOptions: {
id: string
label: string
productCode?: string
productName?: string
productTypeName?: string
passage?: string
storageSlotName?: string
productStatus?: string
}[]
cellProductOptionsLoading: boolean
}>(),
{ disabled: false, sampleCode: '' }
)
const emit = defineEmits<{
'update:items': [items: Record<string, any>[]]
searchCellProduct: [query: string]
save: []
}>()
const selectDialogVisible = ref(false)
const selectTableRef = ref()
const searchKeyword = ref('')
const selectedRows = ref<Record<string, any>[]>([])
/** 样本 success(绿),细胞产品 warning(琥珀),避免双绿相近 */
function inputTypeTagType(inputType: string) {
return inputType === 'cell_product' ? 'warning' : 'success'
}
function inputStatusTagType(status: string) {
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
voided: 'danger',
thawed: 'info'
}
return map[status] || 'info'
}
function inputStatusLabel(status: string) {
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
voided: '已作废',
thawed: '已复苏'
}
return map[status] || status || '—'
}
const filteredCellProducts = computed(() => {
const keyword = searchKeyword.value?.trim().toLowerCase() || ''
if (!keyword) return props.cellProductOptions
return props.cellProductOptions.filter(
(cp) =>
(cp.productCode || '').toLowerCase().includes(keyword) ||
(cp.productName || '').toLowerCase().includes(keyword) ||
(cp.label || '').toLowerCase().includes(keyword)
)
})
function isRowSelectable(row: Record<string, any>) {
const existingIds = new Set(
props.items.filter((i) => i.inputType === 'cell_product').map((i) => i.inputId)
)
return !existingIds.has(row.id)
}
function handleSearch() {
emit('searchCellProduct', searchKeyword.value)
}
function handleAddSample() {
const newItems = [...props.items]
// 自动填入样本编码:如果工单关联了样本编码,且尚未添加过,则自动填入
let autoCode = ''
const code = (props.sampleCode || '').trim()
if (code) {
const alreadyAdded = props.items.some(
(i) => i.inputType === 'sample' && (i.inputCode || '').trim() === code
)
if (!alreadyAdded) {
autoCode = code
}
}
newItems.push({
inputType: 'sample',
inputId: '',
inputCode: autoCode,
inputName: '',
remark: ''
})
emit('update:items', newItems)
}
function handleAddCellProduct() {
searchKeyword.value = ''
selectedRows.value = []
selectDialogVisible.value = true
emit('searchCellProduct', '')
}
function handleSelectionChange(rows: Record<string, any>[]) {
selectedRows.value = rows
}
function confirmSelect() {
if (!selectedRows.value.length) return
const existingIds = new Set(
props.items.filter((i) => i.inputType === 'cell_product').map((i) => i.inputId)
)
const newItems = [...props.items]
let addedCount = 0
for (const row of selectedRows.value) {
if (existingIds.has(row.id)) continue
newItems.push({
inputType: 'cell_product',
inputId: row.id,
inputCode: row.productCode || '',
inputName: row.productName || '',
productCode: row.productCode || '',
productName: row.productName || '',
productTypeName: row.productTypeName || '',
passage: row.passage || '',
storageSlotName: row.storageSlotName || '',
productStatus: row.productStatus || '',
remark: ''
})
addedCount++
}
emit('update:items', newItems)
selectDialogVisible.value = false
if (addedCount === 0) {
ElMessage.info('所选细胞产品已在列表中')
}
}
async function handleDelete(index: number) {
await ElMessageBox.confirm('确定要删除该原材料吗?', '提示', {
type: 'warning'
})
const newItems = [...props.items]
newItems.splice(index, 1)
emit('update:items', newItems)
}
</script>
<style lang="scss" scoped>
.task-input-table {
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.panel-top-actions {
display: flex;
gap: 10px;
}
}
.select-dialog-body {
padding: 0 4px;
}
// 样本行输入框高度适配
:deep(.el-table__cell) {
.el-input {
.el-input__wrapper {
min-height: 32px;
}
}
}
}
</style>
@@ -0,0 +1,177 @@
<template>
<div class="task-output-list">
<!-- 工具栏 -->
<div class="panel-top">
<h2 class="title-bar-heading">产出(细胞产品)</h2>
<el-button v-if="!disabled" type="primary" size="small" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增产出
</el-button>
</div>
<!-- 列表 -->
<el-table
:data="items"
size="small"
stripe
border
v-loading="loading"
empty-text="暂无产出产品"
>
<el-table-column type="index" label="序号" width="56" align="center" />
<el-table-column label="产品编码" prop="productCode" min-width="140" show-overflow-tooltip />
<el-table-column label="产品名称" prop="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="产品类型" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ row.productTypeName || '—' }}
</template>
</el-table-column>
<el-table-column label="规格" min-width="80" show-overflow-tooltip>
<template #default="{ row }">
{{ row.specName || '—' }}
</template>
</el-table-column>
<el-table-column label="代次" prop="passage" width="70" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="110" show-overflow-tooltip />
<el-table-column label="储物格位置" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.storageSlotName || '—' }}
</template>
</el-table-column>
<el-table-column label="数量" width="80" align="center">
<template #default="{ row }">
{{ row.quantity ?? '—' }}
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="70" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.productStatus)" size="small">
{{ statusLabel(row.productStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!disabled" label="操作" width="140" fixed="right" align="center">
<template #default="{ row, $index }">
<el-button type="primary" link size="small" @click="handleEdit(row, $index)">
编辑
</el-button>
<el-button type="danger" link size="small" @click="handleDelete($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑弹窗 -->
<TaskOutputProductDialog
v-model="dialogVisible"
:edit-item="editingItem"
:product-type-options="productTypeOptions"
:spec-options="specOptions"
:storage-slot-options="storageSlotOptions"
:unit-options="unitOptions"
:unit-options-loading="unitOptionsLoading"
@confirm="handleDialogConfirm"
@search-unit="(q: string) => emit('searchUnit', q)"
/>
</div>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import TaskOutputProductDialog from './TaskOutputProductDialog.vue'
defineOptions({ name: 'TaskOutputList' })
const props = withDefaults(
defineProps<{
items: Record<string, any>[]
loading: boolean
productTypeOptions: Array<{ id: string; productTypeName: string }>
specOptions: Array<{ id: string; specName: string }>
storageSlotOptions: Array<{ id: string; name: string }>
unitOptions: { label: string; value: string }[]
unitOptionsLoading: boolean
disabled?: boolean
}>(),
{ disabled: false }
)
const emit = defineEmits<{
'update:items': [items: Record<string, any>[]]
searchUnit: [query: string]
save: []
}>()
const dialogVisible = ref(false)
const editingItem = ref<Record<string, any> | null>(null)
const editingIndex = ref(-1)
function statusTagType(status: string) {
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
voided: 'danger',
thawed: 'info'
}
return map[status] || 'info'
}
function statusLabel(status: string) {
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
voided: '已作废',
thawed: '已复苏'
}
return map[status] || status || '—'
}
function handleAdd() {
editingItem.value = null
editingIndex.value = -1
dialogVisible.value = true
}
function handleEdit(row: Record<string, any>, index: number) {
editingItem.value = { ...row }
editingIndex.value = index
dialogVisible.value = true
}
async function handleDelete(index: number) {
await ElMessageBox.confirm('确定要删除该产出产品吗?删除后将在保存时生效。', '提示', {
type: 'warning'
})
const newItems = [...props.items]
newItems.splice(index, 1)
emit('update:items', newItems)
}
function handleDialogConfirm(data: Record<string, any>) {
const newItems = [...props.items]
if (editingIndex.value >= 0) {
// 编辑:替换原有项,保留 cellProductId
newItems[editingIndex.value] = {
...newItems[editingIndex.value],
...data
}
} else {
// 新增:添加到列表
newItems.push(data)
}
emit('update:items', newItems)
}
</script>
<style lang="scss" scoped>
.task-output-list {
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
}
</style>
@@ -0,0 +1,654 @@
<template>
<el-dialog
:model-value="modelValue"
:title="isEdit ? '编辑产出产品' : '新增产出产品'"
width="1000px"
class="dialog-form-layout task-output-product-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="task-output-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-3">
<!-- 产品编码 -->
<el-form-item label="产品编码" prop="productCode">
<el-input v-model="formData.productCode" placeholder="请输入产品编码" />
</el-form-item>
<!-- 产品名称 -->
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" />
</el-form-item>
<!-- 产品类型 -->
<el-form-item label="产品类型">
<el-select
v-model="formData.productTypeId"
placeholder="请选择产品类型"
clearable
style="width: 100%"
>
<el-option
v-for="item in productTypeOptions"
:key="item.id"
:label="item.productTypeName"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 规格 -->
<el-form-item label="规格">
<el-select
v-model="formData.specId"
placeholder="请选择规格"
clearable
style="width: 100%"
>
<el-option
v-for="item in specOptions"
:key="item.id"
:label="item.specName"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 代次 -->
<el-form-item label="代次">
<el-select
v-model="formData.passage"
placeholder="请选择代次"
clearable
style="width: 100%"
>
<el-option v-for="p in passageOptions" :key="p" :label="p" :value="p" />
</el-select>
</el-form-item>
<!-- 批次号 -->
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="formData.batchNo" placeholder="请输入批次号" />
</el-form-item>
<!-- 储物格位置 -->
<el-form-item label="储物格位置">
<el-select
v-model="formData.storageSlotId"
placeholder="请选择储物格"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in storageSlotOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 预计细胞数量 -->
<el-form-item label="预计细胞数量(亿)">
<el-input-number
v-model="formData.expectedCellCount"
:min="0"
:precision="4"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 活性 -->
<el-form-item label="活性(%)">
<el-input-number
v-model="formData.viability"
:min="0"
:max="100"
:precision="1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 冻存日期 -->
<el-form-item label="冻存日期">
<el-date-picker
v-model="formData.freezeDate"
type="date"
placeholder="请选择冻存日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<!-- 数量 -->
<el-form-item label="数量">
<el-input-number
v-model="formData.quantity"
:min="0"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<!-- 单位 -->
<el-form-item label="单位">
<el-select
v-model="formData.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
style="width: 100%"
:loading="unitOptionsLoading"
:remote-method="(q: string) => emit('searchUnit', q)"
@visible-change="(visible: boolean) => visible && emit('searchUnit', '')"
>
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 有效期 -->
<el-form-item label="有效期">
<el-date-picker
v-model="formData.expiryDate"
type="date"
placeholder="请选择有效期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<!-- 产品图片 (full width) -->
<el-form-item label="产品图片" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="productImageUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*"
@change="handleProductImageChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传图片</span>
</div>
</el-upload>
<div
v-for="(file, index) in productImageFiles"
:key="file.id || index"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="
file.fileUrl && !file._uploading ? handlePreviewProductImage(file) : undefined
"
>
<template v-if="file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else>
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click="handleDownloadProductImage(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="removeProductImage(index)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片上传,单文件不超过 10MB</span>
</div>
</el-form-item>
<!-- 备注 (full width) -->
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<!-- 图片预览弹窗 -->
<el-dialog v-model="previewVisible" title="图片预览" width="800px" align-center destroy-on-close>
<div class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { Plus, Close, Check, Delete, Download, Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'TaskOutputProductDialog' })
type ProductImageFile = {
id?: string
fileName: string
fileUrl: string
fileType?: string
_uploading?: boolean
}
const props = defineProps<{
modelValue: boolean
editItem?: Record<string, any> | null
productTypeOptions: Array<{ id: string; productTypeName: string }>
specOptions: Array<{ id: string; specName: string }>
storageSlotOptions: Array<{ id: string; name: string }>
unitOptions: { label: string; value: string }[]
unitOptionsLoading: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: [data: Record<string, any>]
searchUnit: [query: string]
}>()
const passageOptions = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8']
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
return suffix
return `${minioBaseUrl}/${suffix}`
}
const isEdit = computed(() => !!props.editItem?.cellProductId)
const formRef = ref()
const submitLoading = ref(false)
const productImageUploadRef = ref()
const previewVisible = ref(false)
const previewUrl = ref('')
const productImageFiles = ref<ProductImageFile[]>([])
const formData = reactive({
cellProductId: '',
productCode: '',
productName: '',
productTypeId: '',
specId: '',
passage: '',
batchNo: '',
storageSlotId: '',
expectedCellCount: undefined as number | undefined,
viability: undefined as number | undefined,
freezeDate: '',
quantity: 1 as number,
unit: '',
expiryDate: '',
productImages: '',
remark: ''
})
const formRules = {
productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
}
// Watch editItem to populate form
watch(
() => props.editItem,
(item) => {
if (item) {
formData.cellProductId = item.cellProductId || ''
formData.productCode = item.productCode || ''
formData.productName = item.productName || ''
formData.productTypeId = item.productTypeId || ''
formData.specId = item.specId || ''
formData.passage = item.passage || ''
formData.batchNo = item.batchNo || ''
formData.storageSlotId = item.storageSlotId || ''
formData.expectedCellCount = item.expectedCellCount ?? undefined
formData.viability = item.viability ?? undefined
formData.freezeDate = item.freezeDate || ''
formData.quantity = item.quantity ?? 1
formData.unit = item.unit || ''
formData.expiryDate = item.expiryDate || ''
formData.productImages = item.productImages || ''
formData.remark = item.remark || ''
syncFormToImages(item.productImages || '')
} else {
resetForm()
}
},
{ immediate: true }
)
function resetForm() {
formData.cellProductId = ''
formData.productCode = ''
formData.productName = ''
formData.productTypeId = ''
formData.specId = ''
formData.passage = ''
formData.batchNo = ''
formData.storageSlotId = ''
formData.expectedCellCount = undefined
formData.viability = undefined
formData.freezeDate = ''
formData.quantity = 1
formData.unit = ''
formData.expiryDate = ''
formData.productImages = ''
formData.remark = ''
productImageFiles.value = []
}
function fileNameFromUrl(url: string, index: number) {
const segment = url.split('/').pop() || ''
const base = decodeURIComponent(segment.split('?')[0] || '')
return base || `图片${index + 1}`
}
function syncImagesToForm() {
formData.productImages = productImageFiles.value
.filter((f) => f.fileUrl && !f._uploading)
.map((f) => f.fileUrl)
.join(',')
}
function syncFormToImages(imagesStr: string) {
productImageFiles.value = imagesStr
? imagesStr
.split(',')
.map((s, i) => {
const url = s.trim()
if (!url) return null
return {
id: `existing_${i}`,
fileName: fileNameFromUrl(url, i),
fileUrl: url,
fileType: 'image/'
} as ProductImageFile
})
.filter((f): f is ProductImageFile => Boolean(f))
: []
}
async function handleProductImageChange(uploadFile: any) {
const file = uploadFile.raw || uploadFile
if (!file) return
const maxSizeMb = 10
if (file.size / 1024 / 1024 > maxSizeMb) {
ElMessage.error(`图片大小不能超过 ${maxSizeMb}MB`)
productImageUploadRef.value?.clearFiles()
return
}
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
productImageFiles.value.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileType: file.type || 'image/',
_uploading: true
})
const idx = productImageFiles.value.length - 1
try {
const fd = new FormData()
fd.append('folder', 'cell-product')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
productImageFiles.value[idx] = {
...productImageFiles.value[idx],
id: res.id || tempId,
fileName: file.name || res.fileName || fileNameFromUrl(res.fileUrl, idx),
fileUrl: res.fileUrl,
fileType: res.fileType || file.type || 'image/',
_uploading: false
}
syncImagesToForm()
} catch {
ElMessage.error('图片上传失败')
productImageFiles.value.splice(idx, 1)
} finally {
nextTick(() => productImageUploadRef.value?.clearFiles())
}
}
function removeProductImage(index: number) {
productImageFiles.value.splice(index, 1)
syncImagesToForm()
}
function handlePreviewProductImage(file: ProductImageFile) {
if (file._uploading || !file.fileUrl) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewVisible.value = true
}
function handleDownloadProductImage(file: ProductImageFile) {
downloadAttachmentFile(file, { fullFileUrl })
}
async function handleSubmit() {
await formRef.value?.validate?.()
syncImagesToForm()
submitLoading.value = true
try {
const result: Record<string, any> = {
cellProductId: formData.cellProductId || undefined,
productCode: formData.productCode.trim(),
productName: formData.productName.trim(),
productTypeId: formData.productTypeId || undefined,
specId: formData.specId || undefined,
passage: formData.passage || undefined,
batchNo: formData.batchNo.trim() || undefined,
storageSlotId: formData.storageSlotId || undefined,
expectedCellCount: formData.expectedCellCount ?? undefined,
viability: formData.viability ?? undefined,
freezeDate: formData.freezeDate || undefined,
quantity: formData.quantity,
unit: formData.unit?.trim() || undefined,
expiryDate: formData.expiryDate || undefined,
productImages: formData.productImages?.trim() || undefined,
productStatus: 'frozen',
remark: formData.remark?.trim() || undefined
}
emit('confirm', result)
emit('update:modelValue', false)
} finally {
submitLoading.value = false
}
}
</script>
<style scoped lang="scss">
.task-output-product-dialog {
:deep(.el-dialog__body) {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
}
.attachment-upload {
width: 100%;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s;
&:hover {
border-color: #10b981;
border-style: solid;
background: #ecfdf5;
}
}
.upload-card-text {
font-size: 12px;
color: #10b981;
font-weight: 500;
line-height: 1;
}
.upload-hint {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1;
margin-top: 8px;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.attachment-item {
width: 140px;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #10b981;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
}
&.is-uploading {
opacity: 0.6;
pointer-events: none;
}
}
.attachment-preview {
width: 100%;
height: 96px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.attachment-footer {
display: flex;
flex-direction: column;
flex: 1;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
color: #334155;
line-height: 1.4;
word-break: break-all;
min-height: calc(11px * 1.4 * 2);
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 4px;
flex-shrink: 0;
}
.action-btn {
width: 22px;
height: 22px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #64748b;
cursor: pointer;
background: #f1f5f9;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #10b981;
color: #fff;
}
&--danger:hover {
background: #ef4444;
color: #fff;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.workOrderNo" placeholder="请输入工单号" clearable />
</el-form-item>
<el-form-item label="工单状态">
<el-select v-model="searchForm.workOrderStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.workOrderStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待配置" value="dpz" />
<el-option label="已完成" value="ywc" />
</el-select>
@@ -27,8 +32,12 @@
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
</div>
</el-form>
@@ -43,7 +52,13 @@
stripe
@row-dblclick="handleView"
>
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" />
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center" />
@@ -71,10 +86,20 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<el-table-column label="操作" width="460" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
width="460"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看详情</el-button>
<el-button class="btn-op-item" @click="handleEditTask(row)"><el-icon><EditPen /></el-icon>编辑任务</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看详情</el-button
>
<el-button class="btn-op-item" @click="handleEditTask(row)"
><el-icon><EditPen /></el-icon>编辑任务</el-button
>
<el-button
v-if="row.workOrderStatus === 'dpz'"
class="btn-op-item primary"
@@ -144,11 +169,30 @@
@success="loadData"
/>
<!-- 编辑任务基本信息弹窗 -->
<EditTaskInfoDialog
v-model:visible="editTaskInfoVisible"
:task-id="editTaskInfoId"
@success="handleEditTaskInfoSuccess"
/>
<!-- 任务列表弹窗 -->
<el-dialog v-model="taskSelectVisible" title="生产任务列表" width="780px" destroy-on-close align-center>
<div v-loading="taskSelectLoading" style="max-height:60vh;overflow-y:auto">
<el-table :data="taskSelectList" stripe style="width:100%">
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => i + 1" />
<el-dialog
v-model="taskSelectVisible"
title="生产任务列表"
width="980px"
destroy-on-close
align-center
>
<div v-loading="taskSelectLoading" style="max-height: 60vh; overflow-y: auto">
<el-table :data="taskSelectList" stripe style="width: 100%">
<el-table-column
type="index"
label="序号"
width="68"
align="center"
:index="(i) => i + 1"
/>
<el-table-column prop="taskName" label="任务名称" min-width="140" />
<el-table-column prop="routeName" label="工艺路线" min-width="140">
<template #default="{ row }">{{ row.routeName || '—' }}</template>
@@ -160,14 +204,29 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openTaskEditor(row)">编辑</el-button>
<el-button link type="primary" size="small" @click="openEditTaskInfo(row)"
>编辑信息</el-button
>
<el-button link type="primary" size="small" @click="openTaskEditor(row)"
>编辑工艺</el-button
>
<el-button link type="danger" size="small" @click="handleDeleteTask(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!taskSelectLoading && !taskSelectList.length" description="暂无任务,请先派发任务" :image-size="48" />
<el-empty
v-if="!taskSelectLoading && !taskSelectList.length"
description="暂无任务,请先派发任务"
:image-size="48"
/>
</div>
</el-dialog>
</section>
@@ -178,7 +237,15 @@
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import type { TableInstance } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, RefreshLeft, View, Check, Document, Position, EditPen } from '@element-plus/icons-vue'
import {
Search,
RefreshLeft,
View,
Check,
Document,
Position,
EditPen
} from '@element-plus/icons-vue'
import {
fetchProductionWorkOrderList,
fetchProductionWorkOrderDetail,
@@ -195,7 +262,11 @@
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import DispatchTaskDialog from './DispatchTaskDialog.vue'
import { fetchProductionTaskList } from '@/api/production-task'
import EditTaskInfoDialog from './EditTaskInfoDialog.vue'
import {
fetchProductionTaskList,
fetchDeleteProductionTask
} from '@/api/production-task'
defineOptions({ name: 'ProductionProductionWorkOrder' })
@@ -248,8 +319,16 @@
await layoutWorkOrderTable()
}
}
function handleSearch() { pagination.page = 1; loadData() }
function handleReset() { searchForm.workOrderNo = ''; searchForm.workOrderStatus = ''; pagination.page = 1; loadData() }
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.workOrderNo = ''
searchForm.workOrderStatus = ''
pagination.page = 1
loadData()
}
const viewVisible = ref(false)
const detail = ref<ProductionWorkOrderDetail | null>(null)
@@ -262,12 +341,15 @@
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
const urls = String(r.receiveAttachments || '')
if (!urls.trim()) return []
return urls.split(',').map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
}).filter(Boolean) as Array<Record<string, unknown>>
return urls
.split(',')
.map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
})
function receptionAttachmentFullUrl(url: string) {
const u = String(url || '').trim()
@@ -310,9 +392,10 @@
)
}
if (r.etaTime) transportFields.push({ label: '预计到达', value: r.etaTime })
const transportSection = transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
const transportSection =
transportFields.length > 1 || r.transportType
? [{ title: '运输信息', fields: transportFields }]
: []
return [
{
title: '基本信息',
@@ -320,9 +403,20 @@
{ label: '工单号', value: d.workOrderNo },
{ label: '协议号', value: d.protocolNo },
{ label: '样本编码', value: d.sampleCode },
{ label: '来源类型', value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''), tagType: 'primary' },
{
label: '来源类型',
value:
r.sourceTypeLabel ||
d.receptionSourceLabel ||
receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary'
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' },
{ label: '工单状态', value: woStatusTag(d.workOrderStatus).label, tagType: woStatusTag(d.workOrderStatus).type as any },
{
label: '工单状态',
value: woStatusTag(d.workOrderStatus).label,
tagType: woStatusTag(d.workOrderStatus).type as any
},
{ label: '创建时间', value: d.createTime || '—' },
{ label: '备注', value: d.remark || '—', span: true }
]
@@ -337,7 +431,17 @@
{ label: '接收温度(℃)', value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—' },
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
{ label: '结论', value: r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')), tagType: r.acceptConclusion === 'js' ? 'success' : r.acceptConclusion === 'jj' ? 'danger' : 'info' },
{
label: '结论',
value:
r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')),
tagType:
r.acceptConclusion === 'js'
? 'success'
: r.acceptConclusion === 'jj'
? 'danger'
: 'info'
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
@@ -374,8 +478,10 @@
const taskSelectVisible = ref(false)
const taskSelectList = ref<any[]>([])
const taskSelectLoading = ref(false)
const currentWorkOrderRow = ref<any>(null)
async function handleEditTask(row: any) {
currentWorkOrderRow.value = row
taskSelectLoading.value = true
taskSelectVisible.value = true
taskSelectList.value = []
@@ -397,6 +503,30 @@
taskSelectVisible.value = false
}
// ── 编辑任务基本信息 ──────────────────────────────────
const editTaskInfoVisible = ref(false)
const editTaskInfoId = ref('')
function openEditTaskInfo(task: any) {
editTaskInfoId.value = task.id
editTaskInfoVisible.value = true
}
function handleEditTaskInfoSuccess() {
if (currentWorkOrderRow.value) {
handleEditTask(currentWorkOrderRow.value)
}
}
async function handleDeleteTask(task: any) {
await ElMessageBox.confirm('确定要删除该任务吗?删除后不可恢复。', '提示', { type: 'warning' })
await fetchDeleteProductionTask(task.id)
ElMessage.success('任务已删除')
if (currentWorkOrderRow.value) {
handleEditTask(currentWorkOrderRow.value)
}
}
onMounted(() => loadData())
</script>
@@ -411,8 +541,15 @@
font-size: 13px;
align-items: start;
dt { margin: 0; color: #64748b; font-weight: 500; }
dd { margin: 0; color: #1e293b; }
dt {
margin: 0;
color: #64748b;
font-weight: 500;
}
dd {
margin: 0;
color: #1e293b;
}
}
}
</style>
@@ -23,6 +23,13 @@ import {
} from '@/api/production'
import { fetchItemUnitList } from '@/api/admin'
import { fetchCellProductList } from '@/api/cell-product'
import {
fetchCellProductTypeList,
fetchCellProductSpecList,
fetchStorageFacilityList,
fetchStorageSlotList
} from '@/api/admin'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
// ── helpers ──────────────────────────────────────────────────────────
function toApiFlag(val: boolean | number | string | undefined): string {
@@ -90,9 +97,23 @@ export function useTaskStepEditor() {
const consumableOptionsLoading = ref(false)
const consumableMasterById = ref<Record<string, Record<string, any>>>({})
const cellProductOptions = ref<{ id: string; label: string }[]>([])
const cellProductOptions = ref<
Array<{
id: string
label: string
productCode?: string
productName?: string
productTypeName?: string
passage?: string
storageSlotName?: string
productStatus?: string
}>
>([])
const cellProductOptionsLoading = ref(false)
const cellProductMasterById = ref<Record<string, Record<string, any>>>({})
const productTypeOptions = ref<Array<{ id: string; productTypeName: string }>>([])
const specOptions = ref<Array<{ id: string; specName: string }>>([])
const storageSlotOptions = ref<Array<{ id: string; name: string }>>([])
function mapItemUnitOption(u: { unitName?: string; unitCode?: string }) {
const name = (u.unitName || '').trim()
@@ -108,7 +129,11 @@ export function useTaskStepEditor() {
async function searchItemUnitOptions(query: string) {
itemUnitOptionsLoading.value = true
try {
const res = await fetchItemUnitList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchItemUnitList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
itemUnitOptions.value = (res.list || []).map(mapItemUnitOption).filter((o) => o.value)
} catch {
itemUnitOptions.value = []
@@ -141,7 +166,11 @@ export function useTaskStepEditor() {
async function searchReagentOptions(query: string) {
reagentOptionsLoading.value = true
try {
const res = await fetchReagentList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchReagentList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
cacheReagentMasters(list)
reagentOptions.value = list
@@ -158,7 +187,10 @@ export function useTaskStepEditor() {
function ensureReagentOption(row: Record<string, any>) {
if (row.reagentId && !String(row.reagentId).startsWith('legacy:')) {
if (!reagentOptions.value.some((o) => o.id === row.reagentId))
reagentOptions.value = [{ id: row.reagentId, label: mapReagentOptionLabel(row) }, ...reagentOptions.value]
reagentOptions.value = [
{ id: row.reagentId, label: mapReagentOptionLabel(row) },
...reagentOptions.value
]
return
}
// reagentId 为空(后端不存储该字段)— 通过 reagentCode 反查主表真实 ID
@@ -175,7 +207,10 @@ export function useTaskStepEditor() {
const legacyId = `legacy:${code || name}`
row.reagentId = legacyId
if (!reagentOptions.value.some((o) => o.id === legacyId))
reagentOptions.value = [{ id: legacyId, label: mapReagentOptionLabel(row) }, ...reagentOptions.value]
reagentOptions.value = [
{ id: legacyId, label: mapReagentOptionLabel(row) },
...reagentOptions.value
]
}
function ensureAllStepReagentOptions() {
for (const step of stepList.value) {
@@ -183,11 +218,17 @@ export function useTaskStepEditor() {
}
}
function onReagentRowSelect(row: Record<string, any>, reagentId: string) {
if (!reagentId) { row.reagentId = ''; return }
if (String(reagentId).startsWith('legacy:')) { return }
if (!reagentId) {
row.reagentId = ''
return
}
if (String(reagentId).startsWith('legacy:')) {
return
}
// 去重:同一试剂不允许出现多次(排除当前行和 legacy 占位符)
const dup = (selectedStep.value?.reagents || []).some(
(r: Record<string, any>) => r !== row && r.reagentId === reagentId && !String(r.reagentId).startsWith('legacy:')
(r: Record<string, any>) =>
r !== row && r.reagentId === reagentId && !String(r.reagentId).startsWith('legacy:')
)
if (dup) {
ElMessage.warning('该试剂已添加,请勿重复选择')
@@ -195,11 +236,16 @@ export function useTaskStepEditor() {
return
}
const master = reagentMasterById.value[reagentId]
if (!master) { return }
if (!master) {
return
}
Object.assign(row, {
reagentId: master.id, reagentCode: master.reagentCode || '',
reagentName: master.reagentName || '', spec: master.spec || '',
standardUsage: master.standardUsage ?? '', unit: master.unit || '',
reagentId: master.id,
reagentCode: master.reagentCode || '',
reagentName: master.reagentName || '',
spec: master.spec || '',
standardUsage: master.standardUsage ?? '',
unit: master.unit || '',
remark: master.remark ?? master.description ?? ''
})
ensureItemUnitOption(row.unit)
@@ -230,7 +276,11 @@ export function useTaskStepEditor() {
async function searchConsumableOptions(query: string) {
consumableOptionsLoading.value = true
try {
const res = await fetchConsumableList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchConsumableList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
cacheConsumableMasters(list)
consumableOptions.value = list
@@ -247,7 +297,10 @@ export function useTaskStepEditor() {
function ensureConsumableOption(row: Record<string, any>) {
if (row.consumableId && !String(row.consumableId).startsWith('legacy:')) {
if (!consumableOptions.value.some((o) => o.id === row.consumableId))
consumableOptions.value = [{ id: row.consumableId, label: mapConsumableOptionLabel(row) }, ...consumableOptions.value]
consumableOptions.value = [
{ id: row.consumableId, label: mapConsumableOptionLabel(row) },
...consumableOptions.value
]
return
}
// consumableId 为空(后端不存储该字段)— 通过 consumableCode 反查主表真实 ID
@@ -264,7 +317,10 @@ export function useTaskStepEditor() {
const legacyId = `legacy:${code || name}`
row.consumableId = legacyId
if (!consumableOptions.value.some((o) => o.id === legacyId))
consumableOptions.value = [{ id: legacyId, label: mapConsumableOptionLabel(row) }, ...consumableOptions.value]
consumableOptions.value = [
{ id: legacyId, label: mapConsumableOptionLabel(row) },
...consumableOptions.value
]
}
function ensureAllStepConsumableOptions() {
for (const step of stepList.value) {
@@ -272,11 +328,19 @@ export function useTaskStepEditor() {
}
}
function onConsumableRowSelect(row: Record<string, any>, consumableId: string) {
if (!consumableId) { row.consumableId = ''; return }
if (String(consumableId).startsWith('legacy:')) { return }
if (!consumableId) {
row.consumableId = ''
return
}
if (String(consumableId).startsWith('legacy:')) {
return
}
// 去重:同一耗材不允许出现多次(排除当前行和 legacy 占位符)
const dup = (selectedStep.value?.consumables || []).some(
(c: Record<string, any>) => c !== row && c.consumableId === consumableId && !String(c.consumableId).startsWith('legacy:')
(c: Record<string, any>) =>
c !== row &&
c.consumableId === consumableId &&
!String(c.consumableId).startsWith('legacy:')
)
if (dup) {
ElMessage.warning('该耗材已添加,请勿重复选择')
@@ -284,11 +348,16 @@ export function useTaskStepEditor() {
return
}
const master = consumableMasterById.value[consumableId]
if (!master) { return }
if (!master) {
return
}
Object.assign(row, {
consumableId: master.id, consumableCode: master.consumableCode || '',
consumableName: master.consumableName || '', specModel: master.specModel || '',
standardUsage: master.standardUsage ?? '', unit: master.unit || '',
consumableId: master.id,
consumableCode: master.consumableCode || '',
consumableName: master.consumableName || '',
specModel: master.specModel || '',
standardUsage: master.standardUsage ?? '',
unit: master.unit || '',
remark: master.remark ?? master.description ?? ''
})
ensureItemUnitOption(row.unit)
@@ -303,17 +372,46 @@ export function useTaskStepEditor() {
async function searchCellProductOptions(query: string) {
cellProductOptionsLoading.value = true
try {
const res = await fetchCellProductList({ page: 1, pageSize: 100, keyword: query?.trim() || undefined })
const res = await fetchCellProductList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined
})
const list = res.list || []
const next = { ...cellProductMasterById.value }
for (const p of list) if (p?.id) next[p.id] = p
cellProductMasterById.value = next
cellProductOptions.value = list
.filter((p) => (p.productName || '').trim() || (p.productCode || '').trim())
.map((p) => ({ id: p.id, label: mapCellProductOptionLabel(p) }))
.map((p) => ({
id: p.id,
label: mapCellProductOptionLabel(p),
productCode: p.productCode || '',
productName: p.productName || '',
productTypeName: p.productTypeName || '',
passage: p.passage || '',
storageSlotName: p.storageSlotName || '',
productStatus: p.productStatus || ''
}))
for (const row of taskInputs.value) {
if (row.inputType === 'cell_product' && row.inputId && !cellProductOptions.value.some((o) => o.id === row.inputId)) {
cellProductOptions.value = [{ id: row.inputId, label: row.inputName || row.inputCode || '' }, ...cellProductOptions.value]
if (
row.inputType === 'cell_product' &&
row.inputId &&
!cellProductOptions.value.some((o) => o.id === row.inputId)
) {
cellProductOptions.value = [
{
id: row.inputId,
label: row.inputName || row.inputCode || '',
productCode: row.inputCode || '',
productName: row.inputName || '',
productTypeName: '',
passage: '',
storageSlotName: '',
productStatus: ''
},
...cellProductOptions.value
]
}
}
} catch {
@@ -323,7 +421,10 @@ export function useTaskStepEditor() {
}
}
function onCellProductInputSelect(row: Record<string, any>, productId: string) {
if (!productId) { row.inputId = ''; return }
if (!productId) {
row.inputId = ''
return
}
const master = cellProductMasterById.value[productId]
if (!master) return
row.inputId = master.id
@@ -335,7 +436,9 @@ export function useTaskStepEditor() {
for (const step of stepList.value) {
;(step.reagents || []).forEach((r: Record<string, any>) => ensureItemUnitOption(r.unit))
;(step.consumables || []).forEach((c: Record<string, any>) => ensureItemUnitOption(c.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) => ensureItemUnitOption(d.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) =>
ensureItemUnitOption(d.unit)
)
}
}
@@ -343,16 +446,21 @@ export function useTaskStepEditor() {
if (!selectedStepId.value) return null
return stepList.value.find((s) => s.id === selectedStepId.value) || null
})
const virtualInputStatus = computed(() => taskInputs.value.length > 0 ? 'filled' : 'empty')
const virtualOutputStatus = computed(() => taskOutputs.value.length > 0 ? 'filled' : 'empty')
const completedStepCount = computed(() => stepList.value.filter((s) => s.stepStatus === 'ywc').length)
const virtualInputStatus = computed(() => (taskInputs.value.length > 0 ? 'filled' : 'empty'))
const virtualOutputStatus = computed(() => (taskOutputs.value.length > 0 ? 'filled' : 'empty'))
const completedStepCount = computed(
() => stepList.value.filter((s) => s.stepStatus === 'ywc').length
)
const sopIdsArr = computed({
get() {
if (!selectedStep.value) return []
const raw = selectedStep.value.sopIds
if (!raw) return []
if (Array.isArray(raw)) return raw.filter(Boolean)
return String(raw).split(',').map((s: string) => s.trim()).filter(Boolean)
return String(raw)
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
},
set(val: string[]) {
if (!selectedStep.value) return
@@ -370,34 +478,97 @@ export function useTaskStepEditor() {
(p.processCode || '').toLowerCase().includes(keyword)
)
: group.processes
return { categoryId: group.categoryId, categoryName: group.categoryName, processes: filtered }
return {
categoryId: group.categoryId,
categoryName: group.categoryName,
processes: filtered
}
})
.filter((group) => group.processes.length > 0)
})
async function loadProductTypeAndSpecOptions() {
try {
const [typeRes, specRes] = await Promise.all([
fetchCellProductTypeList({ page: 1, pageSize: 100 }),
fetchCellProductSpecList({ page: 1, pageSize: 100 })
])
productTypeOptions.value = typeRes?.list || typeRes || []
specOptions.value = specRes?.list || specRes || []
} catch {
/* dropdown data failure is non-blocking */
}
}
async function loadStorageSlotOptions() {
try {
const facilityRes = await fetchStorageFacilityList()
const facilities = facilityRes?.list || facilityRes || []
const leafNodes = facilities.filter((n: any) => n.isLeaf === 1)
if (!leafNodes.length) {
storageSlotOptions.value = []
return
}
const results = await Promise.allSettled(
leafNodes.map(async (node: any) => {
const res: any = await fetchStorageSlotList({ facilityNodeId: node.id })
const slots = res?.list || res || []
const nodePath = buildTreePathLabelFromFlat(node.id, facilities, { nameKey: 'nodeName' })
return slots.map((slot: any) => {
const code = slot.slotCode || ''
const name = nodePath && code ? `${nodePath} / ${code}` : code || nodePath
return { id: slot.id, name }
})
})
)
const options: Array<{ id: string; name: string }> = []
results.forEach((r) => {
if (r.status === 'fulfilled') options.push(...r.value)
})
storageSlotOptions.value = options
} catch {
storageSlotOptions.value = []
}
}
async function loadConfigReferenceData() {
const [processes, categories, sops, outputTypes] = await Promise.all([
fetchAllProcesses(), fetchAllProcessCategories(),
fetchAllSopDocuments(), fetchAllOutputTypes()
fetchAllProcesses(),
fetchAllProcessCategories(),
fetchAllSopDocuments(),
fetchAllOutputTypes()
])
categoryOptions.value = categories || []
sopOptions.value = sops || []
outputTypeOptions.value = outputTypes || []
const catMap: Record<string, { categoryId: string; categoryName: string; processes: Record<string, any>[] }> = {}
const catMap: Record<
string,
{ categoryId: string; categoryName: string; processes: Record<string, any>[] }
> = {}
const uKey = '__uncategorized__'
catMap[uKey] = { categoryId: uKey, categoryName: '未分类', processes: [] }
;(categories || []).forEach((cat: Record<string, any>) => {
catMap[cat.id] = { categoryId: cat.id, categoryName: cat.categoryName || cat.name || '未命名分类', processes: [] }
catMap[cat.id] = {
categoryId: cat.id,
categoryName: cat.categoryName || cat.name || '未命名分类',
processes: []
}
})
;(processes || []).forEach((proc: Record<string, any>) => {
const catId = proc.categoryId || uKey
if (!catMap[catId]) catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
if (!catMap[catId])
catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
catMap[catId].processes.push(proc)
})
expandedCategories.value = Object.keys(catMap)
processLibrary.value = Object.values(catMap)
await Promise.all([searchItemUnitOptions(''), searchReagentOptions(''), searchConsumableOptions(''), searchCellProductOptions('')])
await Promise.all([
searchItemUnitOptions(''),
searchReagentOptions(''),
searchConsumableOptions(''),
searchCellProductOptions(''),
loadProductTypeAndSpecOptions(),
loadStorageSlotOptions()
])
}
async function loadTaskDetail(overrideId?: string) {
const id = overrideId || taskId.value
@@ -417,8 +588,15 @@ export function useTaskStepEditor() {
taskInputs.value = (detail.inputs || []).map((i: Record<string, any>) => ({ ...i }))
taskOutputs.value = (detail.outputs || []).map((o: Record<string, any>) => ({ ...o }))
for (const row of taskInputs.value) {
if (row.inputType === 'cell_product' && row.inputId && !cellProductOptions.value.some((o) => o.id === row.inputId)) {
cellProductOptions.value = [{ id: row.inputId, label: row.inputName || row.inputCode || '' }, ...cellProductOptions.value]
if (
row.inputType === 'cell_product' &&
row.inputId &&
!cellProductOptions.value.some((o) => o.id === row.inputId)
) {
cellProductOptions.value = [
{ id: row.inputId, label: row.inputName || row.inputCode || '' },
...cellProductOptions.value
]
}
}
}
@@ -440,7 +618,9 @@ export function useTaskStepEditor() {
await loadTaskDetail()
if (newStep?.id) selectedStepId.value = newStep.id
ElMessage.success('步骤添加成功')
} catch { /* ignore */ } finally {
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -449,8 +629,13 @@ export function useTaskStepEditor() {
if (!taskId.value) return
stepListLoading.value = true
try {
await fetchReorderTaskSteps({ taskId: taskId.value, stepIds: stepList.value.map((s) => s.id) })
} catch { /* ignore */ } finally {
await fetchReorderTaskSteps({
taskId: taskId.value,
stepIds: stepList.value.map((s) => s.id)
})
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -469,7 +654,9 @@ export function useTaskStepEditor() {
await loadTaskDetail()
if (ns?.id) selectedStepId.value = ns.id
ElMessage.success('步骤已复制')
} catch (e: any) { ElMessage.error(e?.message || '复制失败') } finally {
} catch (e: any) {
ElMessage.error(e?.message || '复制失败')
} finally {
stepListLoading.value = false
}
}
@@ -482,7 +669,9 @@ export function useTaskStepEditor() {
if (selectedStepId.value === step.id)
selectedStepId.value = stepList.value.length > 0 ? stepList.value[0].id : ''
ElMessage.success('步骤已删除')
} catch { /* ignore */ } finally {
} catch {
/* ignore */
} finally {
stepListLoading.value = false
}
}
@@ -498,7 +687,9 @@ export function useTaskStepEditor() {
estimated_duration: selectedStep.value.estimatedDuration,
sop_ids: selectedStep.value.sopIds
})
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function onSopIdsChange(val: string[]) {
if (!selectedStep.value) return
@@ -509,84 +700,135 @@ export function useTaskStepEditor() {
if (!selectedStep.value) return
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
selectedStep.value.reagents.push({
reagentId: '', reagentCode: '', reagentName: '', spec: '',
standardUsage: '', unit: '', isKeyMaterial: false, remark: ''
reagentId: '',
reagentCode: '',
reagentName: '',
spec: '',
standardUsage: '',
unit: '',
isKeyMaterial: false,
remark: ''
})
}
function removeReagentRow(i: number) { selectedStep.value?.reagents?.splice(i, 1) }
function removeReagentRow(i: number) {
selectedStep.value?.reagents?.splice(i, 1)
}
async function saveReagents() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.reagents || []).map((r: Record<string, any>) => ({
material_code: r.reagentCode, material_name: r.reagentName, spec: r.spec,
standard_usage: r.standardUsage, actual_usage: r.actualUsage || '',
material_code: r.reagentCode,
material_name: r.reagentName,
spec: r.spec,
standard_usage: r.standardUsage,
actual_usage: r.actualUsage || '',
unit: r.unit,
is_key_material: toApiFlag(r.isKeyMaterial), remark: r.remark || ''
is_key_material: toApiFlag(r.isKeyMaterial),
remark: r.remark || ''
}))
await fetchReplaceStepReagents(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addConsumableRow() {
if (!selectedStep.value) return
if (!selectedStep.value.consumables) selectedStep.value.consumables = []
selectedStep.value.consumables.push({
consumableId: '', consumableCode: '', consumableName: '', specModel: '',
standardUsage: '', unit: '', isRequired: false, remark: ''
consumableId: '',
consumableCode: '',
consumableName: '',
specModel: '',
standardUsage: '',
unit: '',
isRequired: false,
remark: ''
})
}
function removeConsumableRow(i: number) { selectedStep.value?.consumables?.splice(i, 1) }
function removeConsumableRow(i: number) {
selectedStep.value?.consumables?.splice(i, 1)
}
async function saveConsumables() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.consumables || []).map((c: Record<string, any>) => ({
material_code: c.consumableCode, material_name: c.consumableName,
material_code: c.consumableCode,
material_name: c.consumableName,
spec_model: c.specModel,
standard_usage: c.standardUsage, actual_usage: c.actualUsage || '',
standard_usage: c.standardUsage,
actual_usage: c.actualUsage || '',
unit: c.unit,
is_required: toApiFlag(c.isRequired), remark: c.remark || ''
is_required: toApiFlag(c.isRequired),
remark: c.remark || ''
}))
await fetchReplaceStepConsumables(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addOutputRow() {
if (!selectedStep.value) return
if (!selectedStep.value.outputs) selectedStep.value.outputs = []
selectedStep.value.outputs.push({ outputTypeId: '', outputName: '', outputBarcode: '', remark: '' })
selectedStep.value.outputs.push({
outputTypeId: '',
outputName: '',
outputBarcode: '',
remark: ''
})
}
function removeOutputRow(i: number) {
selectedStep.value?.outputs?.splice(i, 1)
}
function removeOutputRow(i: number) { selectedStep.value?.outputs?.splice(i, 1) }
async function saveOutputs() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.outputs || []).map((o: Record<string, any>) => ({
output_type_id: o.outputTypeId, output_name: o.outputName,
output_barcode: o.outputBarcode, remark: o.remark || ''
output_type_id: o.outputTypeId,
output_name: o.outputName,
output_barcode: o.outputBarcode,
remark: o.remark || ''
}))
await fetchReplaceStepOutputs(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
function addDataCollectionRow() {
if (!selectedStep.value) return
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
selectedStep.value.dataCollections.push({ collectionName: '', unit: '', orderNum: 0, remark: '' })
selectedStep.value.dataCollections.push({
collectionName: '',
unit: '',
orderNum: 0,
remark: ''
})
}
function removeDataCollectionRow(i: number) {
selectedStep.value?.dataCollections?.splice(i, 1)
}
function removeDataCollectionRow(i: number) { selectedStep.value?.dataCollections?.splice(i, 1) }
async function saveDataCollections() {
if (!selectedStep.value || configSuppressSave.value) return
try {
const items = (selectedStep.value.dataCollections || []).map((d: Record<string, any>) => ({
collection_name: d.collectionName, unit: d.unit,
collection_value: d.collectionValue || '', remark: d.remark || ''
collection_name: d.collectionName,
unit: d.unit,
collection_value: d.collectionValue || '',
remark: d.remark || ''
}))
await fetchReplaceStepDataCollection(taskId.value, selectedStep.value.id, items)
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
const manualSaveLoading = ref(false)
const stepActionLoading = ref(false)
const taskActionLoading = ref(false)
async function handleManualSave() {
if (!selectedStep.value) return
if (!selectedStep.value || manualSaveLoading.value) return
manualSaveLoading.value = true
try {
await Promise.all([
@@ -605,34 +847,105 @@ export function useTaskStepEditor() {
}
async function handleStartStep(step: Record<string, any>) {
if (!taskId.value || !step.id) return
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'jxz' })
ElMessage.success('步骤已开始执行')
await loadTaskDetail()
if (!taskId.value || !step.id || stepActionLoading.value) return
stepActionLoading.value = true
try {
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'jxz' })
ElMessage.success('步骤已开始执行')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
stepActionLoading.value = false
}
}
async function handleCompleteStep(step: Record<string, any>) {
if (!taskId.value || !step.id) return
await ElMessageBox.confirm('确定完成该步骤吗?', '提示', { type: 'warning' })
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'ywc' })
ElMessage.success('步骤已完成')
await loadTaskDetail()
if (!taskId.value || !step.id || stepActionLoading.value) return
try {
await ElMessageBox.confirm('确定完成该步骤吗?', '提示', { type: 'warning' })
} catch {
return
}
stepActionLoading.value = true
try {
await fetchUpdateTaskStep(taskId.value, step.id, { step_status: 'ywc' })
ElMessage.success('步骤已完成')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
stepActionLoading.value = false
}
}
async function handleStartTask() {
if (!taskDetail.value?.id) return
await ElMessageBox.confirm('确定开始执行该任务吗?', '提示', { type: 'warning' })
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'jxz' })
ElMessage.success('任务已开始执行')
await loadTaskDetail(taskDetail.value.id)
if (!taskDetail.value?.id || taskActionLoading.value) return
try {
await ElMessageBox.confirm('确定开始执行该任务吗?', '提示', { type: 'warning' })
} catch {
return
}
taskActionLoading.value = true
try {
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'jxz' })
ElMessage.success('任务已开始执行')
await loadTaskDetail(taskDetail.value.id)
} catch {
/* global error handling */
} finally {
taskActionLoading.value = false
}
}
function validateTaskIoRequired(): string | null {
if (!taskInputs.value.length) {
return '请先填写原材料(样本/细胞产品),至少添加一项并保存'
}
if (!taskOutputs.value.length) {
return '请先填写产出(细胞产品),至少添加一项并保存'
}
const invalidInput = taskInputs.value.find(
(row) =>
!row.inputType ||
(!(row.inputId || '').trim() &&
!(row.inputCode || '').trim() &&
!(row.inputName || '').trim())
)
if (invalidInput) {
return '原材料存在未填完整的行,请补全类型与编码/名称后保存'
}
const invalidOutput = taskOutputs.value.find(
(row) => !(row.outputName || '').trim() || !(row.outputBarcode || '').trim()
)
if (invalidOutput) {
return '产出存在未填完整的行,请补全输出名称与条码后保存'
}
return null
}
async function handleCompleteTask() {
if (!taskDetail.value?.id) return
await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
ElMessage.success('任务已完成')
await loadTaskDetail(taskDetail.value.id)
if (!taskDetail.value?.id || taskActionLoading.value) return
const ioErr = validateTaskIoRequired()
if (ioErr) {
await ElMessageBox.alert(ioErr, '无法完成任务', { type: 'warning' })
return
}
try {
await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
} catch {
return
}
taskActionLoading.value = true
try {
await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
ElMessage.success('任务已完成')
await loadTaskDetail(taskDetail.value.id)
} catch {
/* global error handling */
} finally {
taskActionLoading.value = false
}
}
function addTaskInputRow() {
@@ -646,27 +959,68 @@ export function useTaskStepEditor() {
remark: ''
})
}
function removeTaskInputRow(i: number) { taskInputs.value.splice(i, 1) }
function removeTaskInputRow(i: number) {
taskInputs.value.splice(i, 1)
}
async function saveTaskInputs() {
if (!taskDetail.value?.id) return
if (!taskInputs.value.length) {
ElMessage.warning('请至少添加一项原材料(样本/细胞产品)')
return
}
const invalid = taskInputs.value.find(
(row) =>
!row.inputType ||
(!(row.inputId || '').trim() &&
!(row.inputCode || '').trim() &&
!(row.inputName || '').trim())
)
if (invalid) {
ElMessage.warning('请补全每行原材料的类型与编码/名称')
return
}
inputSaving.value = true
try {
await fetchSetTaskInputs(taskDetail.value.id, taskInputs.value)
ElMessage.success('输入已保存')
} catch { /* global error handling */ } finally { inputSaving.value = false }
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
inputSaving.value = false
}
}
function addTaskOutputRow() {
taskOutputs.value.push({ cellProductId: '', storageSlotId: '', outputName: '', outputBarcode: '', quantity: 0, unit: '', remark: '' })
taskOutputs.value.push({
cellProductId: '',
storageSlotId: '',
outputName: '',
outputBarcode: '',
quantity: 0,
unit: '',
remark: ''
})
}
function removeTaskOutputRow(i: number) {
taskOutputs.value.splice(i, 1)
}
function removeTaskOutputRow(i: number) { taskOutputs.value.splice(i, 1) }
async function saveTaskOutputs() {
if (!taskDetail.value?.id) return
if (!taskOutputs.value.length) {
ElMessage.warning('请至少添加一项产出(细胞产品)')
return
}
outputSaving.value = true
try {
await fetchSetTaskOutputs(taskDetail.value.id, taskOutputs.value)
ElMessage.success('输出已保存')
} catch { /* global error handling */ } finally { outputSaving.value = false }
ElMessage.success('产出已保存')
await loadTaskDetail()
} catch {
/* global error handling */
} finally {
outputSaving.value = false
}
}
function taskStatusTag(status: string) {
@@ -693,43 +1047,100 @@ export function useTaskStepEditor() {
activeSubTab.value = 'reagent'
processSearchKeyword.value = ''
configSuppressSave.value = true
stepList.value = []
stepListLoading.value = true
try {
await loadConfigReferenceData()
await loadTaskDetail()
} finally {
stepListLoading.value = false
await nextTick()
configSuppressSave.value = false
}
}
return {
taskId, stepList, selectedStepId, selectedStep, stepListLoading,
activeSubTab, processSearchKeyword, configSuppressSave,
mode, taskDetail, taskInputs, taskOutputs, inputSaving, outputSaving,
selectedVirtualStep, processLibraryExpanded,
virtualInputStatus, virtualOutputStatus, completedStepCount,
processLibrary, filteredProcessLibrary, expandedCategories,
categoryOptions, sopOptions, outputTypeOptions,
itemUnitOptions, itemUnitOptionsLoading,
reagentOptions, reagentOptionsLoading,
consumableOptions, consumableOptionsLoading,
cellProductOptions, cellProductOptionsLoading,
taskId,
stepList,
selectedStepId,
selectedStep,
stepListLoading,
activeSubTab,
processSearchKeyword,
configSuppressSave,
mode,
taskDetail,
taskInputs,
taskOutputs,
inputSaving,
outputSaving,
selectedVirtualStep,
processLibraryExpanded,
virtualInputStatus,
virtualOutputStatus,
completedStepCount,
processLibrary,
filteredProcessLibrary,
expandedCategories,
categoryOptions,
sopOptions,
outputTypeOptions,
itemUnitOptions,
itemUnitOptionsLoading,
reagentOptions,
reagentOptionsLoading,
consumableOptions,
consumableOptionsLoading,
cellProductOptions,
cellProductOptionsLoading,
productTypeOptions,
specOptions,
storageSlotOptions,
sopIdsArr,
onDragStart, onDropToCenter, onStepDragEnd, selectStep, selectVirtualStep,
handleCopyStep, handleDeleteStep,
handleStartStep, handleCompleteStep,
handleStartTask, handleCompleteTask,
addTaskInputRow, removeTaskInputRow, saveTaskInputs,
addTaskOutputRow, removeTaskOutputRow, saveTaskOutputs,
taskStatusTag, stepStatusTag,
saveStepBasic, onSopIdsChange,
searchItemUnitOptions, searchReagentOptions, searchConsumableOptions, searchCellProductOptions,
onReagentRowSelect, onConsumableRowSelect, onCellProductInputSelect,
addReagentRow, removeReagentRow, saveReagents,
addConsumableRow, removeConsumableRow, saveConsumables,
addOutputRow, removeOutputRow, saveOutputs,
addDataCollectionRow, removeDataCollectionRow, saveDataCollections,
manualSaveLoading, handleManualSave,
onDragStart,
onDropToCenter,
onStepDragEnd,
selectStep,
selectVirtualStep,
handleCopyStep,
handleDeleteStep,
handleStartStep,
handleCompleteStep,
handleStartTask,
handleCompleteTask,
addTaskInputRow,
removeTaskInputRow,
saveTaskInputs,
addTaskOutputRow,
removeTaskOutputRow,
saveTaskOutputs,
taskStatusTag,
stepStatusTag,
saveStepBasic,
onSopIdsChange,
searchItemUnitOptions,
searchReagentOptions,
searchConsumableOptions,
searchCellProductOptions,
onReagentRowSelect,
onConsumableRowSelect,
onCellProductInputSelect,
addReagentRow,
removeReagentRow,
saveReagents,
addConsumableRow,
removeConsumableRow,
saveConsumables,
addOutputRow,
removeOutputRow,
saveOutputs,
addDataCollectionRow,
removeDataCollectionRow,
saveDataCollections,
manualSaveLoading,
stepActionLoading,
taskActionLoading,
handleManualSave,
initEditor
}
}
@@ -138,9 +138,14 @@
<SystemViewDialog
v-model:visible="viewVisible"
title="工单详情"
width="1120px"
:grid-columns="4"
compact
:loading="viewLoading"
:icon="Document"
:meta="detail?.workOrderNo || ''"
:sections="viewSections"
modal-class="qc-work-order-view-dialog-mask"
>
<template #reception-extra>
<div class="view-reception-attachments">
@@ -156,6 +161,15 @@
</div>
</div>
</template>
<template v-for="task in viewTasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
<div :key="task.id" class="qc-work-order-task-attachments">
<ProtocolAttachmentReadonly
:files="task.attachmentFiles || []"
@preview="handleTaskAttachmentPreview"
@download="handleTaskAttachmentDownload"
/>
</div>
</template>
</SystemViewDialog>
</section>
</div>
@@ -174,15 +188,19 @@
type QcWorkOrder,
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import type { QcTask } from '@/api/qc-task'
import {
workOrderStatusLabel,
receptionSourceLabel,
acceptConclusionLabel,
transportTypeLabel
transportTypeLabel,
taskStatusLabel,
qcTaskConclusionLabel
} from '@/utils/qc-labels'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'QualityInspectionQcWorkOrder' })
@@ -217,9 +235,20 @@
})
const viewVisible = ref(false)
const viewLoading = ref(false)
const detail = ref<QcWorkOrderDetail | null>(null)
type QcWorkOrderTaskView = QcTask & { results?: QcTaskResultItem[] }
const viewTasks = ref<QcWorkOrderTaskView[]>([])
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewTasksWithAttachments = computed(() =>
viewTasks.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
)
function taskAttachmentSlotName(taskId: string) {
return `task-att-${taskId}`
}
const viewReceptionAttachmentFiles = computed(() => {
const r = detail.value?.reception
if (!r) return []
@@ -255,6 +284,125 @@
downloadAttachmentFile(file, { fullFileUrl: receptionAttachmentFullUrl })
}
function taskAttachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleTaskAttachmentPreview(file: Record<string, unknown>) {
const url = taskAttachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleTaskAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: taskAttachmentFullUrl })
}
function conclusionTagType(conclusion?: string) {
if (conclusion === 'tg') return 'success' as const
if (conclusion === 'btg') return 'danger' as const
return 'info' as const
}
function taskStatusTagType(status?: string) {
if (status === 'ywc') return 'success' as const
if (status === 'jxz') return 'warning' as const
return 'info' as const
}
function formatConfiguredLabItems(d: QcWorkOrderDetail) {
return (
d.labItems?.map((li) => `${li.itemName || ''}(${li.itemCode || ''})`).join('、') || '无'
)
}
function formatConfiguredThirdItems(d: QcWorkOrderDetail) {
return (
d.thirdPartyItems
?.map((li) => `${li.companyName || ''} — ${li.itemName || ''}(¥${li.feeAmount ?? '—'})`)
.join('、') || '无'
)
}
function buildTaskTypeSections(
tasks: QcWorkOrderTaskView[],
taskType: 'sysz' | 'dsfjc',
sectionTitle: string
): ViewSection[] {
const filtered = tasks.filter((t) => t.taskType === taskType)
if (!filtered.length) return []
return filtered.map((task, index) => {
const title =
filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
const fields: ViewField[] = [
{ label: '检测项目', value: task.itemName || '—' },
{ label: '项目编码', value: task.itemCode || '—' },
{
label: '任务状态',
value: taskStatusLabel(task.taskStatus || ''),
tagType: taskStatusTagType(task.taskStatus)
},
{
label: '检测结论',
value: task.conclusionLabel || qcTaskConclusionLabel(task.conclusion || '') || '—',
tagType: conclusionTagType(task.conclusion)
},
{ label: '任务号', value: task.taskNo || '—' },
{ label: '完成时间', value: task.completedTime || '—' }
]
if (taskType === 'dsfjc') {
fields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
if (task.feeAmount != null) {
fields.push({ label: '费用(元)', value: String(task.feeAmount) })
}
}
if (task.results?.length) {
for (const item of task.results) {
fields.push({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
})
}
} else if (taskType === 'sysz') {
fields.push({ label: '检测参数', value: '—' })
}
fields.push({ label: '备注', value: task.remark || '—', span: true })
const section: ViewSection = { title, fields }
if (task.attachmentFiles?.length) {
section.slot = taskAttachmentSlotName(task.id)
section.slotTitle = taskType === 'dsfjc' ? '检测材料' : '检测附件'
section.slotTitleStyle = 'field'
} else if (taskType === 'dsfjc') {
fields.push({ label: '检测材料', value: '—' })
} else {
fields.push({ label: '检测附件', value: '—' })
}
return section
})
}
function buildConfiguredOnlySection(
title: string,
configuredLabel: string,
configuredValue: string,
attachmentLabel: string
): ViewSection {
return {
title,
fields: [
{ label: configuredLabel, value: configuredValue, span: true },
{ label: '检测结论', value: '—' },
{ label: '检测参数', value: '—' },
{ label: '备注', value: '—' },
{ label: attachmentLabel, value: '—' }
]
}
}
function formatDetectMethods(row: QcWorkOrder) {
const parts: string[] = []
if (row.hasLab) parts.push('实验室')
@@ -342,28 +490,23 @@
{ label: '备注', value: r.remark || '—', span: true }
]
},
{
title: '实验室项目',
fields: [
{
label: '检测项目',
value: d.labItems?.map((li) => li.itemName + '(' + li.itemCode + ')').join('、') || '无',
span: true
}
]
},
{
title: '第三方项目',
fields: [
{
label: '检测项目',
value:
d.thirdPartyItems?.map((li) => li.companyName + ' — ' + li.itemName + '(¥' + li.feeAmount + ')').join('、') ||
'无',
span: true
}
]
}
...(viewTasks.value.some((t) => t.taskType === 'sysz')
? buildTaskTypeSections(viewTasks.value, 'sysz', '实验室项目')
: d.hasLab
? [buildConfiguredOnlySection('实验室项目', '已配置项目', formatConfiguredLabItems(d), '检测附件')]
: []),
...(viewTasks.value.some((t) => t.taskType === 'dsfjc')
? buildTaskTypeSections(viewTasks.value, 'dsfjc', '第三方项目')
: d.hasThirdParty
? [
buildConfiguredOnlySection(
'第三方项目',
'已配置项目',
formatConfiguredThirdItems(d),
'检测材料'
)
]
: [])
]
})
@@ -433,8 +576,30 @@
}
async function handleView(row: QcWorkOrder) {
detail.value = await fetchQcWorkOrderDetail(row.id)
viewVisible.value = true
viewLoading.value = true
viewTasks.value = []
try {
const d = await fetchQcWorkOrderDetail(row.id)
detail.value = d
const rawTasks = (d.tasks || []) as QcWorkOrderTaskView[]
const enriched = await Promise.all(
rawTasks.map(async (task) => {
if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
return { ...task, results: [] as QcTaskResultItem[] }
}
try {
const results = await fetchQcTaskResults(task.id)
return { ...task, results: results || [] }
} catch {
return { ...task, results: [] as QcTaskResultItem[] }
}
})
)
viewTasks.value = enriched
} finally {
viewLoading.value = false
}
}
onMounted(() => loadData())
@@ -475,6 +640,10 @@
color: var(--mint-text-muted, #64748b);
}
.qc-work-order-task-attachments {
margin-top: 0;
}
.view-reception-attachments {
margin-top: 4px;