This commit is contained in:
zlwu committed 2026-10-17 11:46:53 +08:00
1 parent 48dcf991dc
commit be2226e4ca
100 files changed
+61340 -40975

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+140 -145
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-10-16)
# Graph Report - src (2026-10-17)
## Corpus Check
- Large corpus: 1036 files · ~687,058 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 1036 files · ~687,735 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 3846 nodes · 5638 edges · 67 communities detected
- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1352 edges (avg confidence: 0.8)
- 3834 nodes · 5593 edges · 66 communities detected
- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1336 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -52,35 +52,34 @@
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 46|Community 46]]
- [[_COMMUNITY_Community 47|Community 47]]
- [[_COMMUNITY_Community 49|Community 49]]
- [[_COMMUNITY_Community 48|Community 48]]
- [[_COMMUNITY_Community 51|Community 51]]
- [[_COMMUNITY_Community 53|Community 53]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
- [[_COMMUNITY_Community 56|Community 56]]
- [[_COMMUNITY_Community 57|Community 57]]
- [[_COMMUNITY_Community 58|Community 58]]
- [[_COMMUNITY_Community 59|Community 59]]
- [[_COMMUNITY_Community 60|Community 60]]
- [[_COMMUNITY_Community 62|Community 62]]
- [[_COMMUNITY_Community 61|Community 61]]
- [[_COMMUNITY_Community 64|Community 64]]
- [[_COMMUNITY_Community 65|Community 65]]
- [[_COMMUNITY_Community 66|Community 66]]
- [[_COMMUNITY_Community 67|Community 67]]
- [[_COMMUNITY_Community 68|Community 68]]
- [[_COMMUNITY_Community 69|Community 69]]
- [[_COMMUNITY_Community 73|Community 73]]
- [[_COMMUNITY_Community 76|Community 76]]
- [[_COMMUNITY_Community 77|Community 77]]
- [[_COMMUNITY_Community 94|Community 94]]
- [[_COMMUNITY_Community 72|Community 72]]
- [[_COMMUNITY_Community 75|Community 75]]
- [[_COMMUNITY_Community 78|Community 78]]
- [[_COMMUNITY_Community 93|Community 93]]
- [[_COMMUNITY_Community 96|Community 96]]
- [[_COMMUNITY_Community 97|Community 97]]
- [[_COMMUNITY_Community 98|Community 98]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 343 edges
2. `push()` - 129 edges
3. `Number()` - 103 edges
1. `get()` - 345 edges
2. `push()` - 128 edges
3. `Number()` - 102 edges
4. `emit()` - 35 edges
5. `WebSocketClient` - 26 edges
6. `set()` - 23 edges
@@ -97,336 +96,332 @@
- `isStorageSlotAvailable()` --calls--> `Number()` [INFERRED]
utils/storage-slot-options.ts → views/cell-storage/payment-info/PaymentBySampleTab.vue
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/operations/exhibition-appointment/index.vue → utils/fullscreen-loading.ts
views/system/tenant/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/cell-sample-type/index.vue → utils/fullscreen-loading.ts
views/system/unit/index.vue → utils/fullscreen-loading.ts
## Communities
### Community 0 - "Community 0"
Cohesion: 0.0
Nodes (213): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+205 more)
Nodes (214): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+206 more)
### Community 1 - "Community 1"
Cohesion: 0.01
Nodes (168): fetchUploadAttachment(), fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), handleReportUpload(), handleReportUpload(), walk() (+160 more)
### Community 2 - "Community 2"
Cohesion: 0.02
Nodes (131): handleFocus(), handleInput(), if(), Number(), applyPreferentialContractForV1Mode(), createEmptyTransport(), datetimeToYmd(), defaultInstallmentStartDateYmd() (+123 more)
### Community 2 - "Community 2"
Cohesion: 0.02
Nodes (89): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchProductionTaskList() (+81 more)
### Community 3 - "Community 3"
Cohesion: 0.02
Nodes (85): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateItemUnit(), fetchDeleteItemUnit(), fetchItemUnitList() (+77 more)
Nodes (92): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), buildTemporaryViewSections(), buildInsertedSiblingCard(), fetchMineTasksForWorkOrders(), resolveShowProtocolPaymentAtGroup() (+84 more)
### Community 4 - "Community 4"
Cohesion: 0.02
Nodes (89): handleBlur(), handleFocus(), handleInput(), fetchConfirmPresignTransfer(), fetchPresignOrderDetail(), fetchPresignOrderList(), fetchPresignTransferReceiptConfirmedList(), fetchPresignTransferReceiptConfirmList() (+81 more)
Nodes (81): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaAppList(), fetchOaBankReceipts() (+73 more)
### Community 5 - "Community 5"
Cohesion: 0.02
Nodes (89): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), markPilotCheckupChecked() (+81 more)
Nodes (89): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), fetchUpdatePostBoundRoles() (+81 more)
### Community 6 - "Community 6"
Cohesion: 0.02
Nodes (73): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaBankReceipts(), fetchOaCcUnreadCount() (+65 more)
Cohesion: 0.03
Nodes (80): fetchUploadAttachment(), FireworkSystem, handleReportUpload(), handleReportUpload(), walk(), async(), addDispatchItem(), createDispatchItem() (+72 more)
### Community 7 - "Community 7"
Cohesion: 0.02
Nodes (65): collectIds(), buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields() (+57 more)
Nodes (84): fetchCellSampleTypeList(), fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCellSampleType(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateExhibitionAppointment() (+76 more)
### Community 8 - "Community 8"
Cohesion: 0.03
Nodes (33): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RouteRegistry, RouteTransformer, useAppMode(), useAuth() (+25 more)
Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
### Community 9 - "Community 9"
Cohesion: 0.03
Nodes (57): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), if(), nodeSummary() (+49 more)
Nodes (57): handleBlur(), handleFocus(), handleInput(), fetchCreatePaymentTransfer(), fetchDeletePaymentTransfer(), fetchPaymentChaseList(), fetchPaymentTransferList(), fetchPaymentTransferReceiptConfirmedList() (+49 more)
### Community 10 - "Community 10"
Cohesion: 0.04
Nodes (64): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchCreateRegionStaff(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchDeleteRegionStaff(), fetchHospitalList() (+56 more)
Cohesion: 0.03
Nodes (62): fetchOaRelatedSummaries(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), loadAllAppointmentsForExport() (+54 more)
### Community 11 - "Community 11"
Cohesion: 0.04
Nodes (70): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), openForDraft(), remoteSearchHandoverUsers(), applySchema(), flowNeedsInitiatorSelect() (+62 more)
Nodes (71): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), branchConditionLines(), remoteSearchHandoverUsers(), collectConditionReferencedFields(), collectFormConditionReferencedFields() (+63 more)
### Community 12 - "Community 12"
Cohesion: 0.03
Nodes (52): fetchDictDataOptions(), fetchAttachmentDownloadUrl(), fetchUpdateSampleFreight(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), handleDownloadAttachment(), attachmentFullUrl() (+44 more)
Nodes (54): fetchDictDataOptions(), fetchAttachmentDownloadUrl(), fetchUpdateSampleFreight(), formatProtocolAmount(), handleAttachmentPreview(), resolveFileUrl(), showStepContent(), visibleFields() (+46 more)
### Community 13 - "Community 13"
Cohesion: 0.03
Nodes (30): fetchCollectionProtocolList(), fetchProductionWorkOrderQcInfo(), fetchSampleInsurance(), fetchSampleStorageCert(), fetchUpdateSampleStorageCert(), dash(), fmtMoney(), auditorDisplay() (+22 more)
Cohesion: 0.04
Nodes (58): fetchOaInstanceDetail(), fetchOaTasks(), buildFilterParams(), currentPrinterName(), handleReset(), handleSearch(), load(), openDetail() (+50 more)
### Community 14 - "Community 14"
Cohesion: 0.04
Nodes (51): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+43 more)
Nodes (45): buildBaseFields(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail(), buildOutboundPilotProductionViewSections() (+37 more)
### Community 15 - "Community 15"
Cohesion: 0.04
Nodes (40): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+32 more)
Nodes (51): fetchCreateItemUnit(), fetchCreateMenu(), fetchCreateTenant(), fetchDeleteItemUnit(), fetchDeleteMenu(), fetchDeleteTenant(), fetchItemUnitList(), fetchMenuTree() (+43 more)
### Community 16 - "Community 16"
Cohesion: 0.05
Nodes (56): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+48 more)
Cohesion: 0.04
Nodes (57): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchCreateRegionStaff(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchDeleteRegionStaff(), fetchHospitalList() (+49 more)
### Community 17 - "Community 17"
Cohesion: 0.05
Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more)
Nodes (53): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo() (+45 more)
### Community 18 - "Community 18"
Cohesion: 0.05
Nodes (46): completePilotApplication(), deletePilotApplication(), fetchCreateExhibitionAppointment(), fetchDeleteExhibitionAppointment(), fetchExhibitionAppointmentList(), fetchPilotApplicationList(), fetchUpdateExhibitionAppointment(), startPilotApplication() (+38 more)
Cohesion: 0.04
Nodes (40): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+32 more)
### Community 19 - "Community 19"
Cohesion: 0.05
Nodes (36): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+28 more)
Cohesion: 0.04
Nodes (18): fetchProductionWorkOrderQcInfo(), fetchSampleInsurance(), fetchSampleStorageCert(), fetchUpdateSampleStorageCert(), dash(), fmtMoney(), auditorDisplay(), dash() (+10 more)
### Community 20 - "Community 20"
Cohesion: 0.06
Nodes (25): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+17 more)
Cohesion: 0.05
Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more)
### Community 21 - "Community 21"
Cohesion: 0.06
Nodes (25): fetchUpdatePostBoundRoles(), fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload() (+17 more)
Nodes (40): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), appendTaskResultSections(), batchTasksForPanelSlot() (+32 more)
### Community 22 - "Community 22"
Cohesion: 0.06
Nodes (26): fetchProductionTaskList(), createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments() (+18 more)
Cohesion: 0.05
Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
### Community 23 - "Community 23"
Cohesion: 0.1
Nodes (2): WebSocketClient, TableCache
Cohesion: 0.06
Nodes (25): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+17 more)
### Community 24 - "Community 24"
Cohesion: 0.09
Nodes (31): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+23 more)
Cohesion: 0.1
Nodes (2): WebSocketClient, TableCache
### Community 25 - "Community 25"
Cohesion: 0.06
Nodes (20): fetchEthnicList(), fetchSampleFreightDetail(), fetchSampleFreightList(), fetchSampleFreightSampleOptions(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd() (+12 more)
### Community 26 - "Community 26"
Cohesion: 0.08
Nodes (30): fetchCreateTenant(), fetchDeleteTenant(), fetchTenantList(), fetchUpdateTenant(), fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail(), fetchEnvDetectItemList() (+22 more)
### Community 27 - "Community 27"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 27 - "Community 27"
Cohesion: 0.08
Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more)
### Community 28 - "Community 28"
Cohesion: 0.08
Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more)
### Community 29 - "Community 29"
Cohesion: 0.09
Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more)
Cohesion: 0.11
Nodes (25): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), clearTableSelection(), handleBatchComplete(), handleBatchStart() (+17 more)
### Community 30 - "Community 30"
Cohesion: 0.1
Nodes (22): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo() (+14 more)
Cohesion: 0.09
Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more)
### Community 31 - "Community 31"
Cohesion: 0.13
Nodes (19): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+11 more)
Cohesion: 0.11
Nodes (12): fetchCollectionProtocolList(), handleBatchDelete(), handleDelete(), handleDropdownCommand(), handleEdit(), handleReset(), handleSearch(), loadData() (+4 more)
### Community 32 - "Community 32"
Cohesion: 0.1
Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more)
### Community 33 - "Community 33"
Cohesion: 0.13
Nodes (19): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchDeleteCellSampleType(), fetchUpdateCellSampleType(), handleAdd(), handleBatchDelete(), handleDelete(), handleDropdownCommand() (+11 more)
### Community 34 - "Community 34"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
### Community 35 - "Community 35"
Cohesion: 0.13
Nodes (8): chipTitle(), confirmPicker(), memberOptionLabel(), removeUser(), resolveUser(), signBadge(), signBadgeText(), updateSelect()
### Community 36 - "Community 36"
### Community 34 - "Community 34"
Cohesion: 0.2
Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more)
### Community 37 - "Community 37"
### Community 35 - "Community 35"
Cohesion: 0.16
Nodes (14): fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail(), fetchEnvDetectItemList(), fetchEnvDetectItemsActive(), fetchUpdateEnvDetectItem(), handleAdd(), handleBatchDelete() (+6 more)
### Community 36 - "Community 36"
Cohesion: 0.12
Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit()
### Community 38 - "Community 38"
### Community 37 - "Community 37"
Cohesion: 0.16
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
### Community 39 - "Community 39"
### Community 38 - "Community 38"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 39 - "Community 39"
Cohesion: 0.13
Nodes (4): fetchPresignOrderDetail(), fetchPresignOrderList(), fetchPresignTransferReceiptConfirmedList(), fetchPresignTransferReceiptConfirmList()
### Community 40 - "Community 40"
Cohesion: 0.25
Nodes (12): buildFlowItems(), isCcProgressItem(), peopleDisplayNames(), progressNodeStatusText(), progressPersonLinesFromItem(), statusText(), displayName(), formatProgressPersonLines() (+4 more)
### Community 41 - "Community 41"
Cohesion: 0.33
Nodes (13): buildProtocolPaymentFlowSteps(), dash(), financeReceiptTime(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit() (+5 more)
### Community 41 - "Community 41"
### Community 42 - "Community 42"
Cohesion: 0.19
Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab()
### Community 42 - "Community 42"
### Community 43 - "Community 43"
Cohesion: 0.24
Nodes (10): buildPrintDocumentHtml(), cleanupPrintIframe(), embedSopImagesAsDataUrls(), getSopA4ContentHeightPx(), getSopA4EditorHeightPx(), normalizeSopCheckboxes(), prepareSopHtmlForPrint(), printSopHtml() (+2 more)
### Community 44 - "Community 44"
### Community 45 - "Community 45"
Cohesion: 0.26
Nodes (8): fetchSampleStorageCertQc(), saveSampleStorageCertQc(), applyDetail(), handleSave(), loadDetail(), resetFormKeys(), resolveStorageCertQcTypeKey(), supportsStorageCertQc()
### Community 45 - "Community 45"
Cohesion: 0.42
Nodes (9): buildPaymentFlowSteps(), dash(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit(), receiptSubmitTime() (+1 more)
### Community 46 - "Community 46"
Cohesion: 0.25
Nodes (2): handleTouchEnd(), setTransition()
### Community 47 - "Community 47"
### Community 48 - "Community 48"
Cohesion: 0.28
Nodes (1): FireworkSystem
### Community 49 - "Community 49"
Cohesion: 0.31
Nodes (4): annotateForkEdges(), chainApiToElements(), walkAncestorBranch(), walkAncestorParents()
Nodes (5): fetchMyProductionMaterialConsumptions(), displayConsumptionQty(), handleReset(), handleSearch(), loadData()
### Community 51 - "Community 51"
Cohesion: 0.25
Nodes (3): companyProgressTagType(), companyProgressText(), companyGroupProgress()
### Community 53 - "Community 53"
Cohesion: 0.39
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
### Community 54 - "Community 54"
### Community 52 - "Community 52"
Cohesion: 0.36
Nodes (1): TableHeightCalculator
### Community 56 - "Community 56"
### Community 54 - "Community 54"
Cohesion: 0.33
Nodes (2): logApiCryptoBootInfo(), maskSecret()
### Community 57 - "Community 57"
### Community 55 - "Community 55"
Cohesion: 0.29
Nodes (1): fetchQcMonthTemplateList()
### Community 58 - "Community 58"
### Community 56 - "Community 56"
Cohesion: 0.29
Nodes (2): fetchStorageReleaseReviewDetail(), fetchStorageReleaseReviewList()
### Community 59 - "Community 59"
### Community 57 - "Community 57"
Cohesion: 0.4
Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount()
### Community 60 - "Community 60"
### Community 58 - "Community 58"
Cohesion: 0.5
Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions()
### Community 62 - "Community 62"
### Community 60 - "Community 60"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 65 - "Community 65"
### Community 61 - "Community 61"
Cohesion: 0.4
Nodes (1): fetchQcMaterialAmountAuthUsers()
### Community 64 - "Community 64"
Cohesion: 0.67
Nodes (2): isCurrentPending(), isNotReached()
### Community 66 - "Community 66"
### Community 65 - "Community 65"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 68 - "Community 68"
### Community 67 - "Community 67"
Cohesion: 0.5
Nodes (1): fetchStorageEntrySmsList()
### Community 69 - "Community 69"
### Community 68 - "Community 68"
Cohesion: 0.5
Nodes (1): fetchQcSmsList()
### Community 73 - "Community 73"
### Community 72 - "Community 72"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 76 - "Community 76"
### Community 75 - "Community 75"
Cohesion: 1.0
Nodes (2): copyTextToClipboard(), copyTextWithFallback()
### Community 77 - "Community 77"
### Community 78 - "Community 78"
Cohesion: 1.0
Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles()
### Community 94 - "Community 94"
### Community 93 - "Community 93"
Cohesion: 1.0
Nodes (2): emptyTabCounts(), useProductionWorkOrderTabStepCounts()
### Community 97 - "Community 97"
### Community 96 - "Community 96"
Cohesion: 1.0
Nodes (2): initialProgress(), useQcWorkOrderAttachmentExport()
### Community 98 - "Community 98"
### Community 97 - "Community 97"
Cohesion: 1.0
Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts()
## Knowledge Gaps
- **Thin community `Community 23`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
- **Thin community `Community 24`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 46`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 47`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
- **Thin community `Community 52`** (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 54`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
- **Thin community `Community 54`** (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 56`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
- **Thin community `Community 55`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 57`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts`
- **Thin community `Community 56`** (7 nodes): `batchReviewStorageRelease()`, `batchStoreSample()`, `fetchStorageReleaseReviewDetail()`, `fetchStorageReleaseReviewList()`, `storeSample()`, `submitStorageReleaseReview()`, `storage-release-review.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 58`** (7 nodes): `batchReviewStorageRelease()`, `batchStoreSample()`, `fetchStorageReleaseReviewDetail()`, `fetchStorageReleaseReviewList()`, `storeSample()`, `submitStorageReleaseReview()`, `storage-release-review.ts`
- **Thin community `Community 57`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 59`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
- **Thin community `Community 58`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 60`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
- **Thin community `Community 60`** (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 62`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
- **Thin community `Community 61`** (5 nodes): `fetchBatchRevokeQcMaterialAmountAuth()`, `fetchGrantQcMaterialAmountAuth()`, `fetchQcMaterialAmountAuthUsers()`, `fetchRevokeQcMaterialAmountAuth()`, `qc-material-amount-auth.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 65`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts`
- **Thin community `Community 64`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 66`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
- **Thin community `Community 65`** (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 68`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
- **Thin community `Community 67`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 69`** (4 nodes): `fetchGenerateQcSms()`, `fetchQcSmsList()`, `fetchToggleQcSmsNotified()`, `qc-sms-notification.ts`
- **Thin community `Community 68`** (4 nodes): `fetchGenerateQcSms()`, `fetchQcSmsList()`, `fetchToggleQcSmsNotified()`, `qc-sms-notification.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 73`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
- **Thin community `Community 72`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 76`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
- **Thin community `Community 75`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 77`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
- **Thin community `Community 78`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 94`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
- **Thin community `Community 93`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 97`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
- **Thin community `Community 96`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 98`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
- **Thin community `Community 97`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.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 `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 28`, `Community 29`, `Community 35`, `Community 47`?**
_High betweenness centrality (0.278) - this node is a cross-community bridge._
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 25`, `Community 26`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 41`, `Community 44`, `Community 49`, `Community 57`, `Community 58`, `Community 68`, `Community 69`?**
- **Why does `push()` connect `Community 6` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 24`, `Community 27`, `Community 28`, `Community 30`, `Community 40`?**
_High betweenness centrality (0.292) - this node is a cross-community bridge._
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 25`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 35`, `Community 39`, `Community 42`, `Community 45`, `Community 48`, `Community 55`, `Community 56`, `Community 61`, `Community 67`, `Community 68`?**
_High betweenness centrality (0.272) - this node is a cross-community bridge._
- **Why does `Number()` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 20`, `Community 22`, `Community 24`, `Community 25`, `Community 39`, `Community 40`, `Community 45`?**
_High betweenness centrality (0.153) - this node is a cross-community bridge._
- **Are the 342 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
_`get()` has 342 INFERRED edges - model-reasoned connections that need verification._
- **Are the 128 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
_`push()` has 128 INFERRED edges - model-reasoned connections that need verification._
- **Are the 98 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?**
_`Number()` has 98 INFERRED edges - model-reasoned connections that need verification._
- **Why does `Number()` connect `Community 1` to `Community 0`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 23`, `Community 25`, `Community 27`, `Community 38`, `Community 41`, `Community 48`?**
_High betweenness centrality (0.151) - this node is a cross-community bridge._
- **Are the 344 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
_`get()` has 344 INFERRED edges - model-reasoned connections that need verification._
- **Are the 127 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
_`push()` has 127 INFERRED edges - model-reasoned connections that need verification._
- **Are the 97 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?**
_`Number()` has 97 INFERRED edges - model-reasoned connections that need verification._
- **Are the 34 inferred relationships involving `emit()` (e.g. with `updateSamples()` and `notifyChanged()`) actually correct?**
_`emit()` has 34 INFERRED edges - model-reasoned connections that need verification._
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">3846 nodes &middot; 5638 edges &middot; 404 communities</div>
<div id="stats">3834 nodes &middot; 5593 edges &middot; 404 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 145, "community_name": "Community 145", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 146, "community_name": "Community 146", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 34, "community_name": "Community 34", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 34, "community_name": "Community 34", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 34, "community_name": "Community 34", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 34, "community_name": "Community 34", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 34, "community_name": "Community 34", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective",Line 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": 571}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 251}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 186}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 136}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 135}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 131}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 129}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 126}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 94}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 89}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 87}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 84}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 83}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 80}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 73}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 68}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 67}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 61}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 57}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 56}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 46}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 43}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 43}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 42}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 39}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 38}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 38}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 37}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 35}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 33}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 31}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 24}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 24}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 23}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 22}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 20}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 19}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 17}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 17}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 16}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 14}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 14}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 13}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 13}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 12}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 10}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 9}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 9}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 9}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 9}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 9}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 9}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 9}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 8}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 8}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 8}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 7}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 7}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 7}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 6}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 5}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 5}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 5}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 4}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 4}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 4}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 4}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 4}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 4}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 4}, {"cid": 70, "color": "#4E79A7", "label": "CommuniLine truncated
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 54, "community_name": "Community 54", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 54, "community_name": "Community 54", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 54, "community_name": "Community 54", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 54, "community_name": "Community 54", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 54, "community_name": "Community 54", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 54, "community_name": "Community 54", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 144, "community_name": "Community 144", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 145, "community_name": "Community 145", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 33, "community_name": "Community 33", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 33, "community_name": "Community 33", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 33, "community_name": "Community 33", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 33, "community_name": "Community 33", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 33, "community_name": "Community 33", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective",Line 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": 568}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 186}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 155}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 143}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 132}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 129}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 126}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 123}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 102}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 88}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 87}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 86}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 86}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 85}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 78}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 77}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 73}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 70}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 68}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 68}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 61}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 58}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 57}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 46}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 42}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 38}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 37}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 35}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 35}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 30}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 25}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 24}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 24}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 22}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 19}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 19}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 17}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 17}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 16}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 15}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 14}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 14}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 14}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 13}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 13}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 12}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 9}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 9}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 9}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 9}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 9}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 8}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 8}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 8}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 7}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 7}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 7}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 6}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 5}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 5}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 5}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 5}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 4}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 4}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 4}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 4}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 4}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 4}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 4}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 4}, {"cid": 70, "color": "#4E79A7", "label": "CommunLine truncated
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+25 -25
View File
@@ -17,8 +17,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1781272556.3597305,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1781272556.3601494,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1781272556.3606477,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1792141056.4307427,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1792139044.4306676,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1792207835.5380793,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1792206847.1791198,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1787887312.735774,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1787887320.8486114,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1788162977.6691713,
@@ -87,7 +87,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sample-freight-flatten.ts": 1791468382.3859549,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-labels.ts": 1782893910.9937537,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print-flow.ts": 1786601489.1530275,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1792028747.9199126,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1792208113.061491,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-work-order-card-meta.ts": 1788234947.3617282,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fetch-paged-list.ts": 1781272556.3903377,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-audit-flatten.ts": 1791868355.944149,
@@ -319,14 +319,14 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-video-player/index.vue": 1782893851.5118508,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-cutter-img/index.vue": 1782893851.4950457,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/TinyMceEditor/index.vue": 1787643214.0412707,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-transport-form-section/index.vue": 1791785224.3434331,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-transport-form-section/index.vue": 1792145332.1622937,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-attachment-mini-readonly/index.vue": 1785145511.689019,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/cell-product-lineage-qc-dialog/index.vue": 1785290453.1924367,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-freight-pick-table/index.vue": 1791540667.8527935,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/index.vue": 1785145511.6893718,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/finance-attachments.ts": 1781272556.6256514,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1787107329.207847,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/clinical-payment-confirm-timeline/index.vue": 1791162469.9343283,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/clinical-payment-confirm-timeline/index.vue": 1792143951.1833928,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/outbound-material-readonly-panel/index.vue": 1782893852.2706735,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-reception-dual-readonly-panel/index.vue": 1791471812.3709838,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/dialog-form-body/index.vue": 1782893852.1647537,
@@ -335,7 +335,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/special-sample-display/index.vue": 1783925058.170451,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1782893852.4089837,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1790217249.4347427,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-payment-detail-dialog/index.vue": 1791623760.681438,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-payment-detail-dialog/index.vue": 1792202737.3385131,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-production-info-cards/index.vue": 1791471821.0584927,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-handle-panel/index.vue": 1791860633.8460462,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-storage-release-info/index.vue": 1791471822.4833055,
@@ -431,7 +431,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material-usage.ts": 1782894693.658203,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/pre-transit-sample.ts": 1782204427.8344586,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-company.ts": 1781415389.1483624,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts": 1792075243.0424178,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts": 1792199724.7526388,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/chinaRegion.ts": 1781272556.6606348,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-entry-sms-notification.ts": 1781272556.660898,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/business.ts": 1781750847.0646322,
@@ -447,7 +447,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-waste-output.ts": 1787023802.604376,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/oaApproval.ts": 1792141011.8097997,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material-stock-check.ts": 1781427280.4992144,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation.ts": 1782893816.502791,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation.ts": 1792199726.4842353,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task.ts": 1783327864.9737554,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-item-param.ts": 1782446790.5508618,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/env-detect-item.ts": 1781272556.668322,
@@ -464,19 +464,19 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/protocol-sample-payment.ts": 1792075236.7229176,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1791862690.2468123,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1789715240.9777095,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/index.vue": 1792078851.1747634,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue": 1792079639.101142,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/index.vue": 1792202745.7217324,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue": 1792143951.183733,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1782893854.3402505,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/index.vue": 1792029084.1036322,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue": 1792029084.6662757,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightEditDialog.vue": 1791424830.3864272,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/my-material-usage/index.vue": 1791296731.262585,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1792078851.1662643,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1792202737.8532119,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue": 1782894806.0188534,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordDialog.vue": 1792078851.1693885,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1792078851.1700473,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordDialog.vue": 1792202738.902946,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1792202724.60167,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue": 1791162227.1000674,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1792079326.0241637,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1792143951.1766114,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue": 1792078851.1718132,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue": 1782893854.3675368,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/index.vue": 1791187848.6336563,
@@ -484,23 +484,23 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vue": 1791868912.5231571,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-reception-info/index.vue": 1791200162.920515,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1791504621.1780708,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue": 1792079135.457411,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue": 1792078851.1755798,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue": 1792202727.257655,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue": 1792202728.178532,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1791199760.6008983,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-chase/index.vue": 1792078851.1759324,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-chase/index.vue": 1792202729.212173,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/index.vue": 1791346519.2317798,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue": 1791868429.5362544,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue": 1791429008.46071,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue": 1791769656.733718,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue": 1792079135.4569967,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue": 1792202738.3284647,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue": 1792202725.2582252,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue": 1791328376.3211129,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/MaterialUsageTab.vue": 1783429524.4350624,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/MaterialStockCheckTab.vue": 1791328376.3238451,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/fullmoon-todo/index.vue": 1791199762.446884,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-entry-info/index.vue": 1791200163.9343994,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/index.vue": 1792078851.1768854,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue": 1792079135.4566004,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/InvoiceRequestTab.vue": 1792079135.4561322,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/index.vue": 1792208118.0859683,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue": 1792208127.1538856,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/InvoiceRequestTab.vue": 1792208128.4761107,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material-stock-log/index.vue": 1782893853.8275082,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/outside/Iframe.vue": 1782893853.8888595,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/operations/demo/index.vue": 1791296731.28513,
@@ -509,10 +509,10 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/operations/exhibition-appointment/index.vue": 1791328331.1522717,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/audit/index.vue": 1791346520.1101646,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/sample-info/index.vue": 1791187849.9149377,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1791437070.4771616,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1792143951.182839,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationAuditDialog.vue": 1782893853.885547,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1791775350.7223852,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1791775341.5097153,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1792199835.0448852,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1792143951.1814156,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue": 1782894808.1240149,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/dashboard/console/index.vue": 1791855531.6714394,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/tenant/index.vue": 1791328376.332648,
@@ -0,0 +1,487 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
import ts from 'typescript'
const base = new URL('../src/views/cell-storage/', import.meta.url)
const presign = 'presign-order/PresignOrderFormDialog.vue'
const panel = 'protocol/SamplePaymentPanel.vue'
const protocol = 'protocol/ProtocolFormDialog.vue'
function source(file: string) {
const text = readFileSync(new URL(file, base), 'utf8').match(
/<script setup lang="ts">([\s\S]*?)<\/script>/
)![1]
return ts.createSourceFile(file + '.ts', text, ts.ScriptTarget.Latest, true)
}
function functions(file: string, names: string[], env: Record<string, any>) {
env = {
isEdit: { value: false },
editDetailLoaded: { value: true },
loading: { value: false },
formReady: { value: true },
...env
}
const ast = source(file)
const text = ast.statements
.filter((n) => ts.isFunctionDeclaration(n) && names.includes(n.name!.text))
.map((n) => n.getText(ast))
.join('\n')
const js = ts.transpile(text, { target: ts.ScriptTarget.ES2022 })
return new Function(
...Object.keys(env),
`${js}; return { ${names.filter((n) => text.includes('function ' + n + '(')).join(',')} }`
)(...Object.values(env))
}
function expression(file: string, target: string, env: Record<string, any>) {
const ast = source(file)
let result = ''
function visit(node: ts.Node) {
if (ts.isBinaryExpression(node) && node.left.getText(ast) === target)
result = node.right.getText(ast)
if (ts.isVariableDeclaration(node) && node.name.getText(ast) === target && node.initializer)
result = node.initializer.getText(ast)
node.forEachChild(visit)
}
visit(ast)
assert.ok(result, `missing ${target}`)
const js = ts.transpile(`const result = ${result}`, { target: ts.ScriptTarget.ES2022 })
return new Function(...Object.keys(env), `${js}; return result`)(...Object.values(env))
}
function presignEnv(transfers: any[] = []) {
const writes: any[] = []
const warnings: string[] = []
const env: Record<string, any> = {
form: {
motherName: '测试',
sampleTypeIds: ['sample'],
transfers,
orderDate: '',
expectedDeliveryDate: '',
regionId: '',
hospitalId: '',
sampleCollectTime: '',
collectorUserId: '',
regionManagerUserId: '',
payMode: 'dj',
contractAmount: null,
orderStatus: 'active',
remark: ''
},
props: { orderId: 'order-1' },
saving: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v), success: () => {} },
fetchUpdatePresignOrder: async (_: string, body: any) =>
writes.push(JSON.parse(JSON.stringify(body))),
fetchUpdatePresignTransfer: async (_: string, body: any) =>
writes.push(JSON.parse(JSON.stringify(body))),
fetchCreatePresignTransfer: async (_: string, body: any) => {
writes.push(body)
return { id: 'new' }
},
fetchSubmitPresignTransferFinance: async () => {},
submitDfjTransfersOnSave: async () => [],
emit: () => {}
}
return { env, writes, warnings }
}
test('预签约清空可选字段后,更新请求仍携带空值', async () => {
const { env, writes } = presignEnv()
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
for (const key of [
'expectedDeliveryDate',
'regionId',
'hospitalId',
'sampleCollectTime',
'collectorUserId',
'regionManagerUserId',
'remark'
])
assert.equal(writes[0][key], '', key)
assert.equal(writes[0].contractAmount, null)
})
test('预签约保存转账清空凭证/日期/备注后请求携带空值', async () => {
const { env, writes } = presignEnv([
{ id: 't', payStatus: 'dfj', attachments: [], transferTime: '', remark: '' }
])
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes[1].transferAttachments, '')
assert.equal(writes[1].transferTime, '')
assert.equal(writes[1].remark, '')
})
test('预签约提交财务前同步清空凭证', async () => {
const { env, writes } = presignEnv()
await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
id: 't',
payStatus: 'dfj',
attachments: []
})
assert.equal(writes[0].transferAttachments, '')
})
test('预签约凭证上传时禁止保存订单', async () => {
const { env, writes, warnings } = presignEnv([
{ payStatus: 'dfj', attachments: [{ _uploading: true }] }
])
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
test('预签约凭证上传时禁止提交财务', async () => {
const { env, writes, warnings } = presignEnv()
await functions(presign, ['submitTransfer', 'attachmentsToUrls'], env).submitTransfer({
id: 't',
payStatus: 'dfj',
attachments: [{ _uploading: true }]
})
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
for (const attachments of [[], [{ fileUrl: 'new.pdf' }], undefined]) {
test(`协议整体保存使用当前附件数组(${JSON.stringify(attachments)})`, () => {
const payments = expression(protocol, 'payload.samplePayments', {
formData: {
samplePayments: [
{ schedules: [{ transfers: [{ transferAttachments: 'old.pdf', attachments }] }] }
]
},
toIntOrUndefined: () => undefined,
toFloatOrUndefined: () => undefined,
parseInstallmentRatios: () => []
})
assert.equal(
payments[0].schedules[0].transfers[0].transferAttachments,
attachments ? attachments.map((f) => f.fileUrl).join(',') : 'old.pdf'
)
})
}
test('样本缴费转账行保存删除全部凭证', async () => {
let written: any
const tr = {
id: 't',
transferType: 'bank',
transferAmount: 1,
attachments: [],
transferAttachments: 'old.pdf'
}
const env = {
props: { protocolId: 'p', modelValue: [{ schedules: [{ id: 's', transfers: [tr] }] }] },
savingKey: { value: '' },
isTransferLocked: () => false,
emitChange: () => {},
ElMessage: { warning: () => {}, success: () => {} },
fetchUpdatePaymentTransfer: async (_: string, body: any) => {
written = JSON.parse(JSON.stringify(body))
return { transferAttachments: '', attachments: [] }
}
}
await functions(
panel,
[
'saveTransferRow',
'getTransfer',
'validateTransferRow',
'transferRowKey',
'attachmentsToUrls',
'ensureTransferAttachments',
'applySavedTransfer'
],
env
).saveTransferRow(0, 0, 0)
assert.equal(written.transferAttachments, '')
assert.equal(tr.transferAttachments, '')
})
for (const file of [presign, panel]) {
for (const fail of [false, true]) {
test(`${file} 上传${fail ? '失败' : '成功'}时删除前一个附件不应错位`, async () => {
let resolve!: (value: any) => void
let reject!: (error: Error) => void
const pending = new Promise((yes, no) => {
resolve = yes
reject = no
})
const tr = { attachments: [{ id: 'old', fileUrl: 'old.pdf' }] }
const env = {
form: { transfers: [tr] },
props: { modelValue: [{ schedules: [{ transfers: [tr] }] }] },
emitChange: () => {},
fetchUploadAttachment: () => pending,
ElMessage: { warning: () => {}, error: () => {} }
}
const fns = functions(
file,
['handleTransferAttachmentChange', 'getTransfer', 'ensureTransferAttachments'],
env
)
const upload = { raw: new File(['x'], 'new.pdf') }
const saving =
file === presign
? fns.handleTransferAttachmentChange(0, upload)
: fns.handleTransferAttachmentChange(0, 0, 0, upload)
tr.attachments.splice(0, 1)
if (fail) reject(new Error('upload failed'))
else resolve({ id: 'new', fileUrl: 'new.pdf' })
await saving
assert.equal(tr.attachments.length, fail ? 0 : 1)
if (!fail) assert.equal(tr.attachments[0].fileUrl, 'new.pdf')
})
}
}
for (const handler of ['uploadProtocolAttachmentFile', 'handleDetailAttachmentChange']) {
for (const fail of [false, true]) {
test(`协议 ${handler} 上传${fail ? '失败' : '成功'}不受前项删除影响`, async () => {
let resolve!: (value: any) => void
let reject!: (error: Error) => void
const pending = new Promise((yes, no) => {
resolve = yes
reject = no
})
const list = [{ id: 'old', fileUrl: 'old.pdf' }]
const env = {
formData: { paymentDetails: [{ attachments: list }] },
fetchUploadAttachment: () => pending,
nextTick: (cb: () => void) => cb(),
ElMessage: { error: () => {} }
}
const fns = functions(protocol, [handler], env)
const upload = { raw: new File(['x'], 'new.pdf') }
const saving =
handler === 'uploadProtocolAttachmentFile'
? fns[handler](upload, list, 'protocol', {})
: fns[handler](0, upload)
list.splice(0, 1)
if (fail) reject(new Error('upload failed'))
else resolve({ id: 'new', fileUrl: 'new.pdf' })
await saving
assert.equal(list.length, fail ? 0 : 1)
if (!fail) assert.equal(list[0].fileUrl, 'new.pdf')
})
}
}
for (const field of [
'attachments',
'consentAttachments',
'healthSurveyAttachments',
'paymentDetails',
'samplePayments'
]) {
test(`协议 ${field} 上传中禁止整体保存`, async () => {
let validated = false
const warnings: string[] = []
const file = { _uploading: true }
const formData: any = {
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
paymentDetails: [],
samplePayments: []
}
formData[field] =
field === 'paymentDetails'
? [{ attachments: [file] }]
: field === 'samplePayments'
? [{ schedules: [{ transfers: [{ attachments: [file] }] }] }]
: [file]
const env = {
formData,
formRef: {
value: {
validate: async () => {
validated = true
throw new Error('validation')
}
}
},
submitLoading: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v) }
}
await functions(protocol, ['handleSubmit'], env).handleSubmit()
assert.equal(validated, false)
assert.equal(warnings.length, 1)
})
}
test('正式协议清空主单附件、客户和采集字段后提交空值', () => {
const formData = new Proxy(
{
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
samples: [],
protocolAttachments: 'old.pdf',
consentFormUrls: 'old-consent.pdf',
healthSurveyFormUrls: 'old-health.pdf'
},
{ get: (o, key) => (key in o ? o[key as keyof typeof o] : '') }
)
const payload = JSON.parse(
JSON.stringify(
expression(protocol, 'payload', {
formData,
protocolAttachmentUrls: '',
consentAttachmentUrls: '',
healthSurveyAttachmentUrls: ''
})
)
)
for (const key of ['protocolAttachments', 'consentFormUrls', 'healthSurveyFormUrls', 'remark'])
assert.equal(payload[key], '', key)
for (const key of ['motherPhone', 'motherEmail', 'fatherPhone', 'newbornIdCard'])
assert.equal(payload.partyCustomer[key], '', key)
for (const key of ['hospitalId', 'medicalStaff', 'sampleCollectTime'])
assert.equal(payload.collectionInfo[key], '', key)
})
test('协议详情加载失败后禁止保存未加载的表单', async () => {
let validated = false
const warnings: string[] = []
const env = {
isEdit: { value: true },
editDetailLoaded: { value: false },
formData: {
attachments: [],
consentAttachments: [],
healthSurveyAttachments: [],
paymentDetails: [],
samplePayments: []
},
formRef: {
value: {
validate: async () => {
validated = true
throw new Error('validation')
}
}
},
submitLoading: { value: false },
ElMessage: { warning: (v: string) => warnings.push(v) }
}
await functions(protocol, ['handleSubmit'], env).handleSubmit()
assert.equal(validated, false)
assert.equal(warnings.length, 1)
})
test('协议详情读取失败保持未就绪并提示重试', async () => {
const messages: string[] = []
const ready = { value: true }
const env = {
props: { listRow: { id: 'p' }, editId: 'p' },
isEdit: { value: false },
dialogTitle: { value: '' },
editingId: { value: '' },
editDetailLoaded: ready,
formData: {},
resetForm: () => {},
dialogFormLoading: { value: false },
loadEthnicOptions: async () => {},
fetchCollectionProtocolDetail: async () => {
throw new Error('network')
},
ElMessage: { error: (v: string) => messages.push(v) },
emit: () => {}
}
await functions(protocol, ['handleOpenForEdit'], env).handleOpenForEdit()
assert.equal(ready.value, false)
assert.equal(messages.length, 1)
})
test('编辑后新增协议不沿用上一条协议ID', () => {
const editingId = { value: 'previous-protocol' }
const env = {
editingId,
isEdit: { value: true },
dialogTitle: { value: '' },
resetForm: () => {},
formData: {},
DEFAULT_MOTHER_ETHNIC: '',
loadEthnicOptions: () => {},
userStore: { getUserInfo: {} }
}
functions(protocol, ['handleOpenForAdd'], env).handleOpenForAdd()
assert.equal(editingId.value, null)
})
function presignOpenWatcher(env: Record<string, any>) {
const ast = source(presign)
const statement = ast.statements.find(
(n) =>
ts.isExpressionStatement(n) &&
ts.isCallExpression(n.expression) &&
n.expression.expression.getText(ast) === 'watch'
)!
let callback: (open: boolean) => Promise<void>
new Function(
...Object.keys(env),
'watch',
ts.transpile(statement.getText(ast), { target: ts.ScriptTarget.ES2022 })
)(...Object.values(env), (_: unknown, cb: typeof callback) => {
callback = cb
})
return callback!
}
test('预签约从字典加载开始即处于加载中,全部初始化后才就绪', async () => {
let resolve!: () => void
const pending = new Promise<void>((yes) => {
resolve = yes
})
const loading = { value: false }
const formReady = { value: true }
const events: string[] = []
const open = presignOpenWatcher({
props: { orderId: 'p' },
loading,
formReady,
resetForm: () => {},
loadLookups: () => pending,
loadDetail: async () => {
events.push('detail')
},
ElMessage: { error: () => {} },
emit: () => {}
})
const initialization = open(true)
const during = { loading: loading.value, ready: formReady.value }
resolve()
await initialization
assert.deepEqual(during, { loading: true, ready: false })
assert.deepEqual(events, ['detail'])
assert.equal(loading.value, false)
assert.equal(formReady.value, true)
})
for (const stage of ['lookups', 'detail']) {
test(`预签约${stage}加载失败保持未就绪并提示重试`, async () => {
const loading = { value: false }
const formReady = { value: true }
const errors: string[] = []
const open = presignOpenWatcher({
props: { orderId: 'p' },
loading,
formReady,
resetForm: () => {},
loadLookups: async () => {
if (stage === 'lookups') throw new Error('network')
},
loadDetail: async () => {
if (stage === 'detail') throw new Error('network')
},
ElMessage: { error: (v: string) => errors.push(v) },
emit: () => {}
})
await open(true).catch(() => {})
assert.equal(formReady.value, false)
assert.equal(loading.value, false)
assert.equal(errors.length, 1)
})
}
for (const state of [
{ loading: true, ready: false },
{ loading: false, ready: false }
]) {
test(`预签约未初始化成功不得保存:${JSON.stringify(state)}`, async () => {
const { env, writes, warnings } = presignEnv()
env.loading = { value: state.loading }
env.formReady = { value: state.ready }
await functions(presign, ['handleSave', 'attachmentsToUrls'], env).handleSave()
assert.equal(writes.length, 0)
assert.equal(warnings.length, 1)
})
}
+6
View File
@@ -43,6 +43,12 @@ export function fetchCollectionProtocolDetail(id: string) {
})
}
export function fetchCollectionProtocolStatusChanges(protocolId: string) {
return request.get<{ list: Array<Record<string, unknown>> }>({
url: `/web/admin/collection-protocols/${protocolId}/status-changes`
})
}
export function fetchCollectionProtocolAuditList(params?: {
page?: number
pageSize?: number
+6
View File
@@ -110,6 +110,12 @@ export function fetchSubmitDonationAudit(
})
}
export function fetchDonationStatusChanges(donationId: string) {
return request.get<{ list: Array<Record<string, unknown>> }>({
url: `/web/admin/research-donations/${donationId}/status-changes`
})
}
// 状态变更
export function fetchDonationStatusChange(
donationId: string,
@@ -15,10 +15,6 @@
<div v-loading="loading" class="sample-pay-detail">
<template v-if="detail">
<div class="detail-summary form-grid-4">
<div class="detail-field">
<label>协议号</label>
<span>{{ detail.protocolNo || '—' }}</span>
</div>
<div class="detail-field">
<label>客户</label>
<span>
@@ -72,7 +72,13 @@
>
<template #default="{ row }">{{ fmtMoney(row.price) }}</template>
</el-table-column>
<el-table-column label="存储年限" min-width="96" align="center" header-align="center">
<el-table-column
v-if="!hideCollectDetailFields"
label="存储年限"
min-width="96"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.storageYears != null ? row.storageYears + '年' : '—'
}}</template>
@@ -413,8 +419,9 @@
siteOptions?: Api.Site.SiteItem[]
expressCompanyOptions?: Api.ExpressCompany.ExpressCompanyItem[]
showPrice?: boolean
/** 科研捐赠等:列表与样本弹窗均不展示存储年限及相关采集明细 */
hideCollectDetailFields?: boolean
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列与存储年限) */
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列) */
hideDialogCollectAmountUnit?: boolean
autoGenerateSampleCode?: boolean
reserveSampleCode?: () => Promise<string>
@@ -1,6 +1,6 @@
<!-- OA 审批详情 · 评论输入(列表已并入审批进度时间线) -->
<template>
<section v-if="instanceStatus === 'running'" class="oa-comment-panel">
<section class="oa-comment-panel">
<div class="oa-comment-panel__composer">
<div class="oa-comment-panel__editor">
<el-input
@@ -147,6 +147,14 @@ export const payStatus: StatusRegistry = {
export const payStatusLabel = createLabelGetter(payStatus)
export const payStatusTagType = createTagTypeGetter(payStatus)
/** 采集协议开票申请状态(biz_protocol_invoice_request.status) */
export const invoiceRequestStatus: StatusRegistry = {
dkp: { label: '待开票', tagType: 'warning' },
ykp: { label: '已开票', tagType: 'success' }
}
export const invoiceRequestStatusLabel = createLabelGetter(invoiceRequestStatus)
export const invoiceRequestStatusTagType = createTagTypeGetter(invoiceRequestStatus)
export const auditStatus: StatusRegistry = {
shtg: { label: '审核通过', tagType: 'success' },
shth: { label: '审核退回', tagType: 'danger' }
@@ -12,7 +12,7 @@
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/客户/样本/电话"
placeholder="客户/样本/电话"
clearable
@keyup.enter="handleSearch"
/>
@@ -69,13 +69,6 @@
align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
show-overflow-tooltip
/>
<el-table-column label="客户" min-width="140" align="center">
<template #default="{ row }">
<div>{{ row.motherName || '—' }}</div>
@@ -323,12 +316,12 @@
function formatAmount(v: unknown) {
if (v == null || v === '') return '—'
return formatMoneyDisplay(v)
return formatMoneyDisplay(v as string | number)
}
function formatAmountYuan(v: unknown) {
if (v == null || v === '') return '—'
return `${formatMoneyDisplay(v)} 元`
return `${formatMoneyDisplay(v as string | number)} 元`
}
function hasPaidAmount(row: Record<string, any>) {
@@ -414,7 +407,8 @@
const scheduleId = String(row.scheduleId || '')
const samplePaymentId = String(row.samplePaymentId || '')
if (!scheduleId || !samplePaymentId) return
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${row.scheduleNo || '—'} 期转账记录`
const titleParts = [row.motherName, row.sampleCode].filter(Boolean)
periodDialogTitle.value = `${titleParts.join(' · ')}${titleParts.length ? ' · ' : ''}第 ${row.scheduleNo || '—'} 期转账记录`
periodDialogVisible.value = true
periodLoading.value = true
periodTransfers.value = []
@@ -55,10 +55,6 @@
</div>
</div>
<div class="pending-card__grid">
<div class="pending-card__field">
<label>协议号</label>
<span>{{ row.protocolNo || '-' }}</span>
</div>
<div class="pending-card__field">
<label>包含期数</label>
<span>{{ row.installmentSummary || '-' }}</span>
@@ -70,7 +66,12 @@
</div>
<div class="pending-card__foot">
<div class="pending-card__meta">
<el-tag type="warning" size="small">待开票</el-tag>
<el-tag
:type="invoiceRequestStatusTagType(String(row.status || 'dkp'))"
size="small"
>
{{ invoiceRequestStatusLabel(String(row.status || 'dkp')) }}
</el-tag>
</div>
<el-button class="btn-op-item primary" @click="openProcess(row)">
<el-icon><Check /></el-icon>处理开票
@@ -199,8 +200,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === 'ykp' ? 'success' : 'warning'" size="small">
{{ row.status === 'ykp' ? '已开票' : '待开票' }}
<el-tag
:type="invoiceRequestStatusTagType(String(row.status || ''))"
size="small"
>
{{ invoiceRequestStatusLabel(String(row.status || '')) }}
</el-tag>
</template>
</el-table-column>
@@ -254,9 +258,6 @@
<el-form-item label="客户姓名">
<span class="view-field-text">{{ processRow.motherName || '—' }}</span>
</el-form-item>
<el-form-item label="协议号">
<span class="view-field-text">{{ processRow.protocolNo || '—' }}</span>
</el-form-item>
<el-form-item label="身份证号">
<span class="view-field-text">{{ maskIdCard(processRow.motherIdCard) || '—' }}</span>
</el-form-item>
@@ -371,9 +372,6 @@
<el-form-item label="客户姓名">
<span class="view-field-text">{{ viewRow.motherName || '—' }}</span>
</el-form-item>
<el-form-item label="协议号">
<span class="view-field-text">{{ viewRow.protocolNo || '—' }}</span>
</el-form-item>
<el-form-item label="身份证号">
<span class="view-field-text">{{ maskIdCard(viewRow.motherIdCard) || '—' }}</span>
</el-form-item>
@@ -394,8 +392,11 @@
</el-form-item>
<el-form-item label="状态">
<span class="view-field-tag">
<el-tag :type="viewRow.status === 'ykp' ? 'success' : 'warning'" size="small">
{{ viewRow.status === 'ykp' ? '已开票' : '待开票' }}
<el-tag
:type="invoiceRequestStatusTagType(String(viewRow.status || ''))"
size="small"
>
{{ invoiceRequestStatusLabel(String(viewRow.status || '')) }}
</el-tag>
</span>
</el-form-item>
@@ -507,6 +508,10 @@
import { maskIdCard } from '@/utils/mask-sensitive'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { transferTypeLabel, transferTypeTagType, loadTransferTypeDict } from '@/utils/protocol-payment-labels'
import {
invoiceRequestStatusLabel,
invoiceRequestStatusTagType
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
defineOptions({ name: 'CellStorageInvoiceRequestTab' })
@@ -93,7 +93,12 @@
</div>
<div class="pending-card__foot">
<div class="pending-card__meta">
<el-tag type="warning" size="small">退费确认中</el-tag>
<el-tag
:type="payStatusTagType(String(row.payStatus || 'tfqrz'))"
size="small"
>
{{ payStatusLabel(String(row.payStatus || 'tfqrz')) }}
</el-tag>
<span>提交 {{ formatDisplayOrDash(row.createTime) }}</span>
</div>
<el-button class="btn-op-item primary" @click="openConfirm(row)">
@@ -332,8 +337,13 @@
align="center"
header-align="center"
>
<template #default>
<el-tag type="success" size="small">已退费</el-tag>
<template #default="{ row }">
<el-tag
:type="payStatusTagType(String(row.payStatus || 'ytf'))"
size="small"
>
{{ payStatusLabel(String(row.payStatus || 'ytf')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -485,6 +495,8 @@
import {
formatPaymentDueDate,
payModeLabel,
payStatusLabel,
payStatusTagType,
protocolStatusLabel,
protocolStatusDisplayLabel,
protocolStatusTagType,
@@ -55,9 +55,7 @@
<el-form-item label="关键词">
<el-input
v-model="pendingSearchForm.keyword"
:placeholder="
isPresignTab ? '协议号/客户姓名' : '协议号/样本编码/客户姓名/身份证号'
"
:placeholder="isPresignTab ? '客户姓名' : '样本编码/客户姓名/身份证号'"
clearable
@keyup.enter="handlePendingSearch"
/>
@@ -107,15 +105,6 @@
(index) => (pendingPagination.page - 1) * pendingPagination.pageSize + index + 1
"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
</el-table-column>
<el-table-column
prop="sampleCode"
:label="isPresignTab ? '样本类型' : '样本编码'"
@@ -267,8 +256,13 @@
align="center"
header-align="center"
>
<template #default>
<el-tag type="warning" size="small">付款确认中</el-tag>
<template #default="{ row }">
<el-tag
:type="payStatusTagType(String(row.payStatus || 'jfqrz'))"
size="small"
>
{{ payStatusLabel(String(row.payStatus || 'jfqrz')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -334,9 +328,7 @@
<el-form-item label="关键词">
<el-input
v-model="confirmedSearchForm.keyword"
:placeholder="
isPresignTab ? '协议号/客户姓名' : '协议号/样本编码/客户姓名/身份证号'
"
:placeholder="isPresignTab ? '客户姓名' : '样本编码/客户姓名/身份证号'"
clearable
@keyup.enter="handleConfirmedSearch"
/>
@@ -386,15 +378,6 @@
(index) => (confirmedPagination.page - 1) * confirmedPagination.pageSize + index + 1
"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
</el-table-column>
<el-table-column
prop="sampleCode"
:label="isPresignTab ? '样本类型' : '样本编码'"
@@ -546,8 +529,13 @@
align="center"
header-align="center"
>
<template #default>
<el-tag type="success" size="small">付款完成</el-tag>
<template #default="{ row }">
<el-tag
:type="payStatusTagType(String(row.payStatus || 'yjf'))"
size="small"
>
{{ payStatusLabel(String(row.payStatus || 'yjf')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -715,6 +703,8 @@
import {
formatPaymentDueDate,
payModeLabel,
payStatusLabel,
payStatusTagType,
protocolStatusDisplayLabel,
protocolStatusTagType,
loadTransferTypeDict,
@@ -745,7 +735,7 @@
function formatPaymentAmount(v: unknown): string {
if (v == null || v === '') return '-'
return '¥' + formatMoneyDisplay(v)
return '¥' + formatMoneyDisplay(v as string | number)
}
function isPresignRow(row: Record<string, unknown> | null | undefined) {
@@ -7,7 +7,7 @@
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/客户/样本编码/电话"
placeholder="客户/样本编码/电话"
clearable
@keyup.enter="handleSearch"
/>
@@ -80,14 +80,6 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="客户" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<div class="customer-cell">
@@ -220,10 +212,6 @@
<div v-loading="detailLoading" class="sample-pay-detail">
<template v-if="detail">
<div class="detail-summary form-grid-2">
<div class="detail-field">
<label>协议号</label>
<span>{{ detail.protocolNo || '—' }}</span>
</div>
<div class="detail-field">
<label>客户</label>
<span>
@@ -7,7 +7,7 @@
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/客户/样本编码/电话"
placeholder="客户/样本编码/电话"
clearable
@keyup.enter="handleSearch"
/>
@@ -363,12 +363,12 @@
function formatAmount(v: unknown) {
if (v == null || v === '') return '—'
return formatMoneyDisplay(v)
return formatMoneyDisplay(v as string | number)
}
function formatAmountYuan(v: unknown) {
if (v == null || v === '') return '—'
return `${formatMoneyDisplay(v)} 元`
return `${formatMoneyDisplay(v as string | number)} 元`
}
function transferAttachmentFiles(tr: Record<string, any>) {
@@ -491,7 +491,8 @@
async function openPeriodDialog(row: Record<string, any>, idx: number) {
const cell = periodRaw(row, idx)
if (!cell?.scheduleId) return
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${idx + 1} 期转账记录`
const titleParts = [row.motherName, row.sampleCode].filter(Boolean)
periodDialogTitle.value = `${titleParts.join(' · ')}${titleParts.length ? ' · ' : ''}第 ${idx + 1} 期转账记录`
periodDialogVisible.value = true
periodLoading.value = true
periodTransfers.value = []
@@ -15,7 +15,13 @@
@closed="resetForm"
>
<DialogFormBody v-loading="loading">
<el-form ref="formRef" :model="form" label-position="top" scroll-to-error>
<el-form
ref="formRef"
:model="form"
:disabled="loading || !formReady"
label-position="top"
scroll-to-error
>
<div class="form-grid-3">
<el-form-item label="业务日期">
<el-date-picker
@@ -371,7 +377,7 @@
class="btn-main"
type="primary"
:loading="saving"
:disabled="isConverted"
:disabled="isConverted || loading || !formReady"
@click="handleSave"
>
<el-icon><Check /></el-icon>
@@ -430,6 +436,7 @@
const formRef = ref()
const loading = ref(false)
const formReady = ref(false)
const saving = ref(false)
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
const sampleTypeOptions = ref<Array<Record<string, any>>>([])
@@ -457,7 +464,7 @@
nickName: String(
currentUser?.realName || currentUser?.userName || currentUser?.username || ''
),
phoneNo: String(currentUser?.phone || currentUser?.phoneNo || ''),
phoneNo: String(currentUser?.phone || ''),
deptId: '',
userNo: '',
email: '',
@@ -572,13 +579,14 @@
fileUrl: '',
_uploading: true
})
const idx = row.attachments.length - 1
try {
const fd = new FormData()
fd.append('folder', 'presign-transfer')
fd.append('file', file)
const res: any = await fetchUploadAttachment(fd)
const data = res?.data || res
const idx = row.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
if (idx < 0) return
row.attachments[idx] = {
id: data?.id || tempId,
fileName: data?.fileName || file.name,
@@ -587,7 +595,8 @@
}
} catch {
ElMessage.error('文件上传失败')
row.attachments.splice(idx, 1)
const idx = row.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
if (idx >= 0) row.attachments.splice(idx, 1)
}
}
@@ -786,6 +795,10 @@
}
async function submitTransfer(tr: Record<string, any>) {
if ((tr.attachments || []).some((f: TransferAttachment) => f._uploading)) {
ElMessage.warning('凭证正在上传,请稍后再提交')
return
}
if (!props.orderId || !tr.id) {
ElMessage.warning('请先保存订单后再提交财务确认')
return
@@ -794,9 +807,9 @@
await fetchUpdatePresignTransfer(String(tr.id), {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
remark: tr.remark || undefined
transferTime: tr.transferTime || '',
transferAttachments: attachmentsToUrls(tr.attachments),
remark: tr.remark || ''
})
}
await fetchSubmitPresignTransferFinance(String(tr.id))
@@ -817,6 +830,19 @@
}
async function handleSave() {
if (saving.value) return
if (loading.value || !formReady.value) {
ElMessage.warning('订单尚未初始化成功,请稍候或重新打开后重试')
return
}
if (
form.transfers.some((tr) =>
(tr.attachments || []).some((f: TransferAttachment) => f._uploading)
)
) {
ElMessage.warning('凭证正在上传,请稍后再保存')
return
}
if (!form.motherName.trim()) {
ElMessage.warning('请填写母亲姓名')
return
@@ -830,17 +856,17 @@
const payload: Record<string, unknown> = {
orderDate: form.orderDate || undefined,
motherName: form.motherName,
expectedDeliveryDate: form.expectedDeliveryDate || undefined,
regionId: form.regionId || undefined,
hospitalId: form.hospitalId || undefined,
sampleCollectTime: form.sampleCollectTime || undefined,
collectorUserId: form.collectorUserId || undefined,
regionManagerUserId: form.regionManagerUserId || undefined,
expectedDeliveryDate: form.expectedDeliveryDate || '',
regionId: form.regionId || '',
hospitalId: form.hospitalId || '',
sampleCollectTime: form.sampleCollectTime || '',
collectorUserId: form.collectorUserId || '',
regionManagerUserId: form.regionManagerUserId || '',
payMode: form.payMode || undefined,
contractAmount: form.contractAmount ?? undefined,
contractAmount: form.contractAmount ?? null,
orderStatus: form.orderStatus || 'active',
sampleTypeIds: form.sampleTypeIds,
remark: form.remark || undefined
remark: form.remark || ''
}
let transfersForSubmit: Array<Record<string, any>> = []
@@ -851,18 +877,18 @@
const created: any = await fetchCreatePresignTransfer(props.orderId, {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
remark: tr.remark || undefined
transferTime: tr.transferTime || '',
transferAttachments: attachmentsToUrls(tr.attachments),
remark: tr.remark || ''
})
if (created?.id) tr.id = String(created.id)
} else if (tr.id && tr.payStatus === 'dfj') {
await fetchUpdatePresignTransfer(String(tr.id), {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
remark: tr.remark || undefined
transferTime: tr.transferTime || '',
transferAttachments: attachmentsToUrls(tr.attachments),
remark: tr.remark || ''
})
}
}
@@ -873,9 +899,9 @@
.map((t) => ({
transferType: t.transferType,
transferAmount: t.transferAmount,
transferTime: t.transferTime || undefined,
transferAttachments: attachmentsToUrls(t.attachments) || undefined,
remark: t.remark || undefined
transferTime: t.transferTime || '',
transferAttachments: attachmentsToUrls(t.attachments),
remark: t.remark || ''
}))
const createdOrder: any = await fetchCreatePresignOrder(payload)
transfersForSubmit = Array.isArray(createdOrder?.transfers)
@@ -901,9 +927,19 @@
() => props.modelValue,
async (open) => {
if (!open) return
loading.value = true
formReady.value = false
resetForm()
await loadLookups()
if (props.orderId) await loadDetail()
try {
await loadLookups()
if (props.orderId) await loadDetail()
formReady.value = true
} catch {
ElMessage.error('订单初始化失败,请重新打开后重试')
emit('update:modelValue', false)
} finally {
loading.value = false
}
}
)
</script>
@@ -9,7 +9,7 @@
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="母亲姓名/协议编号"
placeholder="母亲姓名"
clearable
@keyup.enter="handleSearch"
/>
@@ -81,15 +81,6 @@
>
<template #default="{ row }">{{ formatYmd(row.orderDate) }}</template>
</el-table-column>
<el-table-column
prop="protocolNo"
label="协议编号"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
</el-table-column>
<el-table-column
prop="motherName"
label="母亲姓名"
@@ -217,7 +208,7 @@
function fmtMoney(v: unknown) {
if (v == null || v === '') return '-'
return '¥' + formatMoneyDisplay(v)
return '¥' + formatMoneyDisplay(v as string | number)
}
function formatYmd(v: unknown) {
@@ -276,9 +267,8 @@
'转正式协议',
{ type: 'warning' }
)
const res: any = await fetchConvertPresignOrder(String(row.id))
const protocolNo = res?.protocolNo || res?.data?.protocolNo
ElMessage.success(protocolNo ? `已生成正式协议 ${protocolNo}` : '已转正式协议')
await fetchConvertPresignOrder(String(row.id))
ElMessage.success('已转正式协议')
loadData()
}
@@ -65,16 +65,6 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.protocolNo || '—' }}</template>
</el-table-column>
<el-table-column label="客户" min-width="150" align="center" header-align="center">
<template #default="{ row }">
<div>{{ row.motherName || '—' }}</div>
@@ -537,9 +537,7 @@
}
const row = rows[0]
refundProtocolId.value = String(row.id || '')
const name = String(row.motherName || '').trim()
const no = String(row.protocolNo || '').trim()
refundProtocolLabel.value = [name, no].filter(Boolean).join(' / ')
refundProtocolLabel.value = String(row.motherName || '').trim()
refundDialogVisible.value = true
}
@@ -204,7 +204,7 @@
<SampleTransportFormSection
v-model:samples="formData.samples"
sample-source="cell_storage"
:protocol-id="formData.id || ''"
:protocol-id="editingId || ''"
hide-dialog-collect-amount-unit
:sample-type-options="sampleTypeOptions"
:sample-type-options-loading="sampleTypeLoading"
@@ -1232,9 +1232,6 @@
import { formatMoneyDisplay } from '@/utils/money-format'
import {
payModeLabel,
newbornSexLabel,
deliveryModeLabel,
sampleStatusLabel,
loadTransferTypeDict,
getTransferTypeOptions,
transferTypeLabel,
@@ -1262,7 +1259,7 @@
/** 金额只读展示:千分位 + ¥ 前缀 */
function fmtMoney(v: unknown): string {
if (v == null || v === '') return '—'
return '¥' + formatMoneyDisplay(v)
return '¥' + formatMoneyDisplay(v as string | number)
}
const userStore = useUserStore()
@@ -1469,6 +1466,7 @@
/* ====== 新增/编辑弹窗 ====== */
const dialogFormLoading = ref(false)
const editDetailLoaded = ref(false)
const dialogTitle = ref('新增协议单')
const isEdit = ref(false)
const submitLoading = ref(false)
@@ -2078,6 +2076,7 @@
function handleOpenForAdd() {
isEdit.value = false
editingId.value = null
dialogTitle.value = '新增协议单'
resetForm()
formData.protocolStatus = 'sxz'
@@ -2278,6 +2277,7 @@
if (!row || !props.editId) return
isEdit.value = true
editDetailLoaded.value = false
dialogTitle.value = '编辑协议单'
editingId.value = props.editId
resetForm()
@@ -2296,11 +2296,11 @@
try {
const [, bundle] = await Promise.all([
loadEthnicOptions(),
fetchCollectionProtocolDetail(String(row.id)).catch(() => null)
fetchCollectionProtocolDetail(String(row.id))
])
if (bundle) {
applyProtocolBundleToForm(bundle as Record<string, any>, row)
}
if (!bundle) throw new Error('协议详情为空')
applyProtocolBundleToForm(bundle as Record<string, any>, row)
editDetailLoaded.value = true
const ciBundle = (bundle as any)?.collectionInfo as Record<string, any> | null
const hydrateTasks: Promise<void>[] = [
@@ -2345,7 +2345,10 @@
hydrateSampleLookupOptions()
}
} catch {
/* 保留列表行已填基础字段 */
if (!editDetailLoaded.value) {
ElMessage.error('协议详情加载失败,请重新打开后重试')
emit('update:modelValue', false)
}
} finally {
dialogFormLoading.value = false
}
@@ -2353,6 +2356,25 @@
async function handleSubmit() {
if (!formRef.value || submitLoading.value) return
if (isEdit.value && !editDetailLoaded.value) {
ElMessage.warning('协议详情尚未加载成功,请重新打开后重试')
return
}
const uploadLists = [
formData.attachments,
formData.consentAttachments,
formData.healthSurveyAttachments,
...formData.paymentDetails.map((d) => d.attachments || []),
...formData.samplePayments.flatMap((p) =>
(p.schedules || []).flatMap((sch: any) =>
(sch.transfers || []).map((t: any) => t.attachments || [])
)
)
]
if (uploadLists.some((list) => list.some((f: any) => f._uploading))) {
ElMessage.warning('附件正在上传,请稍后再保存')
return
}
submitLoading.value = true
try {
await formRef.value.validate()
@@ -2438,12 +2460,11 @@
.join(',')
const payload: Record<string, unknown> = {
protocolStatus: formData.protocolStatus,
registerDate: formData.registerDate || undefined,
collectorUserId: formData.collectorUserId || undefined,
protocolAttachments: protocolAttachmentUrls || formData.protocolAttachments || undefined,
consentFormUrls: consentAttachmentUrls || formData.consentFormUrls || undefined,
healthSurveyFormUrls:
healthSurveyAttachmentUrls || formData.healthSurveyFormUrls || undefined,
registerDate: formData.registerDate || '',
collectorUserId: formData.collectorUserId || '',
protocolAttachments: protocolAttachmentUrls,
consentFormUrls: consentAttachmentUrls,
healthSurveyFormUrls: healthSurveyAttachmentUrls,
attachments: formData.attachments.map((a) => ({
id: a.id,
fileName: a.fileName,
@@ -2468,39 +2489,39 @@
fileSizeDisplay: a.fileSizeDisplay,
fileType: a.fileType
})),
remark: formData.remark || undefined,
remark: formData.remark || '',
partyCustomer: {
customerId: formData.customerId || undefined,
fatherCustomerId: formData.fatherCustomerId || undefined,
motherName: formData.motherName || undefined,
motherPhone: formData.motherPhone || undefined,
motherIdCard: formData.motherIdCard || undefined,
motherBirthYm: formData.motherBirthYm || undefined,
motherEthnic: formData.motherEthnic || undefined,
motherEmail: formData.motherEmail || undefined,
motherAddress: formData.motherAddress || undefined,
fatherName: formData.fatherName || undefined,
fatherPhone: formData.fatherPhone || undefined,
fatherIdCard: formData.fatherIdCard || undefined,
newbornName: formData.newbornName || undefined,
newbornIdCard: formData.newbornIdCard || undefined,
expectedDeliveryDate: formData.expectedDeliveryDate || undefined,
newbornDeliveryTime: formData.newbornDeliveryTime || undefined,
newbornSex: formData.newbornSex || undefined,
deliveryMode: formData.deliveryMode || undefined,
birthOrder: formData.birthOrder ?? undefined,
newbornWeightGram: formData.newbornWeightGram ?? undefined,
apgar1min: formData.apgar1min ?? undefined,
apgar5min: formData.apgar5min ?? undefined
customerId: formData.customerId || '',
fatherCustomerId: formData.fatherCustomerId || '',
motherName: formData.motherName || '',
motherPhone: formData.motherPhone || '',
motherIdCard: formData.motherIdCard || '',
motherBirthYm: formData.motherBirthYm || '',
motherEthnic: formData.motherEthnic || '',
motherEmail: formData.motherEmail || '',
motherAddress: formData.motherAddress || '',
fatherName: formData.fatherName || '',
fatherPhone: formData.fatherPhone || '',
fatherIdCard: formData.fatherIdCard || '',
newbornName: formData.newbornName || '',
newbornIdCard: formData.newbornIdCard || '',
expectedDeliveryDate: formData.expectedDeliveryDate || '',
newbornDeliveryTime: formData.newbornDeliveryTime || '',
newbornSex: formData.newbornSex || '',
deliveryMode: formData.deliveryMode || '',
birthOrder: formData.birthOrder ?? null,
newbornWeightGram: formData.newbornWeightGram ?? null,
apgar1min: formData.apgar1min ?? null,
apgar5min: formData.apgar5min ?? null
},
collectionInfo: {
regionId: formData.regionId || undefined,
hospitalId: formData.hospitalId || undefined,
medicalStaff: formData.medicalStaff || undefined,
salesUserId: formData.salesUserId || undefined,
regionManagerUserId: formData.regionManagerUserId || undefined,
sampleCollectTime: formData.sampleCollectTime || undefined,
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
regionId: formData.regionId || '',
hospitalId: formData.hospitalId || '',
medicalStaff: formData.medicalStaff || '',
salesUserId: formData.salesUserId || '',
regionManagerUserId: formData.regionManagerUserId || '',
sampleCollectTime: formData.sampleCollectTime || '',
maternalBloodCollectTime: formData.maternalBloodCollectTime || ''
},
samples: formData.samples.map(({ _rowKey, transport, freight, freightDraft, ...s }) => ({
...s,
@@ -2560,7 +2581,9 @@
transferType: t.transferType,
transferAmount: t.transferAmount,
transferTime: t.transferTime,
transferAttachments: fromFiles || t.transferAttachments || undefined,
transferAttachments: Array.isArray(t.attachments)
? fromFiles
: t.transferAttachments || '',
payStatus: t.payStatus || 'dfj',
remark: t.remark
}
@@ -3376,13 +3399,15 @@
const q = (query || '').trim()
if (!q) {
cb([])
return
return []
}
try {
const res = await fetchCustomerList({ page: 1, pageSize: 20, keyword: q })
cb(res.list || [])
return res.list || []
} catch {
cb([])
return []
}
}
@@ -3524,13 +3549,13 @@
fileType: file.type || '',
_uploading: true
})
const idx = list.length - 1
try {
const fd = new FormData()
fd.append('folder', folder)
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
const idx = list.findIndex((f) => f.id === tempId)
if (idx < 0) return
list[idx] = {
...list[idx],
id: res.id || tempId,
@@ -3543,7 +3568,8 @@
}
} catch {
ElMessage.error('文件上传失败')
list.splice(idx, 1)
const idx = list.findIndex((f) => f.id === tempId)
if (idx >= 0) list.splice(idx, 1)
} finally {
nextTick(() => uploadRef.value?.clearFiles())
}
@@ -3668,13 +3694,13 @@
fileType: file.type || '',
_uploading: true
})
const aIdx = detail.attachments.length - 1
try {
const fd = new FormData()
fd.append('folder', 'protocol-payment')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
const aIdx = detail.attachments.findIndex((f) => f.id === tempId)
if (aIdx < 0) return
detail.attachments[aIdx] = {
...detail.attachments[aIdx],
id: res.id || tempId,
@@ -3687,7 +3713,8 @@
}
} catch {
ElMessage.error('文件上传失败')
detail.attachments.splice(aIdx, 1)
const aIdx = detail.attachments.findIndex((f) => f.id === tempId)
if (aIdx >= 0) detail.attachments.splice(aIdx, 1)
}
}
@@ -3,7 +3,7 @@
<el-dialog
:model-value="modelValue"
:title="dialogTitle"
width="720px"
width="960px"
align-center
append-to-body
destroy-on-close
@@ -27,10 +27,17 @@
<template #default="{ row }">{{ fmtMoney(row.refundAmount) }}</template>
</el-table-column>
<el-table-column prop="refundTime" label="退费时间" min-width="160" />
<el-table-column prop="refundRemark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column label="附件" min-width="100">
<el-table-column prop="refundRemark" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column label="附件" min-width="280">
<template #default="{ row }">
{{ (row.refundAttachmentFiles || []).length || (row.refundAttachments ? 1 : 0) }} 个
<ProtocolAttachmentReadonly
v-if="rowAttachmentFiles(row).length"
:files="rowAttachmentFiles(row)"
size="compact"
@preview="handleListPreview"
@download="handleDownloadAttachment"
/>
<span v-else class="muted-dash">—</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="140" fixed="right">
@@ -52,7 +59,7 @@
<el-dialog
v-model="formVisible"
:title="editingId ? '编辑退费登记' : '新增退费登记'"
width="560px"
width="720px"
align-center
append-to-body
destroy-on-close
@@ -87,14 +94,15 @@
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="attachmentUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="onFileChange"
>
<div class="upload-card upload-card--sm">
<el-icon :size="22" color="#047857"><Plus /></el-icon>
<div class="upload-card">
<el-icon :size="28" color="#047857"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
@@ -102,17 +110,57 @@
v-for="(file, idx) in form.attachments"
:key="file.id || idx"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div class="attachment-name" :title="file.fileName">{{ file.fileName }}</div>
<el-icon
class="action-btn action-btn--danger"
@click="form.attachments.splice(idx, 1)"
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
"
>
<Delete />
</el-icon>
<template v-if="isImageFile(file) && file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else-if="isImageFile(file) && !file.fileUrl">
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
<template v-else-if="isVideoFile(file)">
<div class="attachment-preview__icon attachment-preview__icon--video">
<el-icon :size="28"><VideoPlay /></el-icon>
</div>
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</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="handleDownloadAttachment(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="form.attachments.splice(idx, 1)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片与常见文档,单文件不超过 20MB</span>
<span class="upload-hint"
>支持拖拽或点击上传图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span
>
</div>
</el-form-item>
</div>
@@ -128,14 +176,42 @@
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
append-to-body
modal-class="protocol-dialog-mask"
>
<div v-if="previewType === 'image'" class="preview-container">
<img :src="previewUrl" alt="预览" style="max-width: 100%" />
</div>
<div v-else class="preview-container">
<video :src="previewUrl" controls style="max-width: 100%" />
</div>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { Plus, Edit, Delete, Close, Check } from '@element-plus/icons-vue'
import { computed, nextTick, reactive, ref } from 'vue'
import {
Plus,
Edit,
Delete,
Close,
Check,
Download,
Document,
VideoPlay,
Loading
} from '@element-plus/icons-vue'
import AmountInput from '@/components/common/amount-input/index.vue'
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { fetchUploadAttachment } from '@/api/business'
import {
fetchProtocolRefundRecords,
@@ -147,7 +223,7 @@
const props = defineProps<{
modelValue: boolean
protocolId?: string
/** 展示在标题中的协议标识,如母亲姓名 / 协议号 */
/** 展示在标题中的协议标识,如母亲姓名 */
protocolLabel?: string
}>()
@@ -155,26 +231,114 @@
'update:modelValue': [boolean]
}>()
type RefundAttachmentItem = {
id?: string
fileName: string
fileUrl: string
fileSize?: number
fileSizeDisplay?: string
fileType?: string
_uploading?: boolean
}
const records = ref<Array<Record<string, any>>>([])
const loading = ref(false)
const formVisible = ref(false)
const editingId = ref('')
const saving = ref(false)
const attachmentUploadRef = ref()
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
const form = reactive({
refundAmount: null as number | null,
refundTime: '',
refundRemark: '',
attachments: [] as Array<Record<string, any>>
attachments: [] as RefundAttachmentItem[]
})
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const dialogTitle = computed(() => {
const label = String(props.protocolLabel || '').trim()
return label ? `退费登记 · ${label}` : '退费登记'
})
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
return suffix
}
return `${minioBaseUrl}/${suffix}`
}
function isImageFile(file: RefundAttachmentItem) {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return (
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name) ||
(file.fileType || '').startsWith('image/')
)
}
function isVideoFile(file: RefundAttachmentItem) {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return (
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
(file.fileType || '').startsWith('video/')
)
}
function rowAttachmentFiles(row: Record<string, any>): RefundAttachmentItem[] {
if (Array.isArray(row.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
return row.refundAttachmentFiles.map((f: Record<string, any>, index: number) => ({
id: f.id || `refund-att-${index}`,
fileName: f.fileName || '附件',
fileUrl: f.fileUrl || '',
fileSize: f.fileSize,
fileType: f.fileType
}))
}
const raw = String(row.refundAttachments || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
return raw.map((url, index) => {
const name = url.includes('/') ? url.split('/').pop() || `附件${index + 1}` : url
return {
id: `refund-url-${index}`,
fileName: decodeURIComponent(name),
fileUrl: url
}
})
}
function handlePreviewAttachment(file: RefundAttachmentItem) {
if (file._uploading || !file.fileUrl) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewType.value = isVideoFile(file) ? 'video' : 'image'
previewVisible.value = true
}
function handleListPreview(file: Record<string, any>) {
handlePreviewAttachment({
id: file.id,
fileName: file.fileName || '附件',
fileUrl: file.fileUrl || '',
fileType: file.fileType
})
}
function handleDownloadAttachment(file: RefundAttachmentItem | Record<string, any>) {
downloadAttachmentFile(file, { fullFileUrl })
}
function fmtMoney(v: unknown) {
if (v == null || v === '') return '—'
return formatMoneyDisplay(v)
return formatMoneyDisplay(v as string | number)
}
async function loadRecords() {
@@ -203,7 +367,13 @@
form.refundTime = String(row.refundTime || '')
form.refundRemark = String(row.refundRemark || '')
form.attachments = Array.isArray(row.refundAttachmentFiles)
? [...row.refundAttachmentFiles]
? row.refundAttachmentFiles.map((f: Record<string, any>) => ({
id: f.id,
fileName: f.fileName || '附件',
fileUrl: f.fileUrl || '',
fileSize: f.fileSize,
fileType: f.fileType
}))
: []
formVisible.value = true
}
@@ -223,24 +393,47 @@
}
async function onFileChange(uploadFile: any) {
const raw = uploadFile?.raw as File | undefined
const raw = (uploadFile?.raw || uploadFile) as File | undefined
if (!raw) return
if (raw.size > 20 * 1024 * 1024) {
ElMessage.warning('单文件不超过 20MB')
return
}
const fd = new FormData()
fd.append('file', raw)
fd.append('folder', 'protocol-refund-record')
const res: any = await fetchUploadAttachment(fd)
const data = res?.data || res
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
form.attachments.push({
id: data?.id,
fileName: data?.fileName || raw.name,
fileUrl: data?.fileUrl || data?.url,
fileSize: data?.fileSize,
fileType: data?.fileType
id: tempId,
fileName: raw.name,
fileUrl: '',
fileSize: raw.size,
fileSizeDisplay: `${(raw.size / 1024).toFixed(1)} KB`,
fileType: raw.type || '',
_uploading: true
})
try {
const fd = new FormData()
fd.append('file', raw)
fd.append('folder', 'protocol-refund-record')
const res: any = await fetchUploadAttachment(fd)
const data = res?.data || res
const idx = form.attachments.findIndex((f) => f.id === tempId)
if (idx < 0) return
form.attachments[idx] = {
...form.attachments[idx],
id: data?.id || tempId,
fileName: raw.name || data?.fileName,
fileUrl: data?.fileUrl || data?.url || '',
fileSize: data?.fileSize || raw.size,
fileSizeDisplay: data?.fileSizeDisplay || `${(raw.size / 1024).toFixed(1)} KB`,
fileType: data?.fileType || raw.type || '',
_uploading: false
}
} catch {
ElMessage.error('文件上传失败')
const idx = form.attachments.findIndex((f) => f.id === tempId)
if (idx >= 0) form.attachments.splice(idx, 1)
} finally {
nextTick(() => attachmentUploadRef.value?.clearFiles?.())
}
}
async function onSave() {
@@ -249,6 +442,10 @@
ElMessage.warning('请填写退费金额')
return
}
if (form.attachments.some((a) => a._uploading)) {
ElMessage.warning('附件正在上传,请稍后再保存')
return
}
saving.value = true
try {
const urls = form.attachments
@@ -300,60 +497,175 @@
color: var(--mint-text-muted, #94a3b8);
}
.muted-dash {
color: var(--mint-text-muted, #94a3b8);
}
.attachment-upload {
width: 100%;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
gap: 12px;
}
.upload-card--sm {
.upload-card {
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
justify-content: center;
width: 140px;
height: 96px;
cursor: pointer;
border: 1px dashed #a7f3d0;
background: #f0fdf4;
border: 1px dashed #a8d4c4;
border-radius: 8px;
background: #ecfdf5;
transition:
border-color 0.15s,
background 0.15s;
&:hover {
background: #e8f4ee;
border-color: #047857;
border-style: solid;
}
}
.upload-card-text {
margin-top: 4px;
font-size: 12px;
font-weight: 500;
line-height: 1;
color: #047857;
}
.attachment-item {
display: flex;
gap: 6px;
align-items: center;
min-width: 140px;
max-width: 220px;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.attachment-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
}
.action-btn--danger {
color: #ef4444;
cursor: pointer;
}
.upload-hint {
display: block;
margin-top: 6px;
margin-top: 8px;
font-size: 11px;
line-height: 1;
color: var(--mint-text-muted, #334155);
}
.attachment-item {
display: flex;
flex-direction: column;
width: 140px;
overflow: hidden;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #047857;
box-shadow: 0 2px 8px rgba(4, 120, 87, 0.1);
}
&.is-uploading {
pointer-events: none;
opacity: 0.6;
}
}
.attachment-preview {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100%;
height: 96px;
overflow: hidden;
cursor: pointer;
background: #f8fafc;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.attachment-preview__icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 10px;
&--video {
color: #7c3aed;
background: #ede9fe;
}
&--doc {
color: #0284c7;
background: #e0f2fe;
}
}
.attachment-footer {
display: flex;
flex: 1;
flex-direction: column;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
min-height: calc(11px * 1.4 * 2);
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
font-size: 11px;
line-height: 1.4;
color: #334155;
word-break: break-all;
-webkit-box-orient: vertical;
}
.attachment-actions {
display: flex;
flex-shrink: 0;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 4px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
font-size: 12px;
color: var(--mint-text-muted, #94a3b8);
color: var(--mint-text-muted, #334155);
cursor: pointer;
background: #f1f5f9;
border-radius: 4px;
transition:
background 0.15s,
color 0.15s;
&:hover {
color: #fff;
background: #047857;
}
&--danger:hover {
color: #fff;
background: #ef4444;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -52,6 +52,10 @@
></div
></div
>
<div class="data-item"
><div class="data-label">母亲姓名</div
><div class="data-value">{{ basicMotherNameDisplay }}</div></div
>
<div class="data-item"
><div class="data-label">登记日期</div
><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div
@@ -786,6 +790,23 @@
align="center"
show-overflow-tooltip
/>
<el-table-column
label="附件"
min-width="280"
align="left"
header-align="center"
>
<template #default="{ row }">
<ProtocolAttachmentReadonly
v-if="refundRecordAttachments(row).length"
:files="refundRecordAttachments(row)"
size="compact"
@preview="openPreview"
@download="downloadFile"
/>
<span v-else class="muted-dash">—</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无退费登记" :image-size="40" />
</template>
@@ -1099,62 +1120,47 @@
<!-- 状态变更记录(置底) -->
<div id="view-section-status-change" class="section-card section-card--white">
<div class="section-title section-title--banner"><h3>状态变更记录</h3></div>
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
<div v-else class="data-grid data-grid--4">
<template v-if="protocol?.effectiveTime">
<div class="data-item"
><div class="data-label">协议生效时间</div
><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div
<div class="list-body list-wrap view-inner-table-wrap">
<el-table
v-loading="statusChangeLoading"
class="list-table inner-table"
:data="statusChangeRecords"
stripe
empty-text="暂无状态变更记录"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column label="变更类型" min-width="110" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="statusChangeTagType(String(row.changeType || ''))" size="small">
{{ statusChangeTypeLabel(String(row.changeType || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变更时间" min-width="170" align="center" header-align="center">
<template #default="{ row }">{{ dash(row.changeTime) }}</template>
</el-table-column>
<el-table-column label="状态流转" min-width="160" align="center" header-align="center">
<template #default="{ row }">{{ statusChangeFlow(row) }}</template>
</el-table-column>
<el-table-column
label="变更说明"
min-width="200"
align="center"
header-align="center"
show-overflow-tooltip
>
<div class="data-item"
><div class="data-label">协议生效描述</div
><div class="data-value data-value--muted">{{
dash(protocol.effectiveDesc)
}}</div></div
>
</template>
<template v-if="protocol?.expireTime">
<div class="data-item"
><div class="data-label">协议过期时间</div
><div class="data-value">{{ dash(protocol.expireTime) }}</div></div
>
<div class="data-item"
><div class="data-label">协议过期描述</div
><div class="data-value data-value--muted">{{
dash(protocol.expireDesc)
}}</div></div
>
</template>
<template v-if="protocol?.voidTime">
<div class="data-item"
><div class="data-label">作废类型</div
><div class="data-value">{{
voidTypeLabel(String(protocol?.voidType || '')) || '—'
}}</div></div
>
<div class="data-item"
><div class="data-label">协议作废时间</div
><div class="data-value">{{ dash(protocol.voidTime) }}</div></div
>
<div class="data-item"
><div class="data-label">协议作废描述</div
><div class="data-value data-value--muted">{{
dash(protocol.voidDesc)
}}</div></div
>
</template>
<template v-if="protocol?.toResearchTime">
<div class="data-item"
><div class="data-label">协议转科研时间</div
><div class="data-value">{{ dash(protocol.toResearchTime) }}</div></div
>
<div class="data-item"
><div class="data-label">协议转科研描述</div
><div class="data-value data-value--muted">{{
dash(protocol.toResearchDesc)
}}</div></div
>
</template>
<template #default="{ row }">{{ statusChangeDesc(row) }}</template>
</el-table-column>
<el-table-column label="操作人" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ dash(row.operatorUserName) }}</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
@@ -1208,6 +1214,7 @@
import {
fetchCollectionProtocolAuditList,
fetchCollectionProtocolDetail,
fetchCollectionProtocolStatusChanges,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import {
@@ -1305,8 +1312,52 @@
const detailLoading = ref(false)
const lookupLoading = ref(false)
const auditLoading = ref(false)
const statusChangeLoading = ref(false)
const bundle = ref<Record<string, any> | null>(null)
const auditRecords = ref<Array<Record<string, unknown>>>([])
const statusChangeRecords = ref<Array<Record<string, unknown>>>([])
const STATUS_CHANGE_TYPE_LABEL: Record<string, string> = {
activate: '生效',
expire: '过期',
void: '作废',
to_research: '转科研',
to_storage: '转细胞存储'
}
const STATUS_CHANGE_TAG: Record<string, 'success' | 'warning' | 'danger' | 'primary' | 'info'> = {
activate: 'success',
expire: 'warning',
void: 'danger',
to_research: 'primary',
to_storage: 'primary'
}
function statusChangeTypeLabel(changeType: string) {
return STATUS_CHANGE_TYPE_LABEL[changeType] || changeType || '—'
}
function statusChangeTagType(changeType: string) {
return STATUS_CHANGE_TAG[changeType] || 'info'
}
function statusChangeFlow(row: Record<string, unknown>) {
const from = String(row.fromStatus || '')
const to = String(row.toStatus || '')
const voidType = String(row.voidType || '')
const toLabel = protocolStatusDisplayLabel(to, to === 'yzf' ? voidType : '')
if (!from) return toLabel || '—'
return `${protocolStatusDisplayLabel(from)} → ${toLabel}`
}
function statusChangeDesc(row: Record<string, unknown>) {
const desc = String(row.changeDesc || '').trim()
if (String(row.changeType || '') === 'void') {
const vt = voidTypeLabel(String(row.voidType || ''))
if (vt && desc) return `作废类型:${vt};${desc}`
if (vt) return `作废类型:${vt}`
}
return desc || '—'
}
const activeTab = ref('basic')
const sampleTypeOptions = ref<Array<{ id: string; typeName: string }>>([])
@@ -1520,25 +1571,21 @@
/** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */
function fmtMoney(v: unknown): string {
if (v == null || v === '') return '—'
return '¥' + formatMoneyDisplay(v)
return '¥' + formatMoneyDisplay(v as string | number)
}
const basicMotherNameDisplay = computed(() =>
dash(
partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
)
)
const dialogTitle = computed(() => {
const name =
partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
return `查看 — ${formatProtocolDocSubject('cell_storage', { motherName: name })}`
})
const hasAnyStatusChange = computed(() => {
if (!protocol.value) return false
return !!(
protocol.value.effectiveTime ||
protocol.value.expireTime ||
protocol.value.voidTime ||
protocol.value.toResearchTime
)
})
const collectorDisplay = computed(() => {
const fromList = props.listRow?.collectorUserName
if (fromList) return String(fromList)
@@ -1892,6 +1939,13 @@
return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
}
function refundRecordAttachments(row: Record<string, any>) {
if (Array.isArray(row.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
return row.refundAttachmentFiles
}
return urlsToAttachmentFiles(String(row.refundAttachments || ''))
}
function refundAttachmentFiles(detail: Record<string, any>) {
if (Array.isArray(detail.refundAttachmentFiles)) {
return detail.refundAttachmentFiles
@@ -2007,6 +2061,22 @@
}
}
async function loadStatusChangeRecords() {
if (!props.protocolId) {
statusChangeRecords.value = []
return
}
statusChangeLoading.value = true
try {
const res = await fetchCollectionProtocolStatusChanges(props.protocolId)
statusChangeRecords.value = res.list || []
} catch {
statusChangeRecords.value = []
} finally {
statusChangeLoading.value = false
}
}
async function loadAuditRecords() {
if (!props.showAuditHistory || !props.protocolId) {
auditRecords.value = []
@@ -2033,9 +2103,11 @@
lookupLoading.value = true
bundle.value = null
auditRecords.value = []
statusChangeRecords.value = []
const baseLookupsPromise = loadBaseLookups().catch(() => {})
const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
const statusChangePromise = loadStatusChangeRecords()
let data: Record<string, any> | null = null
try {
@@ -2052,6 +2124,7 @@
void finishLookups(data, baseLookupsPromise)
void auditPromise
void statusChangePromise
if (props.highlightSampleCode) {
nextTick(() => {
document
@@ -2074,10 +2147,12 @@
function handleClosed() {
bundle.value = null
auditRecords.value = []
statusChangeRecords.value = []
activeTab.value = 'basic'
detailLoading.value = false
lookupLoading.value = false
auditLoading.value = false
statusChangeLoading.value = false
}
watch(
@@ -444,7 +444,7 @@
function fmtMoney(v: unknown): string {
if (v == null || v === '') return '—'
return '¥' + formatMoneyDisplay(v)
return '¥' + formatMoneyDisplay(v as string | number)
}
function samplePayKey(pay: Record<string, any>, pIdx: number): string {
@@ -896,10 +896,10 @@
tr.payStatus = data.payStatus || tr.payStatus || 'dfj'
tr.scheduleId = data.scheduleId || tr.scheduleId
tr.samplePaymentId = data.samplePaymentId || tr.samplePaymentId
if (Array.isArray(data.attachments) && data.attachments.length) {
if (Array.isArray(data.attachments)) {
tr.attachments = data.attachments.map((a: Record<string, any>) => ({ ...a }))
}
if (data.transferAttachments) tr.transferAttachments = data.transferAttachments
if (data.transferAttachments != null) tr.transferAttachments = data.transferAttachments
emitChange()
}
@@ -922,8 +922,8 @@
const payload = {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr) || undefined,
transferTime: tr.transferTime || '',
transferAttachments: attachmentsToUrls(tr),
remark: tr.remark
}
if (tr.id) {
@@ -965,13 +965,14 @@
_uploading: true
})
emitChange()
const idx = tr.attachments.length - 1
try {
const fd = new FormData()
fd.append('folder', 'protocol-transfer')
fd.append('file', file)
const res: any = await fetchUploadAttachment(fd)
const data = res?.data || res
const idx = tr.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
if (idx < 0) return
tr.attachments[idx] = {
id: data?.id || tempId,
fileName: data?.fileName || file.name,
@@ -981,7 +982,8 @@
emitChange()
} catch {
ElMessage.error('文件上传失败')
tr.attachments.splice(idx, 1)
const idx = tr.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
if (idx >= 0) tr.attachments.splice(idx, 1)
emitChange()
}
}
@@ -399,7 +399,7 @@
}
function handleEdit(row: CollectionProtocolRow) {
formDialogEditId.value = row.id
formDialogEditId.value = String(row.id || '')
formDialogRow.value = row
formDialogVisible.value = true
}
@@ -420,9 +420,7 @@
}
const row = selectedRows.value[0]
refundProtocolId.value = String(row.id || '')
const name = String(row.motherName || '').trim()
const no = String(row.protocolNo || '').trim()
refundProtocolLabel.value = [name, no].filter(Boolean).join(' / ')
refundProtocolLabel.value = String(row.motherName || '').trim()
refundDialogVisible.value = true
}
@@ -93,6 +93,9 @@
</el-form-item>
<el-form-item label="审批人去重">
<el-switch v-model="advanced.approverDedup" />
<div class="oa-designer__hint">
开启后,同一张审批单里某人已经同意过,后续节点再出现该人时系统自动通过,不再生成待办。或签整节点通过,会签仅完成本人;仅「已同意」会计入,拒绝不触发。默认关闭。
</div>
</el-form-item>
</el-form>
</div>
@@ -33,44 +33,6 @@
<template v-if="bundle">
<div>
<!-- KPI 摘要条 -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<el-tag
:type="donationStatusTagType(String(donation?.donationStatus || ''))"
size="small"
>
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><User /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">母亲姓名</div>
<div class="kpi-value">{{ dash(party?.motherName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><Calendar /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">捐赠日期</div>
<div class="kpi-value">{{ formatYmd(donation?.donationDate) }}</div>
</div>
</div>
</div>
<!-- 基本信息 -->
<div id="view-section-basic" class="section-card section-card--white">
<div class="section-title section-title--banner"><h3>基本信息</h3></div>
@@ -85,6 +47,10 @@
></div
></div
>
<div class="data-item"
><div class="data-label">母亲姓名</div
><div class="data-value">{{ dash(party?.motherName) }}</div></div
>
<div class="data-item"
><div class="data-label">捐赠日期</div
><div class="data-value">{{ formatYmd(donation?.donationDate) }}</div></div
@@ -103,62 +69,6 @@
</div>
</div>
<!-- 状态变更记录(独立区域) -->
<div id="view-section-status-change" class="section-card section-card--white">
<div class="section-title"><h3>状态变更记录</h3></div>
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
<div v-else class="data-grid data-grid--4">
<template v-if="donation?.effectiveTime">
<div class="data-item"
><div class="data-label">生效时间</div
><div class="data-value">{{ dash(donation.effectiveTime) }}</div></div
>
<div class="data-item"
><div class="data-label">生效描述</div
><div class="data-value data-value--muted">{{
dash(donation.effectiveDesc)
}}</div></div
>
</template>
<template v-if="donation?.expireTime">
<div class="data-item"
><div class="data-label">过期时间</div
><div class="data-value">{{ dash(donation.expireTime) }}</div></div
>
<div class="data-item"
><div class="data-label">过期描述</div
><div class="data-value data-value--muted">{{
dash(donation.expireDesc)
}}</div></div
>
</template>
<template v-if="donation?.voidTime">
<div class="data-item"
><div class="data-label">作废时间</div
><div class="data-value">{{ dash(donation.voidTime) }}</div></div
>
<div class="data-item"
><div class="data-label">作废描述</div
><div class="data-value data-value--muted">{{
dash(donation.voidDesc)
}}</div></div
>
</template>
<template v-if="donation?.convertTime">
<div class="data-item"
><div class="data-label">转细胞存储时间</div
><div class="data-value">{{ dash(donation.convertTime) }}</div></div
>
<div class="data-item"
><div class="data-label">转细胞存储描述</div
><div class="data-value data-value--muted">{{
dash(donation.convertDesc)
}}</div></div
>
</template>
</div>
</div>
<!-- 采集信息 -->
<div id="view-section-collection" class="section-card section-card--white">
<div class="section-title section-title--banner"><h3>采集信息</h3></div>
@@ -219,7 +129,7 @@
</div>
<div class="sample-card__body">
<div class="sample-card__section">
<div class="sample-card__section-label">样本信息</div>
<div class="sample-card__section-label">样本及运输</div>
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">样本编码</div>
@@ -232,26 +142,6 @@
/>
</div>
</div>
<div class="data-item">
<div class="data-label">存储年限(年)</div>
<div class="data-value">{{ dash(row.storageYears) }}</div>
</div>
<div v-if="row.remark" class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(row.remark) }}</div>
</div>
</div>
</div>
<div
v-if="
row.freight?.id ||
row.freight?.transportMode ||
row.transport?.etaTime
"
class="sample-card__section sample-card__section--transport"
>
<div class="sample-card__section-label">运输与运费</div>
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">运输方式</div>
<div class="data-value">{{
@@ -274,6 +164,10 @@
<div class="data-label">预计到达</div>
<div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
</div>
<div v-if="row.remark" class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(row.remark) }}</div>
</div>
</div>
</div>
<div class="sample-card__section sample-card__section--reception">
@@ -530,6 +424,53 @@
</div>
</div>
</div>
<!-- 状态变更记录(附件信息下方) -->
<div id="view-section-status-change" class="section-card section-card--white">
<div class="section-title section-title--banner"><h3>状态变更记录</h3></div>
<div class="list-body list-wrap view-inner-table-wrap">
<el-table
v-loading="statusChangeLoading"
class="list-table inner-table"
:data="statusChangeRecords"
stripe
empty-text="暂无状态变更记录"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column label="变更类型" min-width="110" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="statusChangeTagType(String(row.changeType || ''))" size="small">
{{ statusChangeTypeLabel(String(row.changeType || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变更时间" min-width="170" align="center" header-align="center">
<template #default="{ row }">{{ dash(row.changeTime) }}</template>
</el-table-column>
<el-table-column label="状态流转" min-width="160" align="center" header-align="center">
<template #default="{ row }">{{ statusChangeFlow(row) }}</template>
</el-table-column>
<el-table-column
label="变更说明"
min-width="200"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ statusChangeDesc(row) }}</template>
</el-table-column>
<el-table-column label="操作人" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ dash(row.operatorUserName) }}</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
</div>
@@ -571,7 +512,7 @@
import { formatDisplayOrDash } from '@/utils/datetime-display'
import { computed, nextTick, ref, watch } from 'vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import { Calendar, CircleCheck, Close, User } from '@element-plus/icons-vue'
import { Close } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
import SampleReceptionDualReadonlyPanel from '@/components/business/sample-reception-dual-readonly-panel/index.vue'
@@ -579,6 +520,7 @@
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
import SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
import {
fetchDonationStatusChanges,
fetchResearchDonationAuditList,
fetchResearchDonationDetail,
type ResearchDonationRow
@@ -594,6 +536,7 @@
} from '@/api/admin'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
donationStatusDisplayLabel,
donationStatusLabel,
donationStatusTagType,
auditStatusLabel,
@@ -606,6 +549,7 @@
acceptConclusionLabel,
acceptConclusionTagType
} from '@/utils/status-tag-registry'
import { loadVoidTypeDict, voidTypeLabel } from '@/utils/void-type-dict'
defineOptions({ name: 'DonationViewDialog' })
@@ -627,7 +571,6 @@
const baseFormTabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'status-change', label: '状态变更记录' },
{ key: 'collection', label: '采集信息' },
{ key: 'sample', label: '样本及运输信息' },
{ key: 'party', label: '客户信息' }
@@ -639,13 +582,58 @@
tabs.push({ key: 'audit', label: '审核记录' })
}
tabs.push({ key: 'attachment', label: '附件信息' })
tabs.push({ key: 'status-change', label: '状态变更记录' })
return tabs
})
const loading = ref(false)
const auditLoading = ref(false)
const statusChangeLoading = ref(false)
const bundle = ref<Record<string, any> | null>(null)
const auditRecords = ref<Array<Record<string, unknown>>>([])
const statusChangeRecords = ref<Array<Record<string, unknown>>>([])
const STATUS_CHANGE_TYPE_LABEL: Record<string, string> = {
activate: '生效',
expire: '过期',
void: '作废',
to_research: '转科研',
to_storage: '转细胞存储'
}
const STATUS_CHANGE_TAG: Record<string, 'success' | 'warning' | 'danger' | 'primary' | 'info'> = {
activate: 'success',
expire: 'warning',
void: 'danger',
to_research: 'primary',
to_storage: 'primary'
}
function statusChangeTypeLabel(changeType: string) {
return STATUS_CHANGE_TYPE_LABEL[changeType] || changeType || '—'
}
function statusChangeTagType(changeType: string) {
return STATUS_CHANGE_TAG[changeType] || 'info'
}
function statusChangeFlow(row: Record<string, unknown>) {
const from = String(row.fromStatus || '')
const to = String(row.toStatus || '')
const voidType = String(row.voidType || '')
const toLabel = donationStatusDisplayLabel(to, to === 'yzf' ? voidType : '')
if (!from) return toLabel || '—'
return `${donationStatusDisplayLabel(from)} → ${toLabel}`
}
function statusChangeDesc(row: Record<string, unknown>) {
const desc = String(row.changeDesc || '').trim()
if (String(row.changeType || '') === 'void') {
const vt = voidTypeLabel(String(row.voidType || ''))
if (vt && desc) return `作废类型:${vt};${desc}`
if (vt) return `作废类型:${vt}`
}
return desc || '—'
}
const activeTab = ref('basic')
const sampleTypeOptions = ref<Array<{ id: string; typeName: string }>>([])
@@ -707,16 +695,6 @@
const dialogTitle = computed(() => '查看捐赠协议单')
const hasAnyStatusChange = computed(() => {
if (!donation.value) return false
return !!(
donation.value.effectiveTime ||
donation.value.expireTime ||
donation.value.voidTime ||
donation.value.convertTime
)
})
const collectorDisplay = computed(() => {
const fromList = props.listRow?.collectorUserName
if (fromList) return String(fromList)
@@ -954,6 +932,22 @@
await Promise.all(tasks)
}
async function loadStatusChangeRecords() {
if (!props.donationId) {
statusChangeRecords.value = []
return
}
statusChangeLoading.value = true
try {
const res = await fetchDonationStatusChanges(props.donationId)
statusChangeRecords.value = res.list || []
} catch {
statusChangeRecords.value = []
} finally {
statusChangeLoading.value = false
}
}
async function loadAuditRecords() {
if (!props.showAuditHistory || !props.donationId) {
auditRecords.value = []
@@ -979,11 +973,13 @@
loading.value = true
bundle.value = null
auditRecords.value = []
statusChangeRecords.value = []
void loadVoidTypeDict()
try {
const data = await fetchResearchDonationDetail(props.donationId)
bundle.value = data as Record<string, any>
await loadLookups(data as Record<string, any>)
await loadAuditRecords()
await Promise.all([loadAuditRecords(), loadStatusChangeRecords()])
if (props.highlightSampleCode) {
nextTick(() => {
document
@@ -1009,9 +1005,11 @@
function handleClosed() {
bundle.value = null
auditRecords.value = []
statusChangeRecords.value = []
activeTab.value = 'basic'
loading.value = false
auditLoading.value = false
statusChangeLoading.value = false
}
watch(
@@ -1044,75 +1042,6 @@
}
}
/* ---- KPI 摘要条(与渠道/申请单查看弹窗一致:浅绿渐变通栏) ---- */
.kpi-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
margin-bottom: 24px;
padding: 18px 20px;
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
border: none;
border-radius: 12px;
box-shadow: none;
}
.kpi-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 16px;
background: transparent;
border-radius: 8px;
transition: background 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.35);
}
}
.kpi-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 10px;
background: rgba(255, 255, 255, 0.72);
border: none;
color: #065f46;
font-size: 18px;
}
.kpi-card__main {
min-width: 0;
flex: 1;
}
.kpi-label {
margin-bottom: 4px;
font-size: 11px;
font-weight: 500;
color: var(--mint-text-muted, #334155);
}
.kpi-value {
font-size: 15px;
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
&--emphasis {
font-size: 16px;
font-weight: 700;
color: #047857;
letter-spacing: 0.02em;
}
}
/* ---- Section Card ---- */
.section-card {
border-radius: 12px;
@@ -1504,37 +1433,12 @@
/* ---- Responsive ---- */
@media (max-width: 1200px) {
.kpi-strip {
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card {
border-right: none !important;
border-bottom: 1px solid rgba(168, 212, 196, 0.75);
&:nth-last-child(-n + 2) {
border-bottom: none;
}
}
.client-sub-cards {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.kpi-strip {
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
border-bottom: 1px solid rgba(168, 212, 196, 0.75);
}
.kpi-strip .kpi-card:last-child {
border-bottom: none;
}
.client-sub-cards {
grid-template-columns: 1fr;
}