This commit is contained in:
zlwu committed 2026-05-27 05:23:28 +08:00
1 parent ad8f8d696f
commit 41aa90aa05
115 files changed
+29626 -10210

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
+123 -105
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-05-26)
# Graph Report - src (2026-05-27)
## Corpus Check
- Large corpus: 632 files · ~390,279 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 641 files · ~402,707 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 1816 nodes · 2360 edges · 37 communities detected
- Extraction: 77% EXTRACTED · 23% INFERRED · 0% AMBIGUOUS · INFERRED: 538 edges (avg confidence: 0.8)
- 1844 nodes · 2394 edges · 41 communities detected
- Extraction: 77% EXTRACTED · 23% INFERRED · 0% AMBIGUOUS · INFERRED: 550 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -33,39 +33,43 @@
- [[_COMMUNITY_Community 21|Community 21]]
- [[_COMMUNITY_Community 22|Community 22]]
- [[_COMMUNITY_Community 23|Community 23]]
- [[_COMMUNITY_Community 24|Community 24]]
- [[_COMMUNITY_Community 25|Community 25]]
- [[_COMMUNITY_Community 26|Community 26]]
- [[_COMMUNITY_Community 27|Community 27]]
- [[_COMMUNITY_Community 29|Community 29]]
- [[_COMMUNITY_Community 30|Community 30]]
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Community 32|Community 32]]
- [[_COMMUNITY_Community 33|Community 33]]
- [[_COMMUNITY_Community 34|Community 34]]
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 39|Community 39]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 47|Community 47]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 193 edges
2. `push()` - 33 edges
1. `get()` - 196 edges
2. `push()` - 34 edges
3. `WebSocketClient` - 26 edges
4. `withFullscreenLoading()` - 16 edges
5. `MenuProcessor` - 16 edges
6. `VersionManager` - 15 edges
7. `emit()` - 15 edges
4. `withFullscreenLoading()` - 17 edges
5. `emit()` - 16 edges
6. `MenuProcessor` - 16 edges
7. `VersionManager` - 15 edges
8. `handleDynamicRoutes()` - 15 edges
9. `TableCache` - 14 edges
10. `buildOutboundPilotContextSections()` - 12 edges
## Surprising Connections (you probably didn't know these)
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/system/site/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
- `handleAttachmentDownload()` --calls--> `downloadAttachmentFile()` [INFERRED]
views/cell-storage/refund-confirm/index.vue → utils/download-attachment.ts
- `handleAttachmentDownload()` --calls--> `downloadAttachmentFile()` [INFERRED]
views/cell-storage/payment-confirm/index.vue → utils/download-attachment.ts
- `subscribeTokenRefresh()` --calls--> `push()` [INFERRED]
utils/http/index.ts → views/operations/storage-facility/index.vue
- `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()` [INFERRED]
@@ -75,190 +79,204 @@
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (143): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+135 more)
Nodes (102): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+94 more)
### Community 1 - "Community 1"
Cohesion: 0.01
Nodes (10): fetchDeleteProjectSystemItem(), fetchDeptTree(), fetchGetRoleList(), fetchNotDeletedRegions(), fetchPostBoundUserIds(), fetchRegionList(), loadRoleList(), handleView() (+2 more)
Cohesion: 0.02
Nodes (99): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), fetchPilotCheckupDetail(), markPilotCheckupChecked() (+91 more)
### Community 2 - "Community 2"
Cohesion: 0.02
Nodes (71): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateMenu(), fetchCreateSampleCode() (+63 more)
Nodes (66): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+58 more)
### Community 3 - "Community 3"
Cohesion: 0.03
Nodes (82): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail() (+74 more)
Nodes (40): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+32 more)
### Community 4 - "Community 4"
Cohesion: 0.03
Nodes (40): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+32 more)
Nodes (22): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+14 more)
### Community 5 - "Community 5"
Cohesion: 0.05
Nodes (34): fetchServiceOrderTxnList(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchReceiptConfirmSubmit(), fetchRefundConfirmedList() (+26 more)
Cohesion: 0.03
Nodes (22): fetchLeaveDetail(), fetchLeaveList(), fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchQcMaterialList() (+14 more)
### Community 6 - "Community 6"
Cohesion: 0.06
Nodes (11): fetchGetLatestUpgradeLog(), getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager (+3 more)
Cohesion: 0.05
Nodes (36): fetchServiceOrderTxnList(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchReceiptConfirmSubmit(), fetchRefundConfirmedList() (+28 more)
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (39): completePilotApplication(), deletePilotApplication(), fetchFullmoonTodoList(), fetchPilotApplicationList(), fetchUpdateFullmoonTodoInsurance(), startPilotApplication(), voidPilotApplication(), fetchCollectionProtocolDetail() (+31 more)
Cohesion: 0.07
Nodes (8): updateInfusionScheduleStatus(), handleSubmit(), handleReset(), handleSearch(), WebSocketClient, TableCache, emit(), handleSubmit()
### Community 8 - "Community 8"
Cohesion: 0.06
Nodes (36): disableDevTools(), handleLock(), handleUnlock(), isMobile(), toLogin(), base64ToBytes(), bytesToBase64(), decryptJson() (+28 more)
Cohesion: 0.04
Nodes (28): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleDownloadAttachment() (+20 more)
### Community 9 - "Community 9"
Cohesion: 0.06
Nodes (38): fetchCreateSite(), fetchCreateUser(), fetchDeleteSite(), fetchDeleteUser(), fetchDictDataOptions(), fetchResetPassword(), fetchRoleOptions(), fetchSiteList() (+30 more)
### Community 10 - "Community 10"
Cohesion: 0.06
Nodes (26): for(), set(), walk(), annotateForkEdges(), chainApiToElements(), nextAncestorLink(), if(), ensureCategoryPathDataLoaded() (+18 more)
### Community 11 - "Community 11"
Cohesion: 0.1
Nodes (2): WebSocketClient, TableCache
### Community 12 - "Community 12"
Cohesion: 0.07
Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
### Community 13 - "Community 13"
### Community 10 - "Community 10"
Cohesion: 0.08
Nodes (32): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+24 more)
### Community 11 - "Community 11"
Cohesion: 0.07
Nodes (22): handleBlur(), handleFocus(), handleInput(), fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport() (+14 more)
### Community 14 - "Community 14"
### Community 12 - "Community 12"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 13 - "Community 13"
Cohesion: 0.07
Nodes (20): fetchAttachmentDownloadUrl(), handleDownloadAttachment(), handleAttachmentDownload(), handleReceptionAttachmentDownload(), attachmentFullUrl(), handleAttachmentDownload(), handleAttachmentPreview(), formatDetectMethods() (+12 more)
Nodes (17): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), collectIds(), postorder(), preorder(), applyTreeExpand() (+9 more)
### Community 14 - "Community 14"
Cohesion: 0.08
Nodes (11): maskIdCard(), buildOutboundPilotContextSections(), applicationStatusLabel(), applicationStatusTagType(), infusionMethodLabel(), infusionStatusLabel(), infusionStatusTagType(), qcStatusLabel() (+3 more)
### Community 15 - "Community 15"
Cohesion: 0.09
Nodes (11): maskIdCard(), buildOutboundPilotContextSections(), applicationStatusLabel(), applicationStatusTagType(), infusionMethodLabel(), infusionStatusLabel(), infusionStatusTagType(), qcStatusLabel() (+3 more)
Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
### Community 16 - "Community 16"
Cohesion: 0.1
Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
Cohesion: 0.09
Nodes (20): fetchCreateSite(), fetchDeleteSite(), fetchSiteList(), fetchUpdateSite(), fetchUserList(), formatHandoverUserLabel(), formatNowDatetime(), remoteSearchHandoverUsers() (+12 more)
### Community 17 - "Community 17"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
Cohesion: 0.11
Nodes (21): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolDetail(), buildInsuranceAttachmentsValue(), buildInsurancePayload(), formatYmdFromAny(), fullFileUrl(), handleBatchEdit() (+13 more)
### Community 18 - "Community 18"
Cohesion: 0.1
Nodes (6): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), handleClose(), handleSubmit(), loadTaskDetail()
Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
### Community 19 - "Community 19"
Cohesion: 0.13
Nodes (18): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), clearTableSelection(), handleBatchComplete(), handleBatchStart() (+10 more)
### Community 20 - "Community 20"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
### Community 21 - "Community 21"
Cohesion: 0.1
Nodes (6): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), handleClose(), handleSubmit(), loadTaskDetail()
### Community 22 - "Community 22"
Cohesion: 0.14
Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
### Community 23 - "Community 23"
Cohesion: 0.16
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
### Community 20 - "Community 20"
Cohesion: 0.15
Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
### Community 21 - "Community 21"
### Community 24 - "Community 24"
Cohesion: 0.2
Nodes (2): ComponentLoader, RouteTransformer
### Community 22 - "Community 22"
### Community 25 - "Community 25"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 23 - "Community 23"
### Community 26 - "Community 26"
Cohesion: 0.17
Nodes (11): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleDelete(), handleReset(), handleSearch() (+3 more)
### Community 27 - "Community 27"
Cohesion: 0.14
Nodes (8): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), themeAnimation(), toggleTheme(), toggleTransition()
### Community 25 - "Community 25"
### Community 29 - "Community 29"
Cohesion: 0.17
Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
### Community 26 - "Community 26"
Cohesion: 0.2
Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
### Community 27 - "Community 27"
Cohesion: 0.28
Nodes (1): FireworkSystem
### Community 29 - "Community 29"
### Community 31 - "Community 31"
Cohesion: 0.39
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
### Community 30 - "Community 30"
### Community 32 - "Community 32"
Cohesion: 0.36
Nodes (1): TableHeightCalculator
### Community 31 - "Community 31"
### Community 33 - "Community 33"
Cohesion: 0.25
Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
### Community 34 - "Community 34"
Cohesion: 0.32
Nodes (3): annotateForkEdges(), chainApiToElements(), nextAncestorLink()
### Community 35 - "Community 35"
Cohesion: 0.33
Nodes (2): logApiCryptoBootInfo(), maskSecret()
### Community 32 - "Community 32"
### Community 36 - "Community 36"
Cohesion: 0.47
Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
### Community 36 - "Community 36"
Cohesion: 0.5
Nodes (3): reload(), handleMenuJump(), openExternalLink()
### Community 39 - "Community 39"
Cohesion: 0.33
Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
### Community 37 - "Community 37"
### Community 41 - "Community 41"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 38 - "Community 38"
### Community 42 - "Community 42"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 39 - "Community 39"
### Community 43 - "Community 43"
Cohesion: 0.5
Nodes (1): fetchSampleSmsNotificationList()
Nodes (1): fetchStorageEntrySmsList()
### Community 41 - "Community 41"
### Community 45 - "Community 45"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 43 - "Community 43"
### Community 47 - "Community 47"
Cohesion: 1.0
Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
## Knowledge Gaps
- **Thin community `Community 11`** (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`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 21`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
- **Thin community `Community 32`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 26`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
- **Thin community `Community 33`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 27`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
- **Thin community `Community 35`** (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 30`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
- **Thin community `Community 39`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
- **Thin community `Community 41`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 37`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
- **Thin community `Community 42`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 38`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
- **Thin community `Community 43`** (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 39`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts`
- **Thin community `Community 45`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 43`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
- **Thin community `Community 47`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 39`, `Community 9`, `Community 10`, `Community 13`, `Community 16`, `Community 18`, `Community 20`, `Community 25`, `Community 26`?**
_High betweenness centrality (0.284) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 3` to `Community 1`, `Community 36`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 27`?**
_High betweenness centrality (0.142) - this node is a cross-community bridge._
- **Are the 192 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
_`get()` has 192 INFERRED edges - model-reasoned connections that need verification._
- **Are the 32 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
_`push()` has 32 INFERRED edges - model-reasoned connections that need verification._
- **Are the 15 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
_`withFullscreenLoading()` has 15 INFERRED edges - model-reasoned connections that need verification._
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 11`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 26`, `Community 29`, `Community 33`, `Community 34`, `Community 39`, `Community 43`?**
_High betweenness centrality (0.283) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 1` to `Community 0`, `Community 7`, `Community 8`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`?**
_High betweenness centrality (0.122) - this node is a cross-community bridge._
- **Are the 195 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
_`get()` has 195 INFERRED edges - model-reasoned connections that need verification._
- **Are the 33 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
_`push()` has 33 INFERRED edges - model-reasoned connections that need verification._
- **Are the 16 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
_`withFullscreenLoading()` has 16 INFERRED edges - model-reasoned connections that need verification._
- **Are the 15 inferred relationships involving `emit()` (e.g. with `handleSubmit()` and `handleBlur()`) actually correct?**
_`emit()` has 15 INFERRED edges - model-reasoned connections that need verification._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.01 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.01 - nodes in this community are weakly interconnected._
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">1816 nodes &middot; 2360 edges &middot; 253 communities</div>
<div id="stats">1844 nodes &middot; 2394 edges &middot; 262 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 31, "community_name": "Community 31", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 31, "community_name": "Community 31", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 31, "community_name": "Community 31", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 31, "community_name": "Community 31", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 31, "community_name": "Community 31", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 31, "community_name": "Community 31", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 75, "community_name": "Community 75", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 76, "community_name": "Community 76", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 17, "community_name": "Community 17", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 17, "community_name": "Community 17", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 17, "community_name": "Community 17", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 17, "community_name": "Community 17", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 17, "community_name": "Community 17", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 267}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 182}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 127}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 120}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 103}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 56}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 55}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 51}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 49}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 47}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 42}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 42}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 41}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 40}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 35}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 29}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 27}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 22}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 22}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 17}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 17}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 17}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 16}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 15}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 13}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 12}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 10}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 9}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 9}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 8}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 8}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 7}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 6}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 6}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 6}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 6}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 5}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 5}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 4}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 4}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 4}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 3}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 3}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 3}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 3}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 2}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 2}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 2}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 2}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 2}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2}, {Line truncated
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 35, "community_name": "Community 35", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 35, "community_name": "Community 35", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 35, "community_name": "Community 35", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 35, "community_name": "Community 35", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 35, "community_name": "Community 35", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 35, "community_name": "Community 35", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 79, "community_name": "Community 79", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 80, "community_name": "Community 80", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 20, "community_name": "Community 20", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 20, "community_name": "Community 20", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 20, "community_name": "Community 20", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 20, "community_name": "Community 20", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 20, "community_name": "Community 20", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective", "laLine truncated
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 303}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 146}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 113}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 104}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 65}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 59}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 58}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 54}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 53}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 43}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 40}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 40}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 36}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 35}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 31}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 31}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 30}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 27}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 27}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 24}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 22}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 22}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 20}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 17}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 17}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 16}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 16}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 15}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 13}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 12}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 9}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 8}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 8}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 8}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 8}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 7}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 6}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 6}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 6}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 6}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 6}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 5}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 4}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 4}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 4}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 3}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 3}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 3}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 3}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 3}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 3}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 3}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 3}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 3}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 2}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 2}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 2}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 2}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 2}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 2}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 2}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 2}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 2}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 2}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 2}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 2}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 2}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 2}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 2}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 2}, {"cid": 70, "color": "#4E79A7", "label": "Community 70", "count": 2},Line truncated
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+28 -19
View File
@@ -17,8 +17,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779802025.7709928,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779800759.9701834,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779830492.5032978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779829952.0242388,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -44,7 +44,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fullscreen-loading.ts": 1779117454.818312,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/router.ts": 1778582068.0393815,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-labels.ts": 1779199125.4894068,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1779800687.4427748,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1779826490.6921933,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fetch-paged-list.ts": 1778775532.895782,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sort-order.ts": 1779790341.2952042,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/outbound-qc-pilot-view-sections.ts": 1779801033.9457984,
@@ -174,16 +174,18 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-cutter-img/index.vue": 1779525781.6051006,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/index.vue": 1779790325.7480125,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/finance-attachments.ts": 1778987672.644368,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779790326.0876904,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779824466.9487405,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/dialog-form-body/index.vue": 1779790325.0384986,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue": 1779790326.2389948,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue": 1779824462.0648055,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1779790325.101185,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1779790325.4147334,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-production-info-cards/index.vue": 1779790326.1909053,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-storage-release-info/index.vue": 1779542542.2434525,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/infusion-schedule-outbound-chain-view/index.vue": 1779830492.0922873,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/work-order-remark-dialog/index.vue": 1779790326.4187233,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/outbound-material-form-sections/index.vue": 1779830388.1680186,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-payment-detail-readonly/index.vue": 1779525781.6098895,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779790326.3887281,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779811081.0609167,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/production-task-view-dialog/index.vue": 1779790325.572335,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-dialog-bridge/index.vue": 1779790325.0697935,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/common/amount-input/index.vue": 1779790326.44512,
@@ -203,6 +205,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/leave.ts": 1778666177.2933538,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception-check-item.ts": 1779007909.2933023,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production.ts": 1779790323.3656075,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts": 1779828287.9085703,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts": 1779827106.6848028,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-item.ts": 1779017836.101254,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-release-review.ts": 1779615813.3431153,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/workflow.ts": 1779790323.5709383,
@@ -213,7 +217,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/chinaRegion.ts": 1778582067.8620915,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-entry-sms-notification.ts": 1779691739.8547683,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/business.ts": 1779159587.6033742,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1779790323.0007687,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1779809235.6101413,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/ethnic.ts": 1778929297.3919344,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-lab-test-item.ts": 1779017838.8231559,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1779594447.3049195,
@@ -241,7 +245,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1779790344.50928,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1779790347.9803133,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/index.vue": 1779790344.752764,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue": 1779790344.1219785,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue": 1779805190.0284903,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/fullmoon-todo/index.vue": 1779790343.5693362,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-entry-info/index.vue": 1779790348.068867,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/index.vue": 1779790344.275864,
@@ -294,8 +298,8 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/source-chain/chainToFlow.ts": 1779790358.1472504,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779790359.1428778,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779790360.846551,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779801736.748366,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779790360.6636097,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779805190.0299485,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779805190.0304468,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779801416.9937282,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779790360.26504,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779790359.9189978,
@@ -307,30 +311,31 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779801620.1553593,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue": 1779701250.4415438,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable-pack/index.vue": 1779790358.448969,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779790358.8545897,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779805190.029252,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779790359.3889246,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779790358.9345891,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1779525781.6949234,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779799317.640114,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779829827.0884113,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue": 1779801046.9472926,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779790362.415862,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779790361.1549423,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779790361.5001903,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779805190.0313084,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779790362.5893314,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779801145.0078545,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue": 1779790362.6423476,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue": 1779701230.688804,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue": 1779790362.6831455,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779790361.21638,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779790362.7538998,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779805190.032101,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/500/index.vue": 1779546148.0553653,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/403/index.vue": 1779546144.3909206,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/404/index.vue": 1779546146.242008,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/cell-product-type/index.vue": 1779790350.9383118,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/index.vue": 1779790354.3513076,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue": 1779790354.1964018,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue": 1779790354.895791,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue": 1779791979.0257256,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue": 1779828418.9739575,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue": 1779830388.4782345,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue": 1779830147.6915238,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleCancelDialog.vue": 1779785733.3155978,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleEditDialog.vue": 1779790355.024449,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue": 1779792910.3544722,
@@ -338,9 +343,13 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/ServiceOrderTxnDialog.vue": 1779792403.5058112,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/index.vue": 1779790353.7514098,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue": 1779790352.7457726,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue": 1779786942.5154335,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/index.vue": 1779825987.213009,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue": 1779830031.9999688,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue": 1779824973.037717,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseMaterialDialog.vue": 1779829323.2023485,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue": 1779805793.0933356,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue": 1779790355.5310535,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue": 1779790356.8604274,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue": 1779830501.0475526,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue": 1779791712.2092516,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/cell-product-price/index.vue": 1779790350.411278,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/index.vue": 1779790354.8558528,
@@ -377,7 +386,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/RoutePermissionValidator.ts": 1778582068.0127926,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/index.ts": 1778582068.0138035,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/IframeRouteManager.ts": 1778582068.0121944,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1779772106.7115505,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1779809233.3957787,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/index.ts": 1778598510.6043763,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/exception.ts": 1779546186.993006,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/routes/staticRoutes.ts": 1779546226.1088154,
-13
View File
@@ -2217,19 +2217,6 @@ export const createOutboundQcWorkOrder = (data: any) =>
params: data
})
// ========== 中试服务 - 放行审核 ==========
export const createReleaseReview = (data: any) =>
request.post({ url: '/web/admin/pilot-release-review/create', params: data })
export const fetchReleaseReviewList = (params: any) =>
request.get({ url: '/web/admin/pilot-release-review/list', params })
export const updateReleaseReview = (data: any) =>
request.put({ url: '/web/admin/pilot-release-review/update', params: data })
export const deleteReleaseReview = (data: any) =>
request.del({ url: '/web/admin/pilot-release-review/delete', data })
// ========== 中试服务 - 应用确认 ==========
export const createInfusionConfirm = (data: any) =>
request.post({ url: '/web/admin/pilot-infusion-confirm/create', params: data })
@@ -0,0 +1,60 @@
import request from '@/utils/http'
export type OutboundHandoverInfo = {
handoverUserId?: string
handoverPerson?: string
handoverTime?: string
handoverLocation?: string
receiverPerson?: string
remark?: string
}
export type OutboundHandoverDetail = {
scheduleSummary?: {
applicationNo?: string
patientName?: string
infusionTime?: string
}
outboundMaterial?: {
remark?: string
cells?: Array<{ cellProductId?: string; remark?: string }>
qcMaterials?: Array<{ qcMaterialId?: string; qty?: number; remark?: string }>
paperMaterials?: Array<{
paperName?: string
copies?: number
attachmentUrl?: string
remark?: string
}>
}
handover?: OutboundHandoverInfo
}
export function fetchInfusionScheduleOutboundHandover(scheduleId: string) {
return request.get<OutboundHandoverDetail>({
url: `/web/admin/pilot-infusion-schedule-mgmt/${scheduleId}/outbound-handover`
})
}
export function saveInfusionScheduleOutboundHandover(
scheduleId: string,
data: {
outboundMaterial: {
remark?: string
cells: { cellProductId: string; remark?: string }[]
qcMaterials: { materialId: string; qty: number; remark?: string }[]
paperMaterials: { paperName: string; copies: number; attachmentUrl: string; remark?: string }[]
}
handover: {
handoverUserId: string
handoverTime: string
handoverLocation?: string
receiverPerson: string
remark?: string
}
}
) {
return request.put<OutboundHandoverDetail>({
url: `/web/admin/pilot-infusion-schedule-mgmt/${scheduleId}/outbound-handover`,
data
})
}
@@ -0,0 +1,219 @@
import request from '@/utils/http'
import type {
OutboundQcDetailApplication,
OutboundQcDetailPatient,
OutboundQcDetailInfusionPlan,
OutboundQcDetailInfusionSchedule,
OutboundQcDetailProduct,
} from '@/api/qc-work-order'
type PageList<T> = {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export type OutboundReleaseReview = {
id: string
scheduleId?: string
/** pilot_service=中试服务 clinical_application=临床应用 */
bizType?: string
bizTypeLabel?: string
applicationNo?: string
patientName?: string
cellType?: string
specText?: string
infusionTime?: string
scheduleStatus?: string
scheduleStatusLabel?: string
qcStatus?: string
qcStatusLabel?: string
reviewConclusion?: string
reviewConclusionLabel?: string
reviewRemark?: string
reviewAttachments?: string
reviewByName?: string
reviewTime?: string
createTime?: string
updateTime?: string
}
export type {
OutboundQcDetailApplication,
OutboundQcDetailPatient,
OutboundQcDetailInfusionPlan,
OutboundQcDetailInfusionSchedule,
OutboundQcDetailProduct,
}
export type OutboundReviewInfo = {
id?: string
scheduleId?: string
reviewConclusion?: string
reviewRemark?: string
reviewAttachments?: string
reviewByName?: string
reviewTime?: string
}
export type OutboundProductionTask = {
id?: string
taskNo?: string
taskName?: string
taskStatus?: string
taskStatusLabel?: string
conclusion?: string
conclusionLabel?: string
startedTime?: string
completedTime?: string
}
export type OutboundProductionInfo = {
workOrderNo?: string
workOrderStatus?: string
workOrderStatusLabel?: string
tasks?: OutboundProductionTask[]
}
export type OutboundQcTask = {
id?: string
taskType?: string
taskTypeLabel?: string
taskStatus?: string
taskStatusLabel?: string
conclusion?: string
conclusionLabel?: string
}
export type OutboundQcInfo = {
workOrderNo?: string
workOrderStatus?: string
workOrderStatusLabel?: string
tasks?: OutboundQcTask[]
}
export type OutboundReleaseReviewDetail = {
application?: OutboundQcDetailApplication
patient?: OutboundQcDetailPatient
infusionPlan?: OutboundQcDetailInfusionPlan
infusionSchedule?: OutboundQcDetailInfusionSchedule
products?: OutboundQcDetailProduct[]
productionInfo?: OutboundProductionInfo
qcInfo?: OutboundQcInfo
reviewInfo?: OutboundReviewInfo
outboundMaterial?: OutboundReleaseMaterialDetail
}
export type OutboundReleaseCellRow = {
id?: string
cellProductId?: string
productCode?: string
productName?: string
productTypeName?: string
specName?: string
passage?: string
batchNo?: string
storageSlotName?: string
productStatus?: string
remark?: string
}
export type OutboundReleaseQcMaterialRow = {
id?: string
qcMaterialId?: string
qty?: number
materialCode?: string
materialName?: string
specModel?: string
unit?: string
remark?: string
}
export type OutboundReleasePaperRow = {
id?: string
paperName?: string
copies?: number
attachmentUrl?: string
remark?: string
}
export type OutboundReleaseMaterialDetail = {
id?: string
infusionScheduleId?: string
paperAttachments?: string
paperMaterials?: OutboundReleasePaperRow[]
remark?: string
cells?: OutboundReleaseCellRow[]
qcMaterials?: OutboundReleaseQcMaterialRow[]
createTime?: string
updateTime?: string
}
export function fetchOutboundReleaseReviewList(params?: {
page?: number
pageSize?: number
scheduleStatus?: string
reviewConclusion?: string
qcStatus?: string
keyword?: string
}) {
return request.get<PageList<OutboundReleaseReview>>({
url: '/web/admin/pilot-release-reviews',
params,
})
}
export function fetchOutboundReleaseReviewDetail(scheduleId: string) {
return request.get<OutboundReleaseReviewDetail>({
url: `/web/admin/pilot-release-reviews/${scheduleId}/detail`,
})
}
export function submitOutboundReleaseReview(
scheduleId: string,
data: {
reviewConclusion: string
reviewRemark?: string
reviewAttachments?: string
}
) {
return request.put<OutboundReleaseReview>({
url: `/web/admin/pilot-release-reviews/${scheduleId}/review`,
data,
})
}
export function batchReviewOutboundRelease(data: {
scheduleIds: string[]
reviewConclusion: string
reviewRemark?: string
reviewAttachments?: string
}) {
return request.put<void>({
url: '/web/admin/pilot-release-reviews/batch-review',
data,
})
}
export function fetchOutboundReleaseMaterial(scheduleId: string) {
return request.get<OutboundReleaseMaterialDetail>({
url: `/web/admin/pilot-release-reviews/${scheduleId}/outbound-materials`,
})
}
export function saveOutboundReleaseMaterial(
scheduleId: string,
data: {
paperMaterials?: { paperName: string; copies: number; attachmentUrl: string; remark?: string }[]
remark?: string
cells: { cellProductId: string; remark?: string }[]
qcMaterials: { materialId: string; qty: number; remark?: string }[]
}
) {
return request.put<OutboundReleaseMaterialDetail>({
url: `/web/admin/pilot-release-reviews/${scheduleId}/outbound-materials`,
data,
})
}
@@ -0,0 +1,682 @@
<!-- 输注计划出库链路只读区(生产/质检/物料/放行/交接/应用确认) -->
<template>
<div v-loading="loading" class="infusion-schedule-outbound-chain">
<template v-if="detail?.id || scheduleId">
<div class="outbound-chain-section">
<div class="section-subtitle">出库生产工单 + 任务</div>
<SampleProductionInfoCards :production-info="detail?.productionInfo" />
</div>
<div class="outbound-chain-section">
<div class="section-subtitle">出库质检工单 + 任务</div>
<SampleQcInfoCards :qc-info="detail?.qcInfo" hide-sample-context />
</div>
<div class="outbound-chain-section">
<div class="section-subtitle">出库物料准备</div>
<div class="outbound-material-view">
<div v-if="!hasOutboundMaterial" class="outbound-material-empty">暂无出库物料准备记录</div>
<template v-else>
<div v-if="outboundMaterialCells.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">出库细胞</h4>
<el-table
:data="outboundMaterialCells"
class="list-table inner-table"
stripe
size="small"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column
prop="productCode"
label="产品编码"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="productName"
label="产品名称"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="productTypeName" label="类型" min-width="90" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column
prop="storageSlotName"
label="储物格"
min-width="130"
show-overflow-tooltip
/>
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="outboundMaterialQcRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">使用的物料</h4>
<el-table
:data="outboundMaterialQcRows"
class="list-table inner-table"
stripe
size="small"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="specModel" label="规格型号" min-width="110" />
<el-table-column label="计划用量" min-width="100" align="center">
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="72" align="center" />
</el-table>
</div>
<div v-if="outboundPaperRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">其他纸质材料</h4>
<el-table
:data="outboundPaperRows"
class="list-table inner-table"
stripe
size="small"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="份数" min-width="80" align="center">
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<template #default="{ row }">
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
<div
v-for="(att, attIdx) in parsePaperAttachments(row.attachmentUrl)"
:key="attIdx"
class="paper-att-card"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<div class="paper-att-card__actions">
<el-icon
v-if="isImageFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
</div>
</div>
</div>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<p v-if="outboundMaterialRemark" class="outbound-material-remark">
备注:{{ outboundMaterialRemark }}
</p>
</template>
</div>
</div>
<div class="outbound-chain-section">
<div class="section-subtitle">出库放行审核</div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">审核结论</div>
<div class="data-value">
<el-tag :type="reviewConclusionTag as any" size="small">
{{ reviewConclusionDisplay }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">审核人</div>
<div class="data-value">{{ dash(review.reviewByName) }}</div>
</div>
<div class="data-item">
<div class="data-label">审核时间</div>
<div class="data-value">{{ dash(review.reviewTime) }}</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">审核备注</div>
<div class="data-value data-value--muted">{{ dash(review.reviewRemark) }}</div>
</div>
</div>
<div v-if="reviewAttachmentFiles.length" class="attachment-row">
<div class="data-label">审核附件</div>
<ProtocolAttachmentReadonly
:files="reviewAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</div>
<div class="outbound-chain-section">
<div class="section-subtitle">出库交接</div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">交接人</div>
<div class="data-value">{{ dash(outboundHandover.handoverPerson) }}</div>
</div>
<div class="data-item">
<div class="data-label">交接时间</div>
<div class="data-value">{{ dash(outboundHandover.handoverTime) }}</div>
</div>
<div class="data-item">
<div class="data-label">交接地点</div>
<div class="data-value">{{ dash(outboundHandover.handoverLocation) }}</div>
</div>
<div class="data-item">
<div class="data-label">接收人</div>
<div class="data-value">{{ dash(outboundHandover.receiverPerson) }}</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(outboundHandover.remark) }}</div>
</div>
</div>
</div>
<div class="outbound-chain-section">
<div class="section-subtitle">应用确认</div>
<div class="data-grid data-grid--3">
<template v-if="hasInfusionConfirm(infusionConfirm)">
<div class="data-item">
<div class="data-label">确认人</div>
<div class="data-value">{{ dash(infusionConfirm.confirmUserName) }}</div>
</div>
<div class="data-item">
<div class="data-label">确认时间</div>
<div class="data-value">{{ dash(infusionConfirm.confirmTime) }}</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">确认说明</div>
<div class="data-value data-value--muted">{{ dash(infusionConfirm.confirmDesc) }}</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(infusionConfirm.remark) }}</div>
</div>
</template>
<template v-else-if="isInfusionConfirmed">
<div class="data-item">
<div class="data-label">确认状态</div>
<div class="data-value">
<el-tag :type="infusionStatusTagType('yrqr') as any" size="small">
{{ infusionStatusLabel('yrqr') }}
</el-tag>
</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">说明</div>
<div class="data-value data-value--muted">
已在输注计划管理中确认,暂无独立应用确认记录
</div>
</div>
</template>
<div v-else class="data-item">
<div class="data-label">确认状态</div>
<div class="data-value">
<el-tag type="info" size="small">未确认</el-tag>
</div>
</div>
</div>
</div>
</template>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
append-to-body
style="backdrop-filter: none"
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Document, Download, View } from '@element-plus/icons-vue'
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { fetchInfusionScheduleMgmtDetail } from '@/api/admin'
import {
storageReleaseConclusionLabel,
storageReleaseConclusionTagType,
infusionStatusLabel,
infusionStatusTagType,
cellProductStatusLabel,
cellProductStatusTagType
} from '@/utils/status-tag-registry'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'InfusionScheduleOutboundChainView' })
const props = withDefaults(
defineProps<{
scheduleId?: string
/** 外部已加载的详情(如 ScheduleViewDialog),传入则不再请求 */
detailData?: Record<string, any> | null
/** 懒加载:为 false 时不请求(申请单查看折叠态) */
active?: boolean
}>(),
{
scheduleId: '',
detailData: null,
active: true
}
)
const loading = ref(false)
const fetchedDetail = ref<Record<string, any> | null>(null)
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
type PaperAttachmentView = {
fileName: string
attachmentUrl: string
fileUrl: string
fileType?: string
}
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const detail = computed(() => props.detailData || fetchedDetail.value)
const review = computed(() => detail.value?.reviewInfo || {})
const outboundHandover = computed(() => detail.value?.outboundHandover || {})
const infusionConfirm = computed(() => detail.value?.infusionConfirm || {})
const outboundMaterial = computed(() => detail.value?.outboundMaterial || null)
const outboundMaterialCells = computed(() => outboundMaterial.value?.cells || [])
const outboundMaterialQcRows = computed(() => outboundMaterial.value?.qcMaterials || [])
const outboundMaterialRemark = computed(() => outboundMaterial.value?.remark || '')
const outboundPaperRows = computed(() => {
const materials = outboundMaterial.value?.paperMaterials || []
if (materials.length) {
return materials.map((p: Record<string, any>) => ({
paperName: p.paperName || '',
copies: p.copies,
attachmentUrl: p.attachmentUrl || ''
}))
}
const urls = outboundMaterial.value?.paperAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u: string) => u.trim())
.filter(Boolean)
.map((attachmentUrl: string) => ({
paperName: paperAttachmentName(attachmentUrl),
copies: 1,
attachmentUrl
}))
})
const hasOutboundMaterial = computed(
() =>
outboundMaterialCells.value.length > 0 ||
outboundMaterialQcRows.value.length > 0 ||
outboundPaperRows.value.length > 0 ||
!!outboundMaterialRemark.value.trim()
)
const reviewAttachmentFiles = computed(() => {
const urls = review.value?.reviewAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u: string) => u.trim())
.filter(Boolean)
.map((fileUrl: string, i: number) => ({
id: `att_${i}`,
fileUrl,
fileName: fileUrl.split('/').pop() || ''
}))
})
const reviewConclusionValue = computed(() => review.value?.reviewConclusion || '')
const reviewConclusionDisplay = computed(() =>
reviewConclusionValue.value
? storageReleaseConclusionLabel(reviewConclusionValue.value)
: '待审核'
)
const reviewConclusionTag = computed(() =>
reviewConclusionValue.value
? storageReleaseConclusionTagType(reviewConclusionValue.value)
: 'info'
)
const isInfusionConfirmed = computed(() => {
const d = detail.value || {}
const sched = d.infusionSchedule || {}
return sched.infusionStatus === 'yrqr' || d.infusionStatus === 'yrqr'
})
function dash(v: unknown) {
if (v == null || v === '') return '—'
return String(v)
}
function hasInfusionConfirm(confirm: Record<string, any>) {
return !!(confirm?.id || confirm?.confirmTime || confirm?.confirmUserName)
}
function paperAttachmentName(url: string) {
return url.split('/').pop() || '附件'
}
function splitUrls(str: string): string[] {
if (!str?.trim()) return []
return str
.split(',')
.map((u) => u.trim())
.filter(Boolean)
}
function parsePaperAttachments(urlStr: string): PaperAttachmentView[] {
return splitUrls(urlStr).map((url) => ({
fileName: paperAttachmentName(url),
attachmentUrl: url,
fileUrl: url
}))
}
function isImageFile(file: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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 attachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handlePreviewPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
previewUrl.value = attachmentFullUrl(att.attachmentUrl)
previewType.value = isVideoFile(att) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
downloadAttachmentFile(
{
fileName: att.fileName,
fileUrl: att.attachmentUrl,
fileType: att.fileType || ''
},
{ fullFileUrl: attachmentFullUrl }
)
}
function handleAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
async function loadDetail() {
const id = (props.scheduleId || '').trim()
if (!id || props.detailData) return
loading.value = true
try {
const res: any = await fetchInfusionScheduleMgmtDetail({ scheduleId: id })
fetchedDetail.value =
res && typeof res === 'object' && !Array.isArray(res)
? (res as Record<string, any>)
: ((res as any)?.data ?? null)
} catch {
fetchedDetail.value = null
} finally {
loading.value = false
}
}
watch(
() => [props.scheduleId, props.active, props.detailData] as const,
([id, active, external]) => {
if (external) {
fetchedDetail.value = null
return
}
if (!id || !active) return
loadDetail()
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
.infusion-schedule-outbound-chain {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #e2e8f0;
}
.outbound-chain-section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.section-subtitle {
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.outbound-material-view {
width: 100%;
}
.outbound-material-empty {
font-size: 13px;
color: #94a3b8;
}
.outbound-material-block {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.outbound-material-subtitle {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
color: #64748b;
}
.outbound-material-remark {
margin: 12px 0 0;
font-size: 13px;
color: #64748b;
}
.attachment-row {
margin-top: 12px;
}
.data-grid {
display: grid;
gap: 12px 16px;
&--3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.data-item--full {
grid-column: 1 / -1;
}
.data-label {
font-size: 11px;
color: #64748b;
margin-bottom: 4px;
}
.data-value {
font-size: 14px;
color: #0f172a;
font-weight: 500;
word-break: break-word;
&--muted {
color: #475569;
font-weight: 400;
}
}
:deep(.sample-card__section-label) {
display: none;
}
:deep(.sample-card__section--qc),
:deep(.sample-card__section--production) {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.paper-att-cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.paper-att-card {
width: 100px;
height: 72px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 4px;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
margin-bottom: 2px;
}
.paper-att-card__name {
width: 100%;
font-size: 11px;
color: #334155;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.paper-att-card__actions {
position: absolute;
top: 2px;
right: 2px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.15s;
}
.paper-att-card:hover .paper-att-card__actions {
opacity: 1;
}
.paper-att-card__action {
width: 18px;
height: 18px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.35);
cursor: pointer;
&:hover {
background: #10b981;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -0,0 +1,977 @@
<!-- 出库物料编辑区(细胞 / 质检物料 / 纸质材料) -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="outbound-material-form-sections">
<div class="material-sections">
<!-- 出库细胞 -->
<section class="section-card">
<div class="section-head">
<h3 class="section-title section-title--mint-bar">出库细胞</h3>
<el-button type="primary" size="small" class="btn-main" @click="openCellSelect">
<el-icon><Plus /></el-icon>
添加细胞产品
</el-button>
</div>
<div class="list-body list-wrap inner-table-wrap">
<el-table
:data="cellRows"
class="list-table inner-table"
stripe
max-height="280"
empty-text="请从细胞产品库勾选"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column
prop="productCode"
label="产品编码"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="productName"
label="产品名称"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="productTypeName" label="产品类型" min-width="100" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column prop="passage" label="代次" min-width="70" align="center" />
<el-table-column
prop="storageSlotName"
label="储物格"
min-width="140"
show-overflow-tooltip
/>
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="removeCellRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</section>
<!-- 使用的物料 -->
<section class="section-card">
<div class="section-head">
<h3 class="section-title section-title--mint-bar">使用的物料</h3>
<el-button type="primary" size="small" class="btn-main" @click="addMaterialRow">
<el-icon><Plus /></el-icon>
添加物料
</el-button>
</div>
<el-table
v-if="materialRows.length"
:data="materialRows"
:row-key="(row) => row._rowKey"
stripe
class="list-table inner-table"
max-height="280"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column label="物料名称" min-width="180">
<template #default="{ row, $index }">
<el-select
v-model="row.materialId"
filterable
placeholder="搜索并选择物料"
class="inner-cell-input"
@change="(v) => onMaterialSelect($index, v)"
>
<el-option
v-for="opt in materialOptions"
:key="opt.id"
:label="opt.label"
:value="opt.id"
:disabled="isMaterialOptionDisabled(opt.id, $index)"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="120">
<template #default="{ row }">{{ row.specModel || '—' }}</template>
</el-table-column>
<el-table-column label="计划用量" min-width="130">
<template #default="{ row }">
<el-input-number
v-model="row.qty"
:min="0.0001"
:precision="4"
:step="1"
controls-position="right"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center">
<template #default="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="当前库存" min-width="100" align="center">
<template #default="{ row }">
{{ stockQtyDisplay(row.materialId) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="removeMaterialRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<p v-else class="section-empty-hint">点击「添加物料」从质检物料库勾选并填写计划用量</p>
</section>
<!-- 其他纸质材料 -->
<section class="section-card">
<div class="section-head">
<h3 class="section-title section-title--mint-bar">其他纸质材料</h3>
<el-button type="primary" size="small" class="btn-main" @click="addPaperRow">
<el-icon><Plus /></el-icon>
添加纸质材料
</el-button>
</div>
<el-table
v-if="paperRows.length"
:data="paperRows"
:row-key="(row) => row._rowKey"
stripe
class="list-table inner-table"
max-height="280"
>
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column label="名称" min-width="180">
<template #default="{ row }">
<el-input
v-model="row.paperName"
placeholder="请输入材料名称"
maxlength="200"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="份数" min-width="120">
<template #default="{ row }">
<el-input-number
v-model="row.copies"
:min="1"
:precision="0"
:step="1"
controls-position="right"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<template #default="{ row, $index }">
<div class="paper-att-cards">
<div
v-for="(att, attIdx) in row.attachments"
:key="attIdx"
class="paper-att-card"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<div class="paper-att-card__actions">
<el-icon
v-if="isImageFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
<el-icon
class="paper-att-card__action paper-att-card__action--danger"
title="删除"
@click="removePaperAttachment($index, attIdx)"
>
<Delete />
</el-icon>
</div>
</div>
<el-upload
:auto-upload="false"
:show-file-list="false"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="(file) => handlePaperAttachmentChange($index, file)"
>
<div
class="paper-att-card paper-att-card--trigger"
:class="{ 'is-loading': row._uploading }"
>
<el-icon class="paper-att-card__trigger-icon"><Plus /></el-icon>
<span>{{ row._uploading ? '上传中' : '上传' }}</span>
</div>
</el-upload>
</div>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="removePaperRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<p v-else class="section-empty-hint">点击「添加纸质材料」逐条填写名称、份数并上传附件;仅作台账留存,不扣库存</p>
</section>
<el-form label-position="top" class="biz-dialog-form">
<el-form-item label="备注">
<el-input
v-model="formRemark"
type="textarea"
:rows="2"
placeholder="可选"
maxlength="512"
show-word-limit
/>
</el-form-item>
</el-form>
</div>
<!-- 细胞产品选择 -->
<el-dialog
v-model="cellSelectVisible"
title="选择细胞产品"
width="1200px"
class="dialog-form-layout"
align-center
destroy-on-close
append-to-body
:close-on-click-modal="false"
style="backdrop-filter: none"
>
<el-input
v-model="cellSearchKeyword"
placeholder="搜索产品编码或名称"
clearable
style="margin-bottom: 12px"
@input="loadCellProductOptions"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<div class="list-body list-wrap">
<el-table
ref="cellSelectTableRef"
class="list-table"
:data="cellProductOptions"
stripe
row-key="id"
height="380"
v-loading="cellProductLoading"
@selection-change="onCellSelectionChange"
>
<el-table-column
type="selection"
min-width="45"
align="center"
:selectable="isCellRowSelectable"
/>
<el-table-column prop="productCode" label="产品编码" min-width="180" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="productTypeName" label="产品类型" min-width="90" />
<el-table-column prop="passage" label="代次" min-width="70" align="center" />
<el-table-column prop="storageSlotName" label="储物格" min-width="140" show-overflow-tooltip />
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="cellProductStatusTagType(row.productStatus) as any" size="small">
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" min-width="110" show-overflow-tooltip />
<el-table-column prop="specName" label="规格" min-width="90" show-overflow-tooltip />
</el-table>
</div>
<template #footer>
<el-button @click="cellSelectVisible = false">取消</el-button>
<el-button type="primary" :disabled="!cellSelectedRows.length" @click="confirmCellSelect">
确定(已选 {{ cellSelectedRows.length }} 项)
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
append-to-body
style="backdrop-filter: none"
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import {
Plus,
Delete,
Search,
Document,
Download,
View
} from '@element-plus/icons-vue'
import { fetchUploadAttachment } from '@/api/business'
import { fetchCellProductList } from '@/api/cell-product'
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
import type { OutboundReleaseCellRow } from '@/api/outbound-release-review'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { cellProductStatusLabel, cellProductStatusTagType } from '@/utils/status-tag-registry'
const props = withDefaults(
defineProps<{
/** 出库交接等需扣库存场景:仅允许选择已冻存细胞,且已选行保留可选 */
requireFrozenCellProduct?: boolean
}>(),
{
requireFrozenCellProduct: false
}
)
type AttachmentLike = {
fileName?: string
fileUrl?: string
attachmentUrl?: string
fileType?: string
}
type PaperAttachment = {
attachmentUrl: string
fileName: string
fileType?: string
}
type PaperRow = {
_rowKey: string
paperName: string
copies?: number
attachments: PaperAttachment[]
_uploading?: boolean
}
type MaterialRow = {
_rowKey: string
materialId: string
materialName: string
specModel: string
unit: string
qty?: number
remark?: string
}
defineOptions({ name: 'OutboundMaterialFormSections' })
const loading = ref(false)
const formRemark = ref('')
const cellRows = ref<OutboundReleaseCellRow[]>([])
const materialRows = ref<MaterialRow[]>([])
const paperRows = ref<PaperRow[]>([])
const materialOptions = ref<QcMaterialOption[]>([])
const cellSelectVisible = ref(false)
const cellSearchKeyword = ref('')
const cellProductOptions = ref<Record<string, any>[]>([])
const cellProductLoading = ref(false)
const cellSelectedRows = ref<Record<string, any>[]>([])
const cellSelectTableRef = ref()
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
let materialRowSeq = 0
let paperRowSeq = 0
function nextMaterialRowKey() {
materialRowSeq += 1
return `mr_${Date.now()}_${materialRowSeq}`
}
function nextPaperRowKey() {
paperRowSeq += 1
return `pr_${Date.now()}_${paperRowSeq}`
}
function fullFileUrl(url: string) {
if (!url) return ''
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) {
return url
}
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
}
function isImageFile(file: AttachmentLike) {
const url = (file.fileUrl || file.attachmentUrl || '').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: AttachmentLike) {
const url = (file.fileUrl || file.attachmentUrl || '').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 stockQtyDisplay(materialId: string) {
const opt = materialOptions.value.find((o) => o.id === materialId)
if (!opt || opt.stockQty == null) return '—'
return String(opt.stockQty)
}
function isMaterialOptionDisabled(materialId: string, currentIndex: number) {
return materialRows.value.some((r, i) => i !== currentIndex && r.materialId === materialId)
}
function onMaterialSelect(index: number, id: string) {
const row = materialRows.value[index]
if (!row) return
if (!id) {
row.materialId = ''
row.materialName = ''
row.specModel = ''
row.unit = ''
return
}
if (isMaterialOptionDisabled(id, index)) {
ElMessage.warning('该物料已在列表中,请勿重复添加')
row.materialId = ''
return
}
const opt = materialOptions.value.find((o) => o.id === id)
row.materialId = id
row.materialName = opt?.materialName || ''
row.specModel = opt?.specModel || ''
row.unit = opt?.unit || ''
}
function addMaterialRow() {
materialRows.value.push({
_rowKey: nextMaterialRowKey(),
materialId: '',
materialName: '',
specModel: '',
unit: '',
qty: undefined
})
}
function removeMaterialRow(index: number) {
materialRows.value.splice(index, 1)
}
function addPaperRow() {
paperRows.value.push({
_rowKey: nextPaperRowKey(),
paperName: '',
copies: 1,
attachments: []
})
}
function removePaperRow(index: number) {
paperRows.value.splice(index, 1)
}
function removePaperAttachment(rowIndex: number, attachmentIndex: number) {
const row = paperRows.value[rowIndex]
if (!row) return
row.attachments.splice(attachmentIndex, 1)
}
function attachmentFileName(url: string) {
return url.split('/').pop() || '附件'
}
function parseAttachmentList(urlStr: string): PaperAttachment[] {
if (!urlStr?.trim()) return []
return urlStr
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((url) => ({
attachmentUrl: url,
fileName: attachmentFileName(url)
}))
}
function mapPaperRowsFromApi(
paperMaterials: Array<{
paperName?: string
copies?: number
attachmentUrl?: string
}> = [],
legacyUrls = ''
) {
if (paperMaterials.length) {
return paperMaterials.map((p) => ({
_rowKey: nextPaperRowKey(),
paperName: p.paperName || '',
copies: p.copies && p.copies > 0 ? p.copies : 1,
attachments: parseAttachmentList(p.attachmentUrl || '')
}))
}
if (!legacyUrls.trim()) return []
return legacyUrls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((attachmentUrl) => ({
_rowKey: nextPaperRowKey(),
paperName: attachmentFileName(attachmentUrl),
copies: 1,
attachments: [{ attachmentUrl, fileName: attachmentFileName(attachmentUrl) }]
}))
}
function removeCellRow(index: number) {
cellRows.value.splice(index, 1)
}
function isCellRowSelectable(row: Record<string, any>) {
const ids = new Set(cellRows.value.map((c) => c.cellProductId))
if (ids.has(row.id)) return false
if (props.requireFrozenCellProduct && row.productStatus && row.productStatus !== 'frozen') {
return false
}
return true
}
function onCellSelectionChange(rows: Record<string, any>[]) {
cellSelectedRows.value = rows
}
function openCellSelect() {
cellSearchKeyword.value = ''
cellSelectedRows.value = []
cellSelectVisible.value = true
loadCellProductOptions()
}
async function loadCellProductOptions() {
cellProductLoading.value = true
try {
const res = await fetchCellProductList({
page: 1,
pageSize: 100,
keyword: cellSearchKeyword.value?.trim() || undefined,
productStatus: props.requireFrozenCellProduct ? 'frozen' : undefined
})
cellProductOptions.value = res.list || []
} finally {
cellProductLoading.value = false
}
}
function confirmCellSelect() {
if (!cellSelectedRows.value.length) return
const existing = new Set(cellRows.value.map((c) => c.cellProductId))
let added = 0
for (const row of cellSelectedRows.value) {
if (existing.has(row.id)) continue
cellRows.value.push({
cellProductId: row.id,
productCode: row.productCode || '',
productName: row.productName || '',
productTypeName: row.productTypeName || '',
specName: row.specName || '',
passage: row.passage || '',
batchNo: row.batchNo || '',
storageSlotName: row.storageSlotName || '',
productStatus: row.productStatus || ''
})
existing.add(row.id)
added += 1
}
if (added) {
cellSelectVisible.value = false
}
}
async function loadFromApiData(data: {
remark?: string
cells?: OutboundReleaseCellRow[]
qcMaterials?: Array<{
qcMaterialId?: string
materialName?: string
specModel?: string
unit?: string
qty?: number
remark?: string
}>
paperMaterials?: Array<{
paperName?: string
copies?: number
attachmentUrl?: string
}>
paperAttachments?: string
}) {
formRemark.value = data.remark || ''
cellRows.value = (data.cells || []).map((c) => ({ ...c }))
materialRows.value = (data.qcMaterials || []).map((m) => ({
_rowKey: nextMaterialRowKey(),
materialId: m.qcMaterialId || '',
materialName: m.materialName || '',
specModel: m.specModel || '',
unit: m.unit || '',
qty: m.qty,
remark: m.remark || ''
}))
paperRows.value = mapPaperRowsFromApi(data.paperMaterials, data.paperAttachments)
}
async function ensureMaterialOptions() {
if (materialOptions.value.length) return
try {
materialOptions.value = (await fetchQcMaterialOptions()) || []
} catch {
materialOptions.value = []
}
}
function validateMaterial() {
if (paperRows.value.some((r) => r._uploading)) {
ElMessage.warning('纸质材料附件上传中,请稍候')
return false
}
for (const row of materialRows.value) {
if (!row.materialId) {
ElMessage.warning('请为每条物料选择名称')
return false
}
const qty = row.qty
if (qty == null || !Number.isFinite(Number(qty)) || Number(qty) <= 0) {
ElMessage.warning('物料计划用量须大于 0')
return false
}
}
for (const row of paperRows.value) {
if (!row.paperName?.trim()) {
ElMessage.warning('请填写每条纸质材料的名称')
return false
}
const copies = row.copies
if (copies == null || !Number.isFinite(Number(copies)) || Number(copies) <= 0) {
ElMessage.warning('纸质材料份数须大于 0')
return false
}
if (!row.attachments.length) {
ElMessage.warning('请为每条纸质材料上传附件')
return false
}
}
return true
}
function buildSavePayload() {
return {
remark: formRemark.value.trim() || undefined,
paperMaterials: paperRows.value.map((p) => ({
paperName: p.paperName.trim(),
copies: Number(p.copies),
attachmentUrl: p.attachments.map((a) => a.attachmentUrl).join(',')
})),
cells: cellRows.value.map((c) => ({
cellProductId: String(c.cellProductId || ''),
remark: c.remark
})),
qcMaterials: materialRows.value.map((m) => ({
materialId: m.materialId,
qty: Number(m.qty),
remark: m.remark
}))
}
}
onMounted(() => {
ensureMaterialOptions()
})
function resetState() {
cellRows.value = []
materialRows.value = []
paperRows.value = []
formRemark.value = ''
cellSelectVisible.value = false
}
function handlePreviewPaperAttachment(att: PaperAttachment) {
if (!att.attachmentUrl) return
previewUrl.value = fullFileUrl(att.attachmentUrl)
previewType.value = isVideoFile(att) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadPaperAttachment(att: PaperAttachment) {
if (!att.attachmentUrl) return
downloadAttachmentFile(
{
fileName: att.fileName,
fileUrl: att.attachmentUrl,
fileType: att.fileType || ''
},
{ fullFileUrl }
)
}
async function handlePaperAttachmentChange(
index: number,
uploadFile: { raw?: File }
) {
const file = uploadFile.raw
const row = paperRows.value[index]
if (!file || !row || row._uploading) return
row._uploading = true
try {
const fd = new FormData()
fd.append('folder', 'outbound-release-material')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
row.attachments.push({
attachmentUrl: res.fileUrl,
fileName: file.name || res.fileName || attachmentFileName(res.fileUrl),
fileType: res.fileType || file.type || ''
})
} catch {
ElMessage.error('文件上传失败')
} finally {
row._uploading = false
}
}
defineExpose({
loading,
loadFromApiData,
resetState,
validateMaterial,
buildSavePayload,
ensureMaterialOptions
})
</script>
<style scoped lang="scss">
.material-summary {
margin-bottom: 16px;
padding: 12px 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 20px;
}
.material-summary-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.material-summary-label {
color: #64748b;
flex-shrink: 0;
}
.material-summary-value {
color: #1e293b;
font-weight: 500;
}
.material-sections {
display: flex;
flex-direction: column;
gap: 20px;
}
.section-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px 16px 16px;
background: #fff;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 12px;
}
.section-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.section-empty-hint {
margin: 0;
font-size: 13px;
color: #94a3b8;
}
.inner-cell-input {
width: 100%;
}
.paper-att-cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.paper-att-card {
width: 100px;
height: 72px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 4px;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
margin-bottom: 2px;
}
.paper-att-card__name {
width: 100%;
font-size: 11px;
color: #334155;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.paper-att-card__actions {
position: absolute;
top: 2px;
right: 2px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.15s;
}
.paper-att-card:hover .paper-att-card__actions {
opacity: 1;
}
.paper-att-card__action {
width: 18px;
height: 18px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.35);
cursor: pointer;
&:hover {
background: #10b981;
}
&--danger:hover {
background: #ef4444;
}
}
.paper-att-card--trigger {
border: 1px dashed #a7f3d0;
background: transparent;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
&:hover {
border-color: #10b981;
background: #ecfdf5;
}
&.is-loading {
pointer-events: none;
opacity: 0.6;
}
}
.paper-att-card__trigger-icon {
font-size: 16px;
color: #10b981;
margin-bottom: 2px;
}
.paper-att-card--trigger span {
font-size: 11px;
color: #64748b;
}
.preview-container {
text-align: center;
}
</style>
@@ -103,6 +103,8 @@
const props = defineProps<{
visible: boolean
taskId?: string
/** 不展示样本信息、运输信息、样本接收(出库放行审核等) */
hideSampleContext?: boolean
}>()
const emit = defineEmits<{
@@ -265,9 +267,10 @@
}
]
: []
const sampleSections: ViewSection[] = isTempQc
? []
: [
const sampleSections: ViewSection[] =
props.hideSampleContext || isTempQc
? []
: [
{
title: '样本信息',
fields: [
@@ -84,7 +84,11 @@
</template>
<Teleport to="body">
<QcTaskViewDialog v-model:visible="taskViewVisible" :task-id="taskViewId" />
<QcTaskViewDialog
v-model:visible="taskViewVisible"
:task-id="taskViewId"
:hide-sample-context="hideSampleContext"
/>
</Teleport>
</div>
</template>
@@ -144,6 +148,8 @@
const props = defineProps<{
qcInfo?: SampleQcInfo | null
sampleReception?: SampleReceptionBrief | null
/** 任务详情弹窗不展示样本信息、样本接收(出库放行等场景) */
hideSampleContext?: boolean
}>()
const taskViewVisible = ref(false)
@@ -45,12 +45,12 @@
<dt>{{ field.label }}</dt>
<dd>
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{
field.value ?? '—'
formatViewValue(field.value)
}}</el-tag>
<ul v-else-if="field.items?.length" class="view-plain-list">
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<template v-else>{{ field.value ?? '—' }}</template>
<template v-else>{{ formatViewValue(field.value) }}</template>
</dd>
</div>
</dl>
@@ -84,6 +84,13 @@
return val != null && (typeof val === 'object' || typeof val === 'function')
}
/** 查看弹窗字段展示:null/undefined/空字符串 → —;布尔转是/否 */
function formatViewValue(val: unknown): string {
if (val === null || val === undefined || val === '') return '—'
if (typeof val === 'boolean') return val ? '是' : '否'
return String(val)
}
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
export interface ViewField {
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin_ts", "label": "admin.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L1"}, {"id": "api_admin_fetchgetmenulist", "label": "fetchGetMenuList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L10"}, {"id": "api_admin_fetchgetrolelist", "label": "fetchGetRoleList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L19"}, {"id": "api_admin_fetchuseroptions", "label": "fetchUserOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L26"}, {"id": "api_admin_fetchroleoptions", "label": "fetchRoleOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L33"}, {"id": "api_admin_fetchtenantlist", "label": "fetchTenantList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L43"}, {"id": "api_admin_fetchcreatetenant", "label": "fetchCreateTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L56"}, {"id": "api_admin_fetchupdatetenant", "label": "fetchUpdateTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L71"}, {"id": "api_admin_fetchdeletetenant", "label": "fetchDeleteTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L89"}, {"id": "api_admin_fetchuserlist", "label": "fetchUserList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L100"}, {"id": "api_admin_fetchuserdetail", "label": "fetchUserDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L119"}, {"id": "api_admin_fetchcreateuser", "label": "fetchCreateUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L125"}, {"id": "api_admin_fetchupdateuser", "label": "fetchUpdateUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L147"}, {"id": "api_admin_fetchdeleteuser", "label": "fetchDeleteUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L170"}, {"id": "api_admin_fetchresetpassword", "label": "fetchResetPassword()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L177"}, {"id": "api_admin_fetchuploaduseravatar", "label": "fetchUploadUserAvatar()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L185"}, {"id": "api_admin_fetchregionlist", "label": "fetchRegionList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L196"}, {"id": "api_admin_fetchactiveregions", "label": "fetchActiveRegions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L214"}, {"id": "api_admin_fetchnotdeletedregions", "label": "fetchNotDeletedRegions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L231"}, {"id": "api_admin_fetchcategorylist", "label": "fetchCategoryList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L259"}, {"id": "api_admin_fetchcreatecategory", "label": "fetchCreateCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L277"}, {"id": "api_admin_fetchupdatecategory", "label": "fetchUpdateCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L290"}, {"id": "api_admin_fetchdeletecategory", "label": "fetchDeleteCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L307"}, {"id": "api_admin_fetchuploadcategoryimage", "label": "fetchUploadCategoryImage()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L313"}, {"id": "api_admin_fetchunitlist", "label": "fetchUnitList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L324"}, {"id": "api_admin_fetchcreateunit", "label": "fetchCrLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_outbound_material_form_sections_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/outbound-material-form-sections/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_status_tag_registry_ts", "label": "status-tag-registry.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L1"}, {"id": "utils_status_tag_registry_createlabelgetter", "label": "createLabelGetter()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L29"}, {"id": "utils_status_tag_registry_createtagtypegetter", "label": "createTagTypeGetter()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L33"}, {"id": "utils_status_tag_registry_txntypelabel", "label": "txnTypeLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L381"}, {"id": "utils_status_tag_registry_txntypetagtype", "label": "txnTypeTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L392"}, {"id": "utils_status_tag_registry_txnstatuslabel", "label": "txnStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L404"}, {"id": "utils_status_tag_registry_txnstatustagtype", "label": "txnStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L414"}, {"id": "utils_status_tag_registry_applicationstatuslabel", "label": "applicationStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L425"}, {"id": "utils_status_tag_registry_applicationstatustagtype", "label": "applicationStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L429"}, {"id": "utils_status_tag_registry_checkupstatuslabel", "label": "checkupStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L442"}, {"id": "utils_status_tag_registry_checkupstatustagtype", "label": "checkupStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L450"}, {"id": "utils_status_tag_registry_feestatuslabel", "label": "feeStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L458"}, {"id": "utils_status_tag_registry_feestatustagtype", "label": "feeStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L466"}, {"id": "utils_status_tag_registry_schedulestatuslabel", "label": "scheduleStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L475"}, {"id": "utils_status_tag_registry_schedulestatustagtype", "label": "scheduleStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L484"}, {"id": "utils_status_tag_registry_infusionstatuslabel", "label": "infusionStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L495"}, {"id": "utils_status_tag_registry_infusionstatustagtype", "label": "infusionStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L504"}, {"id": "utils_status_tag_registry_infusionmethodlabel", "label": "infusionMethodLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L515"}, {"id": "utils_status_tag_registry_qcstatuslabel", "label": "qcStatusLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L526"}, {"id": "utils_status_tag_registry_qcstatustagtype", "label": "qcStatusTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L530"}, {"id": "utils_status_tag_registry_releasereviewconclusionlabel", "label": "releaseReviewConclusionLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L536"}, {"id": "utils_status_tag_registry_releasereviewconclusiontagtype", "label": "releaseReviewConclusionTagTypeLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "label": "InfusionScheduleHandoverDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L1"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_formatnowdatetime", "label": "formatNowDatetime()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L158"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_formathandoveruserlabel", "label": "formatHandoverUserLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L164"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_applydefaulthandoveruser", "label": "applyDefaultHandoverUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L174"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_resethandoverform", "label": "resetHandoverForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L193"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_remotesearchhandoverusers", "label": "remoteSearchHandoverUsers()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L203"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_onhandoveruserchange", "label": "onHandoverUserChange()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L232"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L115", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L116", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L117", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_outbound_material_form_sections_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L121", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_store_modules_user", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L122", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "infusion_schedule_management_infusionschedulehandoverdialog_formatnowdatetime", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1"}, {"id": "qc_work_order_index_formatconfiguredlabitems", "label": "formatConfiguredLabItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1034"}, {"id": "qc_work_order_index_formatconfiguredthirditems", "label": "formatConfiguredThirdItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1040"}, {"id": "qc_work_order_index_buildtasktypesections", "label": "buildTaskTypeSections()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1051"}, {"id": "qc_work_order_index_buildconfiguredonlysection", "label": "buildConfiguredOnlySection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1109"}, {"id": "qc_work_order_index_formatdetectmethods", "label": "formatDetectMethods()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1127"}, {"id": "qc_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1134"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_formatconfiguredlabitems", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1034", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_formatconfiguredthirditems", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1040", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_buildtasktypesections", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1051", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_buildconfiguredonlysection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1109", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_formatdetectmethods", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1127", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_formatmetric", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1134", "weight": 1.0}], "raw_calls": [{"caller_nid": "qc_work_order_index_formatconfiguredlabitems", "callee": "join", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1036"}, {"caller_nid": "qc_work_order_index_formatconfiguredlabitems", "callee": "map", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1036"}, {"caller_nid": "qc_work_order_index_formatconfiguredthirditems", "callee": "join", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1042"}, {"caller_nid": "qc_work_order_index_formatconfiguredthirditems", "callee": "map", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1042"}, {"caller_nid": "qc_wLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_sample_qc_info_cards_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleaseviewdialog_vue", "label": "OutboundReleaseViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L1"}, {"id": "outbound_release_review_outboundreleaseviewdialog_handleattachmentpreview", "label": "handleAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L366"}, {"id": "outbound_release_review_outboundreleaseviewdialog_handleattachmentdownload", "label": "handleAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L371"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleaseviewdialog_vue", "target": "outbound_release_review_outboundreleaseviewdialog_handleattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L366", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleaseviewdialog_vue", "target": "outbound_release_review_outboundreleaseviewdialog_handleattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L371", "weight": 1.0}], "raw_calls": [{"caller_nid": "outbound_release_review_outboundreleaseviewdialog_handleattachmentpreview", "callee": "attachmentFullUrl", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L367"}, {"caller_nid": "outbound_release_review_outboundreleaseviewdialog_handleattachmentpreview", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L367"}, {"caller_nid": "outbound_release_review_outboundreleaseviewdialog_handleattachmentpreview", "callee": "open", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L368"}, {"caller_nid": "outbound_release_review_outboundreleaseviewdialog_handleattachmentdownload", "callee": "downloadAttachmentFile", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L372"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_outbound_material_form_sections_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/outbound-material-form-sections/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasematerialdialog_vue", "label": "OutboundReleaseMaterialDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseMaterialDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleaseviewdialog_vue", "label": "OutboundReleaseViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseViewDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_scheduleviewdialog_vue", "label": "ScheduleViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "label": "sqxp.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_types_router", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasematerialdialog_vue", "label": "OutboundReleaseMaterialDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseMaterialDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_infusion_schedule_outbound_chain_view_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/infusion-schedule-outbound-chain-view/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "label": "InfusionScheduleHandoverDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L1"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_formatnowdatetime", "label": "formatNowDatetime()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L158"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_formathandoveruserlabel", "label": "formatHandoverUserLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L164"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_applydefaulthandoveruser", "label": "applyDefaultHandoverUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L174"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_resethandoverform", "label": "resetHandoverForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L193"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_remotesearchhandoverusers", "label": "remoteSearchHandoverUsers()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L203"}, {"id": "infusion_schedule_management_infusionschedulehandoverdialog_onhandoveruserchange", "label": "onHandoverUserChange()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L232"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L115", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L116", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L117", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_outbound_material_form_sections_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L121", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_store_modules_user", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue", "source_location": "L122", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue", "target": "infusion_schedule_management_infusionschedulehandoverdialog_formatnowdatetime", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover_ts", "label": "infusion-schedule-handover.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L1"}, {"id": "api_infusion_schedule_handover_fetchinfusionscheduleoutboundhandover", "label": "fetchInfusionScheduleOutboundHandover()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L32"}, {"id": "api_infusion_schedule_handover_saveinfusionscheduleoutboundhandover", "label": "saveInfusionScheduleOutboundHandover()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L38"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover_ts", "target": "api_infusion_schedule_handover_fetchinfusionscheduleoutboundhandover", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L32", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_infusion_schedule_handover_ts", "target": "api_infusion_schedule_handover_saveinfusionscheduleoutboundhandover", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L38", "weight": 1.0}], "raw_calls": [{"caller_nid": "api_infusion_schedule_handover_fetchinfusionscheduleoutboundhandover", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L33"}, {"caller_nid": "api_infusion_schedule_handover_saveinfusionscheduleoutboundhandover", "callee": "put", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts", "source_location": "L56"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_system_view_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_qc_task_view_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L1"}, {"id": "qc_task_view_dialog_index_attachmentfullurl", "label": "attachmentFullUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L396"}, {"id": "qc_task_view_dialog_index_handleattachmentpreview", "label": "handleAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L403"}, {"id": "qc_task_view_dialog_index_handleattachmentdownload", "label": "handleAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L408"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_qc_task_view_dialog_index_vue", "target": "qc_task_view_dialog_index_attachmentfullurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L396", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_qc_task_view_dialog_index_vue", "target": "qc_task_view_dialog_index_handleattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L403", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_qc_task_view_dialog_index_vue", "target": "qc_task_view_dialog_index_handleattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L408", "weight": 1.0}, {"source": "qc_task_view_dialog_index_handleattachmentpreview", "target": "qc_task_view_dialog_index_attachmentfullurl", "relation": "calls", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L404", "weight": 1.0}], "raw_calls": [{"caller_nid": "qc_task_view_dialog_index_attachmentfullurl", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L397"}, {"caller_nid": "qc_task_view_dialog_index_attachmentfullurl", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L397"}, {"caller_nid": "qc_task_view_dialog_index_attachmentfullurl", "callee": "test", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L399"}, {"caller_nid": "qc_task_view_dialog_index_attachmentfullurl", "callee": "replace", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L400"}, {"caller_nid": "qc_task_view_dialog_index_handleattachmentpreview", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L404"}, {"caller_nid": "qc_task_view_dialog_index_handleattachmentpreview", "callee": "open", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L405"}, {"caller_nid": "qc_task_view_dialog_index_handleattachmentdownload", "callee": "downloadAttachmentFile", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue", "source_location": "L409"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_applicationviewdialog_vue", "label": "ApplicationViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L1"}, {"id": "pilot_application_applicationviewdialog_for", "label": "for()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L951"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_applicationviewdialog_vue", "target": "pilot_application_applicationviewdialog_for", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L951", "weight": 1.0}], "raw_calls": [{"caller_nid": "pilot_application_applicationviewdialog_for", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L952"}, {"caller_nid": "pilot_application_applicationviewdialog_for", "callee": "Number", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L955"}, {"caller_nid": "pilot_application_applicationviewdialog_for", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L956"}, {"caller_nid": "pilot_application_applicationviewdialog_for", "callee": "set", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue", "source_location": "L958"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "label": "OutboundReleaseReviewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L1"}, {"id": "outbound_release_review_outboundreleasereviewdialog_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L235"}, {"id": "outbound_release_review_outboundreleasereviewdialog_isimagefile", "label": "isImageFile()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L243"}, {"id": "outbound_release_review_outboundreleasereviewdialog_isvideofile", "label": "isVideoFile()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L253"}, {"id": "outbound_release_review_outboundreleasereviewdialog_removeattachment", "label": "removeAttachment()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L263"}, {"id": "outbound_release_review_outboundreleasereviewdialog_handlepreviewattachment", "label": "handlePreviewAttachment()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L267"}, {"id": "outbound_release_review_outboundreleasereviewdialog_handledownloadattachment", "label": "handleDownloadAttachment()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L274"}, {"id": "outbound_release_review_outboundreleasereviewdialog_resetform", "label": "resetForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L332"}, {"id": "outbound_release_review_outboundreleasereviewdialog_handlesubmit", "label": "handleSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L341"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L170", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L171", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_business", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L181", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L182", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue", "source_location": "L183", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_scheduleviewdialog_vue", "label": "ScheduleViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L1"}, {"id": "pilot_application_index_handleselectionchange", "label": "handleSelectionChange()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L342"}, {"id": "pilot_application_index_cleartableselection", "label": "clearTableSelection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L346"}, {"id": "pilot_application_index_loaddata", "label": "loadData()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L365"}, {"id": "pilot_application_index_handlesearch", "label": "handleSearch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L386"}, {"id": "pilot_application_index_handlereset", "label": "handleReset()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L391"}, {"id": "pilot_application_index_handleview", "label": "handleView()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L403"}, {"id": "pilot_application_index_handlecreate", "label": "handleCreate()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L412"}, {"id": "pilot_application_index_caneditapplication", "label": "canEditApplication()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L418"}, {"id": "pilot_application_index_handleedit", "label": "handleEdit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L420"}, {"id": "pilot_application_index_onformsaved", "label": "onFormSaved()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L425"}, {"id": "pilot_application_index_handledelete", "label": "handleDelete()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L430"}, {"id": "pilot_application_index_runbatchstatusaction", "label": "runBatchStatusAction()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L443"}, {"id": "pilot_application_index_handlebatchstart", "label": "handleBatchStart()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L511"}, {"id": "pilot_application_index_handlebatchcomplete", "label": "handleBatchComplete()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L520"}, {"id": "pilot_application_index_handlebatchvoid", "label": "handleBatchVoid()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L529"}, {"id": "pilot_application_index_handlestart", "label": "handleStart()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L539"}, {"id": "pilot_application_index_handlecomplete", "label": "handleComplete()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L556"}, {"id": "pilot_application_index_handlevoid", "label": "handleVoid()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L573"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_index_vue", "target": "pilot_application_index_handleselectionchange", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/index.vue", "source_location": "L342", "weight": 1.0}, {"sLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "label": "outbound-release-review.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L1"}, {"id": "api_outbound_release_review_fetchoutboundreleasereviewlist", "label": "fetchOutboundReleaseReviewList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L154"}, {"id": "api_outbound_release_review_fetchoutboundreleasereviewdetail", "label": "fetchOutboundReleaseReviewDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L168"}, {"id": "api_outbound_release_review_submitoutboundreleasereview", "label": "submitOutboundReleaseReview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L174"}, {"id": "api_outbound_release_review_batchreviewoutboundrelease", "label": "batchReviewOutboundRelease()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L188"}, {"id": "api_outbound_release_review_fetchoutboundreleasematerial", "label": "fetchOutboundReleaseMaterial()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L200"}, {"id": "api_outbound_release_review_saveoutboundreleasematerial", "label": "saveOutboundReleaseMaterial()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L206"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_qc_work_order", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_fetchoutboundreleasereviewlist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L154", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_fetchoutboundreleasereviewdetail", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L168", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_submitoutboundreleasereview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L174", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_batchreviewoutboundrelease", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L188", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_fetchoutboundreleasematerial", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L200", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_outbound_release_review_ts", "target": "api_outbound_release_review_saveoutboundreleasematerial", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L206", "weight": 1.0}], "raw_calls": [{"caller_nid": "api_outbound_release_review_fetchoutboundreleasereviewlist", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts", "source_location": "L162"}, {"caller_nid": "api_outbound_release_review_fetchoutboundreleasereviewdetail", "callee": "get", "source_file": "/Users/wLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue", "source_location": "L1"}, {"id": "infusion_schedule_management_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue", "source_location": "L489"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_infusion_schedule_management_index_vue", "target": "infusion_schedule_management_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/index.vue", "source_location": "L489", "weight": 1.0}], "raw_calls": []}
+11
View File
@@ -728,6 +728,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
keepAlive: true,
menuCode: 'pilot-service:infusion-schedule-management:list'
}
},
{
id: '80000000000000000000000000000013',
name: 'OutboundReleaseReview',
path: 'outbound-release-review',
component: '/pilot-service/outbound-release-review/index',
meta: {
title: '出库放行审核',
keepAlive: true,
menuCode: 'pilot-service:outbound-release-review:list'
}
}
]
}
@@ -577,3 +577,27 @@ export function productStatusTagType(status: string): string {
}
return map[status] || 'info'
}
// ========== 细胞产品库 - 产品状态 ==========
export function cellProductStatusLabel(status: string): string {
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
sold: '已售卖',
used: '已使用',
voided: '已作废',
thawed: '已复苏'
}
return map[status] || status || '—'
}
export function cellProductStatusTagType(status: string): string {
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
sold: 'warning',
used: 'info',
voided: 'danger',
thawed: 'info'
}
return map[status] || 'info'
}
@@ -0,0 +1,402 @@
<!-- 输注计划管理 — 出库交接 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
:model-value="modelValue"
title="出库交接"
width="1120px"
class="dialog-form-layout outbound-handover-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
style="backdrop-filter: none"
@update:model-value="emit('update:modelValue', $event)"
@closed="handleClosed"
>
<div v-if="scheduleSummary" class="material-summary">
<div class="material-summary-row">
<span class="material-summary-label">申请单号</span>
<span class="material-summary-value">{{ scheduleSummary.applicationNo || '—' }}</span>
</div>
<div class="material-summary-row">
<span class="material-summary-label">患者姓名</span>
<span class="material-summary-value">{{ scheduleSummary.patientName || '—' }}</span>
</div>
<div class="material-summary-row">
<span class="material-summary-label">输注时间</span>
<span class="material-summary-value">{{ scheduleSummary.infusionTime || '—' }}</span>
</div>
</div>
<div v-loading="loading" class="handover-body">
<OutboundMaterialFormSections ref="materialSectionsRef" require-frozen-cell-product />
<section class="section-card handover-form-section">
<h3 class="section-title section-title--mint-bar">交接信息</h3>
<el-form label-position="top" class="biz-dialog-form">
<div class="form-grid-2">
<el-form-item label="交接人" required>
<el-select
v-model="handoverUserId"
filterable
remote
clearable
reserve-keyword
placeholder="搜索并选择用户"
:remote-method="remoteSearchHandoverUsers"
:loading="handoverUserLoading"
class="w-full"
@change="onHandoverUserChange"
>
<el-option
v-for="u in handoverUserOptions"
:key="u.id"
:label="formatHandoverUserLabel(u)"
:value="u.id"
/>
</el-select>
</el-form-item>
<el-form-item label="交接时间" required>
<el-date-picker
v-model="handoverTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
placeholder="选择交接时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="交接地点">
<el-input
v-model="handoverLocation"
placeholder="可选"
maxlength="200"
clearable
/>
</el-form-item>
<el-form-item label="接收人" required>
<el-input
v-model="receiverPerson"
placeholder="请输入接收人"
maxlength="200"
clearable
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="handoverRemark"
type="textarea"
:rows="2"
placeholder="可选"
maxlength="512"
show-word-limit
/>
</el-form-item>
</div>
</el-form>
</section>
</div>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
<el-icon><Check /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Check, Close } from '@element-plus/icons-vue'
import { fetchUserList } from '@/api/admin'
import {
fetchInfusionScheduleOutboundHandover,
saveInfusionScheduleOutboundHandover
} from '@/api/infusion-schedule-handover'
import OutboundMaterialFormSections from '@/components/business/outbound-material-form-sections/index.vue'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'InfusionScheduleHandoverDialog' })
const props = defineProps<{
modelValue: boolean
scheduleId: string
scheduleRow?: Record<string, any> | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'saved'): void
}>()
const userStore = useUserStore()
const materialSectionsRef = ref<InstanceType<typeof OutboundMaterialFormSections> | null>(null)
const loading = ref(false)
const submitting = ref(false)
const scheduleSummary = ref<{
applicationNo?: string
patientName?: string
infusionTime?: string
} | null>(null)
const handoverUserId = ref('')
const handoverTime = ref('')
const handoverLocation = ref('')
const receiverPerson = ref('')
const handoverRemark = ref('')
const handoverUserOptions = ref<Api.User.UserItem[]>([])
const handoverSelectedSnapshot = ref<Api.User.UserItem | null>(null)
const handoverUserLoading = ref(false)
function formatNowDatetime() {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
function formatHandoverUserLabel(u: Api.User.UserItem) {
const name = (u.nickName || '').trim() || '-'
const no = (u.userNo || '').trim()
const phone = (u.phoneNo || '').trim()
const parts = [name]
if (no) parts.push(no)
if (phone) parts.push(phone)
return parts.join(' · ')
}
function applyDefaultHandoverUser() {
const info = (userStore.info || {}) as Record<string, any>
const uid = String(info.userId ?? info.id ?? '').trim()
const nick = String(
info.nickName ?? info.realName ?? info.userName ?? info.username ?? ''
).trim()
if (!uid) return
handoverUserId.value = uid
handoverSelectedSnapshot.value = {
id: uid,
nickName: nick || uid,
userNo: info.userName || info.username || '',
phoneNo: info.phone || info.phoneNo || ''
} as Api.User.UserItem
handoverUserOptions.value = handoverSelectedSnapshot.value
? [handoverSelectedSnapshot.value]
: []
}
function resetHandoverForm() {
handoverUserId.value = ''
handoverTime.value = formatNowDatetime()
handoverLocation.value = ''
receiverPerson.value = ''
handoverRemark.value = ''
handoverUserOptions.value = []
handoverSelectedSnapshot.value = null
}
async function remoteSearchHandoverUsers(query: string) {
handoverUserLoading.value = true
try {
const q = (query || '').trim()
const res = await fetchUserList({
page: 1,
pageSize: 50,
keyword: q || undefined,
status: '1'
})
let list = [...(res.list || [])]
const curId = handoverUserId.value
if (
curId &&
handoverSelectedSnapshot.value?.id === curId &&
!list.some((x) => x.id === curId)
) {
list = [handoverSelectedSnapshot.value, ...list]
}
handoverUserOptions.value = list
} catch {
handoverUserOptions.value = handoverSelectedSnapshot.value
? [handoverSelectedSnapshot.value]
: []
} finally {
handoverUserLoading.value = false
}
}
function onHandoverUserChange(val: string | null | undefined) {
if (!val) {
handoverUserId.value = ''
handoverSelectedSnapshot.value = null
handoverUserOptions.value = []
return
}
const hit = handoverUserOptions.value.find((u) => u.id === val)
if (hit) handoverSelectedSnapshot.value = hit
}
function fillHandoverFromApi(handover?: {
handoverUserId?: string
handoverPerson?: string
handoverTime?: string
handoverLocation?: string
receiverPerson?: string
remark?: string
}) {
const hasRecord = !!(handover?.handoverUserId || handover?.handoverTime || handover?.receiverPerson)
if (!hasRecord) {
resetHandoverForm()
applyDefaultHandoverUser()
return
}
handoverUserId.value = handover?.handoverUserId || ''
handoverTime.value = handover?.handoverTime || formatNowDatetime()
handoverLocation.value = handover?.handoverLocation || ''
receiverPerson.value = handover?.receiverPerson || ''
handoverRemark.value = handover?.remark || ''
if (handoverUserId.value) {
handoverSelectedSnapshot.value = {
id: handoverUserId.value,
nickName: handover?.handoverPerson || handoverUserId.value,
userNo: '',
phoneNo: ''
} as Api.User.UserItem
handoverUserOptions.value = [handoverSelectedSnapshot.value]
}
}
async function loadData() {
if (!props.scheduleId) return
loading.value = true
try {
const data = await fetchInfusionScheduleOutboundHandover(props.scheduleId)
scheduleSummary.value = data.scheduleSummary || {
applicationNo: props.scheduleRow?.applicationNo,
patientName: props.scheduleRow?.patientName,
infusionTime: props.scheduleRow?.infusionTime
}
await materialSectionsRef.value?.ensureMaterialOptions()
await materialSectionsRef.value?.loadFromApiData({
remark: data.outboundMaterial?.remark,
cells: data.outboundMaterial?.cells as any,
qcMaterials: data.outboundMaterial?.qcMaterials,
paperMaterials: data.outboundMaterial?.paperMaterials
})
fillHandoverFromApi(data.handover)
} finally {
loading.value = false
}
}
watch(
() => [props.modelValue, props.scheduleId] as const,
([vis, id]) => {
if (vis && id) {
nextTick(() => loadData())
}
},
{ immediate: true }
)
function handleClosed() {
materialSectionsRef.value?.resetState()
scheduleSummary.value = null
resetHandoverForm()
}
async function handleSubmit() {
if (!props.scheduleId) return
const sections = materialSectionsRef.value
if (!sections?.validateMaterial()) return
if (!handoverUserId.value?.trim()) {
ElMessage.warning('请选择交接人')
return
}
if (!handoverTime.value?.trim()) {
ElMessage.warning('请选择交接时间')
return
}
if (!receiverPerson.value?.trim()) {
ElMessage.warning('请填写接收人')
return
}
submitting.value = true
try {
await saveInfusionScheduleOutboundHandover(props.scheduleId, {
outboundMaterial: sections.buildSavePayload(),
handover: {
handoverUserId: handoverUserId.value.trim(),
handoverTime: handoverTime.value.trim(),
handoverLocation: handoverLocation.value.trim() || undefined,
receiverPerson: receiverPerson.value.trim(),
remark: handoverRemark.value.trim() || undefined
}
})
ElMessage.success('出库交接已保存')
emit('update:modelValue', false)
emit('saved')
} finally {
submitting.value = false
}
}
</script>
<style scoped lang="scss">
.material-summary {
margin-bottom: 16px;
padding: 12px 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 20px;
}
.material-summary-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.material-summary-label {
color: #64748b;
flex-shrink: 0;
}
.material-summary-value {
color: #1e293b;
font-weight: 500;
}
.handover-body {
display: flex;
flex-direction: column;
gap: 20px;
}
.section-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px 16px 16px;
background: #fff;
}
.section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.handover-form-section :deep(.el-form-item) {
margin-bottom: 0;
}
</style>
@@ -1,105 +1,217 @@
<!-- 输注计划管理 — 查看弹窗 -->
<!-- 输注计划管理 — 查看弹窗(全链路关联信息) -->
<template>
<SystemViewDialog
v-model:visible="innerVisible"
:title="viewData.patientName || '输注计划详情'"
:title="heroTitle"
:icon="Document"
:code="viewData.applicationNo"
:code="heroCode"
code-label="申请单号"
:sections="sections"
:loading="loading"
:grid-columns="3"
width="1080px"
:grid-columns="4"
compact
width="1120px"
append-to-body
modal-class="infusion-schedule-view-dialog-mask"
>
<template #products>
<div class="list-body list-wrap view-inner-table-wrap">
<el-table
v-if="viewData.products && viewData.products.length"
:data="viewData.products"
v-if="products.length"
:data="products"
stripe
class="list-table inner-table"
max-height="320"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="协议价格(元)"
min-width="120"
align="right"
header-align="center"
>
<el-table-column type="index" label="序号" min-width="68" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="cellTypeName" label="细胞类型" min-width="120" align="center" />
<el-table-column prop="specText" label="规格" min-width="100" align="center" />
<el-table-column label="协议价格(元)" min-width="120" align="right">
<template #default="{ row }">
{{ row.agreementPrice != null ? `¥${formatMoneyDisplay(row.agreementPrice)}` : '—' }}
</template>
</el-table-column>
<el-table-column
label="实际价格(元)"
min-width="120"
align="right"
header-align="center"
>
<el-table-column label="实际价格(元)" min-width="120" align="right">
<template #default="{ row }">
{{ row.actualPrice != null ? `¥${formatMoneyDisplay(row.actualPrice)}` : '—' }}
</template>
</el-table-column>
<el-table-column
prop="quantity"
label="数量"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column prop="quantity" label="数量" min-width="80" align="center" />
</el-table>
<el-empty v-else description="暂无产品" :image-size="48" />
</div>
</template>
<template #production-info>
<SampleProductionInfoCards :production-info="detail?.productionInfo" />
</template>
<template #qc-info>
<SampleQcInfoCards :qc-info="detail?.qcInfo" hide-sample-context />
</template>
<template #outbound-material>
<div class="view-section-slot outbound-material-view">
<div v-if="!hasOutboundMaterial" class="outbound-material-empty">暂无出库物料准备记录</div>
<template v-else>
<div v-if="outboundMaterialCells.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">出库细胞</h4>
<el-table :data="outboundMaterialCells" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="productCode" label="产品编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="productTypeName" label="类型" min-width="90" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column prop="storageSlotName" label="储物格" min-width="130" show-overflow-tooltip />
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="outboundMaterialQcRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">使用的物料</h4>
<el-table :data="outboundMaterialQcRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="specModel" label="规格型号" min-width="110" />
<el-table-column label="计划用量" min-width="100" align="center">
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="72" align="center" />
</el-table>
</div>
<div v-if="outboundPaperRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">其他纸质材料</h4>
<el-table :data="outboundPaperRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="份数" min-width="80" align="center">
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<template #default="{ row }">
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
<div
v-for="(att, attIdx) in parsePaperAttachments(row.attachmentUrl)"
:key="attIdx"
class="paper-att-card"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<div class="paper-att-card__actions">
<el-icon
v-if="isImageFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
</div>
</div>
</div>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<p v-if="outboundMaterialRemark" class="outbound-material-remark">
备注:{{ outboundMaterialRemark }}
</p>
</template>
</div>
</template>
<template #review-attachments>
<div class="view-section-slot">
<ProtocolAttachmentReadonly
:files="reviewAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</template>
</SystemViewDialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
append-to-body
style="backdrop-filter: none"
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Document } from '@element-plus/icons-vue'
import { Document, Download, View } from '@element-plus/icons-vue'
import SystemViewDialog, {
type ViewSection,
type TagType
} from '@/components/business/system-view-dialog/index.vue'
import { fetchDictDataOptions, fetchInfusionScheduleMgmtDetail } from '@/api/admin'
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { fetchInfusionScheduleMgmtDetail } from '@/api/admin'
import {
applicationStatusLabel,
applicationStatusTagType,
channelServiceOrderTypeLabel,
channelServiceOrderTypeTagType,
storageReleaseConclusionLabel,
storageReleaseConclusionTagType,
scheduleStatusLabel,
scheduleStatusTagType,
infusionStatusLabel,
infusionStatusTagType,
qcStatusLabel,
qcStatusTagType,
infusionMethodLabel,
cellProductStatusLabel,
cellProductStatusTagType,
sexLabel
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
import { maskIdCard } from '@/utils/mask-sensitive'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'ScheduleViewDialog' })
const props = defineProps<{
modelValue: boolean
@@ -113,56 +225,247 @@
})
const loading = ref(false)
const viewData = ref<Record<string, any>>({})
const infusionMethodOptions = ref<Api.Dict.DictDataOption[]>([])
const detail = ref<Record<string, any> | null>(null)
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
const formatInfusionMethod = (code: unknown) => {
if (!code) return undefined
const hit = infusionMethodOptions.value.find((o) => o.value === code)
return hit?.label || String(code)
type PaperAttachmentView = {
fileName: string
attachmentUrl: string
fileUrl: string
fileType?: string
}
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const heroTitle = computed(() => {
const patient = detail.value?.patient || {}
return patient.patientName || detail.value?.patientName || '输注计划详情'
})
const heroCode = computed(() => {
const app = detail.value?.application || {}
return app.applicationNo || detail.value?.applicationNo || ''
})
const products = computed(() => detail.value?.products || [])
const outboundMaterial = computed(() => detail.value?.outboundMaterial || null)
const outboundMaterialCells = computed(() => outboundMaterial.value?.cells || [])
const outboundMaterialQcRows = computed(() => outboundMaterial.value?.qcMaterials || [])
const outboundMaterialRemark = computed(() => outboundMaterial.value?.remark || '')
const outboundPaperRows = computed(() => {
const materials = outboundMaterial.value?.paperMaterials || []
if (materials.length) {
return materials.map((p: Record<string, any>) => ({
paperName: p.paperName || '',
copies: p.copies,
attachmentUrl: p.attachmentUrl || ''
}))
}
const urls = outboundMaterial.value?.paperAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u: string) => u.trim())
.filter(Boolean)
.map((attachmentUrl: string) => ({
paperName: paperAttachmentName(attachmentUrl),
copies: 1,
attachmentUrl
}))
})
const hasOutboundMaterial = computed(
() =>
outboundMaterialCells.value.length > 0 ||
outboundMaterialQcRows.value.length > 0 ||
outboundPaperRows.value.length > 0 ||
!!outboundMaterialRemark.value.trim()
)
const reviewAttachmentFiles = computed(() => {
const urls = detail.value?.reviewInfo?.reviewAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u: string) => u.trim())
.filter(Boolean)
.map((fileUrl: string, i: number) => ({
id: `att_${i}`,
fileUrl,
fileName: fileUrl.split('/').pop() || ''
}))
})
function formatCourseNo(schedule: Record<string, any>) {
const sortNo = schedule?.sortNo ?? detail.value?.sortNo
const total = schedule?.totalCourseCount ?? detail.value?.totalCourseCount
if (sortNo != null && total != null) {
return `第 ${sortNo} 次 / 共 ${total} 次`
}
if (sortNo != null) return `第 ${sortNo} 次`
return '—'
}
function formatNeedCheckup(v: unknown) {
if (v === 1 || v === '1') return '需要'
if (v === 0 || v === '0') return '不需要'
return '—'
}
function formatCellType(plan: Record<string, any>, schedule: Record<string, any>) {
return schedule.cellType || plan.cellType || detail.value?.cellType || '—'
}
function hasInfusionConfirm(confirm: Record<string, any>) {
return !!(confirm?.id || confirm?.confirmTime || confirm?.confirmUserName)
}
const sections = computed((): ViewSection[] => {
const row = viewData.value
if (!row?.id) return []
const d = detail.value
if (!d?.id) return []
const app = d.application || {}
const patient = d.patient || {}
const plan = d.infusionPlan || {}
const schedule = d.infusionSchedule || {}
const review = d.reviewInfo || {}
const oh = d.outboundHandover || {}
const confirm = d.infusionConfirm || {}
const reviewConclusionValue = review.reviewConclusion || ''
const reviewConclusionDisplay = reviewConclusionValue
? storageReleaseConclusionLabel(reviewConclusionValue)
: '待审核'
const reviewConclusionTag = reviewConclusionValue
? storageReleaseConclusionTagType(reviewConclusionValue)
: 'info'
const orderType = app.orderType || d.orderType || ''
const confirmFields: ViewSection['fields'] = hasInfusionConfirm(confirm)
? [
{ label: '确认人', value: confirm.confirmUserName || '—' },
{ label: '确认时间', value: confirm.confirmTime || '—' },
{ label: '确认说明', value: confirm.confirmDesc || '—', span: true },
{ label: '备注', value: confirm.remark || '—', span: true }
]
: schedule.infusionStatus === 'yrqr' || d.infusionStatus === 'yrqr'
? [
{
label: '确认状态',
value: infusionStatusLabel('yrqr'),
tagType: infusionStatusTagType('yrqr') as TagType
},
{
label: '说明',
value: '已在输注计划管理中确认,暂无独立应用确认记录',
span: true
}
]
: [{ label: '确认状态', value: '未确认', tagType: 'info' }]
return [
{
title: '申请信息',
title: '中试申请 / 渠道 / 服务订单',
fields: [
{ label: '申请单号', value: row.applicationNo },
{ label: '渠道名称', value: row.channelName }
{ label: '申请单号', value: app.applicationNo || d.applicationNo || '—' },
{ label: '申请单名称', value: app.applicationName || d.applicationName || '—' },
{
label: '申请单状态',
value: applicationStatusLabel(app.applicationStatus || d.applicationStatus || ''),
tagType: applicationStatusTagType(app.applicationStatus || d.applicationStatus || '')
},
{ label: '渠道名称', value: app.channelName || d.channelName || '—' },
{
label: '服务订单',
value: app.channelServiceOrderName || d.channelServiceOrderName || '—'
},
{
label: '订单类型',
value: orderType ? channelServiceOrderTypeLabel(orderType) : '—',
tagType: orderType ? channelServiceOrderTypeTagType(orderType) : undefined
}
]
},
{
title: '患者信息',
fields: [
{ label: '患者姓名', value: row.patientName },
{ label: '性别', value: sexLabel(row.sex) || '—' },
{ label: '年龄', value: row.age != null ? String(row.age) : undefined }
{ label: '患者姓名', value: patient.patientName || d.patientName || '—' },
{ label: '性别', value: sexLabel(patient.sex || d.patientSex) || '—' },
{
label: '年龄',
value:
patient.age != null && patient.age !== ''
? String(patient.age)
: d.patientAge != null
? String(d.patientAge)
: '—'
},
{ label: '联系电话', value: patient.phoneNo || '—' },
{
label: '身份证号',
value: patient.idCardNo ? maskIdCard(patient.idCardNo) : '—'
},
{ label: '紧急联系人', value: patient.emergencyContact || '—' },
{ label: '紧急联系电话', value: patient.emergencyPhone || '—' },
{ label: '是否需要体检', value: formatNeedCheckup(patient.needCheckup) },
{ label: '临床诊断', value: patient.clinicalDiagnosis || '—', span: true }
]
},
{
title: '输注计划',
title: '输注方案',
fields: [
{ label: '规格', value: row.specText },
{ label: '输注时间', value: row.infusionTime },
{ label: '输注地点', value: row.infusionLocation },
{ label: '输注方式', value: formatInfusionMethod(row.infusionMethod) },
{ label: '细胞类型', value: formatCellType(plan, schedule) },
{ label: '规格', value: plan.specText || schedule.specText || d.specText || '—' },
{
label: '总疗程数',
value:
plan.totalCourseCount != null
? String(plan.totalCourseCount)
: schedule.totalCourseCount != null
? String(schedule.totalCourseCount)
: d.totalCourseCount != null
? String(d.totalCourseCount)
: '—'
},
{ label: '疗程间隔', value: plan.courseInterval || '—' },
{ label: '输注开始日期', value: plan.infusionStartDate || '—' }
]
},
{
title: '本次输注计划',
fields: [
{ label: '疗程进度', value: formatCourseNo(schedule) },
{ label: '细胞类型', value: formatCellType(plan, schedule) },
{ label: '规格', value: schedule.specText || d.specText || '—' },
{ label: '输注时间', value: schedule.infusionTime || d.infusionTime || '—' },
{ label: '输注地点', value: schedule.infusionLocation || d.infusionLocation || '—' },
{
label: '输注方式',
value: infusionMethodLabel(schedule.infusionMethod || d.infusionMethod || '')
},
{
label: '生产状态',
value: scheduleStatusLabel(row.scheduleStatus),
tagType: scheduleStatusTagType(row.scheduleStatus) as TagType
value: scheduleStatusLabel(schedule.scheduleStatus || d.scheduleStatus || ''),
tagType: scheduleStatusTagType(schedule.scheduleStatus || d.scheduleStatus || '') as TagType
},
{
label: '输注状态',
value: infusionStatusLabel(row.infusionStatus),
tagType: infusionStatusTagType(row.infusionStatus) as TagType
value: infusionStatusLabel(schedule.infusionStatus || d.infusionStatus || ''),
tagType: infusionStatusTagType(schedule.infusionStatus || d.infusionStatus || '') as TagType
},
{
label: '质检状态',
value: qcStatusLabel(row.qcStatus),
tagType: qcStatusTagType(row.qcStatus) as TagType
}
value: qcStatusLabel(schedule.qcStatus || d.qcStatus || ''),
tagType: qcStatusTagType(schedule.qcStatus || d.qcStatus || '') as TagType
},
{
label: '取消原因',
value: schedule.cancelReason || d.cancelReason || '—',
span: true
},
{ label: '备注', value: schedule.remark || d.remark || '—', span: true }
]
},
{
@@ -171,33 +474,138 @@
slot: 'products'
},
{
title: '其他信息',
fields: [{ label: '备注', value: row.remark || undefined, span: true }]
title: '出库生产工单 + 任务',
slot: 'production-info',
fields: []
},
{
title: '出库质检工单 + 任务',
slot: 'qc-info',
fields: []
},
{
title: '出库物料准备',
slot: 'outbound-material',
fields: []
},
{
title: '出库放行审核',
slot: 'review-attachments',
fields: [
{
label: '审核结论',
value: reviewConclusionDisplay,
tagType: reviewConclusionTag
},
{ label: '审核人', value: review.reviewByName || '—' },
{ label: '审核时间', value: review.reviewTime || '—' },
{ label: '审核备注', value: review.reviewRemark || '—', span: true }
]
},
{
title: '出库交接',
fields: [
{ label: '交接人', value: oh.handoverPerson || '—' },
{ label: '交接时间', value: oh.handoverTime || '—' },
{ label: '交接地点', value: oh.handoverLocation || '—' },
{ label: '接收人', value: oh.receiverPerson || '—' },
{ label: '备注', value: oh.remark || '—', span: true }
]
},
{
title: '应用确认',
fields: confirmFields
}
]
})
const loadInfusionMethodOptions = async () => {
try {
infusionMethodOptions.value = (await fetchDictDataOptions('szfs')) || []
} catch {
infusionMethodOptions.value = []
}
function paperAttachmentName(url: string) {
return url.split('/').pop() || '附件'
}
function splitUrls(str: string): string[] {
if (!str?.trim()) return []
return str
.split(',')
.map((u) => u.trim())
.filter(Boolean)
}
function parsePaperAttachments(urlStr: string): PaperAttachmentView[] {
return splitUrls(urlStr).map((url) => ({
fileName: paperAttachmentName(url),
attachmentUrl: url,
fileUrl: url
}))
}
function isImageFile(file: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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 handlePreviewPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
previewUrl.value = attachmentFullUrl(att.attachmentUrl)
previewType.value = isVideoFile(att) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
downloadAttachmentFile(
{
fileName: att.fileName,
fileUrl: att.attachmentUrl,
fileType: att.fileType || ''
},
{ fullFileUrl: attachmentFullUrl }
)
}
function attachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
const loadDetail = async () => {
const id = props.scheduleRow?.id
if (!id) return
viewData.value = { ...(props.scheduleRow || {}) }
detail.value = { ...(props.scheduleRow || {}) }
loading.value = true
try {
await loadInfusionMethodOptions()
const res: any = await fetchInfusionScheduleMgmtDetail({ scheduleId: id })
const detail =
const data =
res && typeof res === 'object' && !Array.isArray(res)
? (res as Record<string, any>)
: ((res as any)?.data ?? {})
viewData.value = { ...viewData.value, ...detail }
detail.value = { ...detail.value, ...data }
} catch {
// keep initial data from row
} finally {
@@ -211,7 +619,7 @@
if (val && props.scheduleRow?.id) {
loadDetail()
} else if (!val) {
viewData.value = {}
detail.value = null
}
}
)
@@ -250,4 +658,118 @@
padding: 24px 0;
}
}
.view-section-slot :deep(.sample-card__section-label) {
display: none;
}
.view-section-slot :deep(.sample-card__section--qc),
.view-section-slot :deep(.sample-card__section--production) {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.outbound-material-view {
width: 100%;
}
.outbound-material-empty {
font-size: 13px;
color: #94a3b8;
}
.outbound-material-block {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.outbound-material-subtitle {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.outbound-material-remark {
margin: 12px 0 0;
font-size: 13px;
color: #64748b;
}
.paper-att-cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.paper-att-card {
width: 100px;
height: 72px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 4px;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
margin-bottom: 2px;
}
.paper-att-card__name {
width: 100%;
font-size: 11px;
color: #334155;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.paper-att-card__actions {
position: absolute;
top: 2px;
right: 2px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.15s;
}
.paper-att-card:hover .paper-att-card__actions {
opacity: 1;
}
.paper-att-card__action {
width: 18px;
height: 18px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.35);
cursor: pointer;
&:hover {
background: #10b981;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -203,7 +203,7 @@
</el-table-column>
<el-table-column
label="操作"
min-width="420"
min-width="520"
fixed="right"
align="center"
header-align="center"
@@ -229,6 +229,9 @@
<el-button v-if="canEditSchedule(row)" class="btn-op-item" @click="handleCancel(row)">
<el-icon><CircleClose /></el-icon>取消
</el-button>
<el-button v-if="canShowHandover(row)" class="btn-op-item" @click="handleHandover(row)">
<el-icon><Switch /></el-icon>出库交接
</el-button>
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
@@ -264,6 +267,14 @@
:patient-name="cancelPatientName"
@success="loadData"
/>
<!-- 出库交接 -->
<InfusionScheduleHandoverDialog
v-model="handoverDialogVisible"
:schedule-id="handoverScheduleId"
:schedule-row="handoverRow"
@saved="loadData"
/>
</div>
</template>
@@ -277,7 +288,8 @@
EditPen,
CircleCheck,
CircleClose,
SuccessFilled
SuccessFilled,
Switch
} from '@element-plus/icons-vue'
import { fetchInfusionScheduleMgmtList, updateInfusionScheduleStatus } from '@/api/admin'
import {
@@ -293,6 +305,7 @@
import ScheduleEditDialog from './ScheduleEditDialog.vue'
import ScheduleViewDialog from './ScheduleViewDialog.vue'
import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
import InfusionScheduleHandoverDialog from './InfusionScheduleHandoverDialog.vue'
defineOptions({ name: 'InfusionScheduleManagement' })
@@ -367,6 +380,9 @@
const cancelDialogVisible = ref(false)
const cancelScheduleId = ref('')
const cancelPatientName = ref('')
const handoverDialogVisible = ref(false)
const handoverScheduleId = ref('')
const handoverRow = ref<any>(null)
// ── 数据加载 ──────────────────────────
const loadData = async () => {
@@ -434,6 +450,14 @@
/** 待输注、已用户确认均可完成(待输注时将自动先确认再完成) */
const canCompleteSchedule = (row: any) => canEditSchedule(row)
const canShowHandover = (row: any) => getInfusionStatus(row) !== 'yqx'
const handleHandover = (row: any) => {
handoverScheduleId.value = row.id
handoverRow.value = { ...row }
handoverDialogVisible.value = true
}
const buildCompleteConfirmMessage = (row: any) => {
const orderTypeLabel = channelServiceOrderTypeLabel(row.orderType)
const patient = row.patientName ? `患者「${row.patientName}」` : '该患者'
@@ -0,0 +1,595 @@
<!-- 出库放行审核 — 审核弹窗(支持单条和批量) -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
:model-value="modelValue"
:title="dialogTitle"
width="640px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
style="backdrop-filter: none"
@update:model-value="emit('update:modelValue', $event)"
@closed="resetForm"
>
<div v-if="isSingleMode && singleDetail" class="review-info-summary">
<div class="review-summary-row">
<span class="review-summary-label">申请单号</span>
<span class="review-summary-value">{{ singleDetail.application?.applicationNo || '—' }}</span>
</div>
<div class="review-summary-row">
<span class="review-summary-label">患者姓名</span>
<span class="review-summary-value">{{ singleDetail.patient?.patientName || '—' }}</span>
</div>
<div class="review-summary-row">
<span class="review-summary-label">细胞类型</span>
<span class="review-summary-value">{{ singleDetail.infusionPlan?.cellType || '—' }}</span>
</div>
</div>
<p v-else-if="scheduleIds.length > 1" class="audit-target-hint">
将对已选
<strong>{{ scheduleIds.length }}</strong> 条记录提交相同审核结果。
</p>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="biz-dialog-form"
@submit.prevent
>
<el-form-item label="审核结论" prop="reviewConclusion">
<el-radio-group v-model="form.reviewConclusion">
<el-radio value="tg">通过</el-radio>
<el-radio value="btg">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.reviewRemark"
type="textarea"
:rows="3"
placeholder="可选"
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item label="附件" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="attachmentUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="handleAttachmentChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, fIdx) in attachmentFiles"
:key="file.id || fIdx"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
"
>
<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.stop="handleDownloadAttachment(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click.stop="removeAttachment(fIdx)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel 等文件</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
append-to-body
style="backdrop-filter: none"
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import {
Check,
Close,
Plus,
Document,
Delete,
Download,
Loading,
VideoPlay
} from '@element-plus/icons-vue'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
fetchOutboundReleaseReviewDetail,
submitOutboundReleaseReview,
batchReviewOutboundRelease,
type OutboundReleaseReviewDetail
} from '@/api/outbound-release-review'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
type AttachmentItem = {
id: string
fileName: string
fileUrl: string
fileType: string
_uploading?: boolean
}
defineOptions({ name: 'OutboundReleaseReviewDialog' })
const props = defineProps<{
modelValue: boolean
scheduleIds: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'submitted'): void
}>()
const formRef = ref<FormInstance>()
const submitting = ref(false)
const singleDetail = ref<OutboundReleaseReviewDetail | null>(null)
const attachmentFiles = ref<AttachmentItem[]>([])
const isSingleMode = computed(() => props.scheduleIds.length === 1)
const form = reactive({
reviewConclusion: 'tg',
reviewRemark: ''
})
const rules: FormRules = {
reviewConclusion: [{ required: true, message: '请选择审核结论', trigger: 'change' }]
}
const dialogTitle = computed(() =>
props.scheduleIds.length > 1
? `批量审核(${props.scheduleIds.length} 条)`
: '出库放行审核'
)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function fullFileUrl(url: string) {
if (!url) return ''
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) {
return url
}
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
}
function isImageFile(file: AttachmentItem) {
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: AttachmentItem) {
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 removeAttachment(index: number) {
attachmentFiles.value.splice(index, 1)
}
function handlePreviewAttachment(file: AttachmentItem) {
if (file._uploading || !file.fileUrl) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewType.value = isVideoFile(file) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadAttachment(file: AttachmentItem) {
if (file._uploading || !file.fileUrl) return
downloadAttachmentFile(file, { fullFileUrl })
}
async function handleAttachmentChange(uploadFile: { raw?: File }) {
const file = uploadFile.raw
if (!file) return
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
attachmentFiles.value.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileType: file.type || '',
_uploading: true
})
const idx = attachmentFiles.value.length - 1
try {
const fd = new FormData()
fd.append('folder', 'outbound-release-review')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
attachmentFiles.value[idx] = {
...attachmentFiles.value[idx],
id: res.id || tempId,
fileName: file.name || res.fileName,
fileUrl: res.fileUrl,
fileType: res.fileType || file.type || '',
_uploading: false
}
} catch {
ElMessage.error('文件上传失败')
attachmentFiles.value.splice(idx, 1)
} finally {
nextTick(() => attachmentUploadRef.value?.clearFiles())
}
}
watch(
() => [props.modelValue, props.scheduleIds] as const,
async ([vis, ids]) => {
if (!vis || !ids.length) {
singleDetail.value = null
return
}
if (ids.length === 1) {
try {
singleDetail.value = await fetchOutboundReleaseReviewDetail(ids[0])
} catch {
singleDetail.value = null
}
}
},
{ immediate: true }
)
function resetForm() {
form.reviewConclusion = 'tg'
form.reviewRemark = ''
attachmentFiles.value = []
singleDetail.value = null
previewVisible.value = false
formRef.value?.clearValidate()
}
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid || !props.scheduleIds.length) return
if (attachmentFiles.value.some((f) => f._uploading)) {
ElMessage.warning('附件上传中,请稍候')
return
}
const attachmentUrls = attachmentFiles.value
.filter((f) => f.fileUrl && !f._uploading)
.map((f) => f.fileUrl)
.join(',')
const payload = {
reviewConclusion: form.reviewConclusion,
reviewRemark: form.reviewRemark.trim() || undefined,
reviewAttachments: attachmentUrls || undefined
}
submitting.value = true
try {
if (props.scheduleIds.length === 1) {
await submitOutboundReleaseReview(props.scheduleIds[0], payload)
ElMessage.success('审核成功')
} else {
await withFullscreenLoading(async () => {
await batchReviewOutboundRelease({
scheduleIds: props.scheduleIds,
...payload
})
ElMessage.success(`批量审核成功,共 ${props.scheduleIds.length} 条`)
}, fullscreenLoadingText.save)
}
emit('update:modelValue', false)
emit('submitted')
} finally {
submitting.value = false
}
}
</script>
<style scoped lang="scss">
.audit-target-hint {
margin: 0 0 16px;
font-size: 13px;
line-height: 1.5;
color: var(--mint-text-muted, #64748b);
strong {
color: #1e293b;
font-weight: 600;
}
}
.review-info-summary {
margin-bottom: 16px;
padding: 12px 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 20px;
}
.review-summary-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.review-summary-label {
color: #64748b;
flex-shrink: 0;
}
.review-summary-value {
color: #1e293b;
font-weight: 500;
}
.form-col-span-2 {
width: 100%;
}
/* 附件上传卡片化布局 — 对齐 OpenSpec ui-conventions / storage-cert */
.attachment-upload {
width: 100%;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s;
&:hover {
border-color: #10b981;
border-style: solid;
background: #ecfdf5;
}
}
.upload-card-text {
font-size: 12px;
color: #10b981;
font-weight: 500;
line-height: 1;
}
.upload-hint {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1;
margin-top: 8px;
}
.attachment-item {
width: 140px;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #10b981;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
}
&.is-uploading {
opacity: 0.6;
pointer-events: none;
}
}
.attachment-preview {
width: 100%;
height: 96px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.attachment-preview__icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
&--video {
background: #ede9fe;
color: #7c3aed;
}
&--doc {
background: #e0f2fe;
color: #0284c7;
}
}
.attachment-footer {
display: flex;
flex-direction: column;
flex: 1;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
color: #334155;
line-height: 1.4;
word-break: break-all;
min-height: calc(11px * 1.4 * 2);
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 4px;
flex-shrink: 0;
}
.action-btn {
width: 22px;
height: 22px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #64748b;
cursor: pointer;
background: #f1f5f9;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #10b981;
color: #fff;
}
&--danger:hover {
background: #ef4444;
color: #fff;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -0,0 +1,588 @@
<!-- 出库放行审核 — 只读查看 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<SystemViewDialog
v-model:visible="innerVisible"
title="出库放行审核详情"
width="1120px"
:grid-columns="4"
compact
:icon="CircleCheck"
:meta="viewHeroMeta"
:sections="viewSections"
:loading="loading"
append-to-body
modal-class="outbound-release-view-dialog-mask"
>
<template #production-info>
<SampleProductionInfoCards :production-info="detail?.productionInfo" />
</template>
<template #qc-info>
<SampleQcInfoCards :qc-info="detail?.qcInfo" hide-sample-context />
</template>
<template #review-attachments>
<div class="view-section-slot">
<ProtocolAttachmentReadonly
:files="reviewAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</template>
<template #outbound-material>
<div class="view-section-slot outbound-material-view">
<div v-if="!hasOutboundMaterial" class="outbound-material-empty">暂无出库物料准备记录</div>
<template v-else>
<div v-if="outboundMaterialCells.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">出库细胞</h4>
<el-table :data="outboundMaterialCells" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="productCode" label="产品编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="productTypeName" label="类型" min-width="90" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column prop="storageSlotName" label="储物格" min-width="130" show-overflow-tooltip />
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="outboundMaterialQcRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">使用的物料</h4>
<el-table :data="outboundMaterialQcRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="specModel" label="规格型号" min-width="110" />
<el-table-column label="计划用量" min-width="100" align="center">
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="72" align="center" />
</el-table>
</div>
<div v-if="outboundPaperRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">其他纸质材料</h4>
<el-table :data="outboundPaperRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="份数" min-width="80" align="center">
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<template #default="{ row }">
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
<div
v-for="(att, attIdx) in parsePaperAttachments(row.attachmentUrl)"
:key="attIdx"
class="paper-att-card"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<div class="paper-att-card__actions">
<el-icon
v-if="isImageFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
</div>
</div>
</div>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
<p v-if="outboundMaterialRemark" class="outbound-material-remark">
备注:{{ outboundMaterialRemark }}
</p>
</template>
</div>
</template>
</SystemViewDialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
append-to-body
style="backdrop-filter: none"
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { CircleCheck, Document, Download, View } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
import {
fetchOutboundReleaseReviewDetail,
type OutboundReleaseReviewDetail
} from '@/api/outbound-release-review'
import {
applicationStatusLabel,
applicationStatusTagType,
storageReleaseConclusionLabel,
storageReleaseConclusionTagType,
scheduleStatusLabel,
scheduleStatusTagType,
qcStatusLabel,
qcStatusTagType,
infusionMethodLabel,
cellProductStatusLabel,
cellProductStatusTagType
} from '@/utils/status-tag-registry'
import { maskIdCard } from '@/utils/mask-sensitive'
import { downloadAttachmentFile } from '@/utils/download-attachment'
defineOptions({ name: 'OutboundReleaseViewDialog' })
const props = defineProps<{
visible: boolean
scheduleId: string
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const loading = ref(false)
const detail = ref<OutboundReleaseReviewDetail | null>(null)
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
type PaperAttachmentView = {
fileName: string
attachmentUrl: string
fileUrl: string
fileType?: string
}
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
watch(
() => [props.visible, props.scheduleId] as const,
async ([vis, id]) => {
if (!vis || !id) {
detail.value = null
return
}
loading.value = true
try {
detail.value = await fetchOutboundReleaseReviewDetail(id)
} catch {
detail.value = null
} finally {
loading.value = false
}
},
{ immediate: true }
)
const viewHeroMeta = computed(() => {
if (!detail.value) return ''
return resolvePilotDetail(detail.value).application?.applicationNo || ''
})
const reviewAttachmentFiles = computed(() => {
const urls = detail.value?.reviewInfo?.reviewAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((fileUrl, i) => ({
id: `att_${i}`,
fileUrl,
fileName: fileUrl.split('/').pop() || ''
}))
})
const outboundMaterial = computed(() => detail.value?.outboundMaterial || null)
const outboundMaterialCells = computed(() => outboundMaterial.value?.cells || [])
const outboundMaterialQcRows = computed(() => outboundMaterial.value?.qcMaterials || [])
const outboundMaterialRemark = computed(() => outboundMaterial.value?.remark || '')
const outboundPaperRows = computed(() => {
const materials = outboundMaterial.value?.paperMaterials || []
if (materials.length) {
return materials.map((p) => ({
paperName: p.paperName || '',
copies: p.copies,
attachmentUrl: p.attachmentUrl || ''
}))
}
const urls = outboundMaterial.value?.paperAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((attachmentUrl) => ({
paperName: paperAttachmentName(attachmentUrl),
copies: 1,
attachmentUrl
}))
})
const hasOutboundMaterial = computed(
() =>
outboundMaterialCells.value.length > 0 ||
outboundMaterialQcRows.value.length > 0 ||
outboundPaperRows.value.length > 0 ||
!!outboundMaterialRemark.value.trim()
)
function paperAttachmentName(url: string) {
return url.split('/').pop() || '附件'
}
function splitUrls(str: string): string[] {
if (!str?.trim()) return []
return str
.split(',')
.map((u) => u.trim())
.filter(Boolean)
}
function parsePaperAttachments(urlStr: string): PaperAttachmentView[] {
return splitUrls(urlStr).map((url) => ({
fileName: paperAttachmentName(url),
attachmentUrl: url,
fileUrl: url
}))
}
function isImageFile(file: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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: PaperAttachmentView) {
const url = (file.fileUrl || file.attachmentUrl || '').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 handlePreviewPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
previewUrl.value = attachmentFullUrl(att.attachmentUrl)
previewType.value = isVideoFile(att) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadPaperAttachment(att: PaperAttachmentView) {
if (!att.attachmentUrl) return
downloadAttachmentFile(
{
fileName: att.fileName,
fileUrl: att.attachmentUrl,
fileType: att.fileType || ''
},
{ fullFileUrl: attachmentFullUrl }
)
}
/** 兼容旧版嵌套 pilotContext;新版详情已与 OutboundReleaseReviewDetail 展平一致 */
function resolvePilotDetail(d: OutboundReleaseReviewDetail) {
const nested = (d as { pilotContext?: OutboundReleaseReviewDetail }).pilotContext
if (nested && typeof nested === 'object') {
return { ...nested, productionInfo: d.productionInfo, qcInfo: d.qcInfo, reviewInfo: d.reviewInfo }
}
return d
}
const viewSections = computed<ViewSection[]>(() => {
const d = detail.value
if (!d) return []
const ctx = resolvePilotDetail(d)
const app = ctx.application || {}
const patient = ctx.patient || {}
const plan = ctx.infusionPlan || {}
const schedule = ctx.infusionSchedule || {}
const review = ctx.reviewInfo || {}
const reviewConclusionValue = review.reviewConclusion || ''
const reviewConclusionDisplay = reviewConclusionValue
? storageReleaseConclusionLabel(reviewConclusionValue)
: '待审核'
const reviewConclusionTag = reviewConclusionValue
? storageReleaseConclusionTagType(reviewConclusionValue)
: 'info'
return [
{
title: '中试服务申请单信息',
fields: [
{ label: '申请单号', value: app.applicationNo || '—' },
{ label: '申请单名称', value: app.applicationName || '—' },
{
label: '申请单状态',
value: applicationStatusLabel(app.applicationStatus || ''),
tagType: applicationStatusTagType(app.applicationStatus || '')
},
{ label: '渠道名称', value: app.channelName || '—' }
]
},
{
title: '患者信息',
fields: [
{ label: '患者姓名', value: patient.patientName || '—' },
{ label: '性别', value: patient.sex || '—' },
{ label: '年龄', value: patient.age || '—' },
{ label: '联系电话', value: patient.phoneNo || '—' },
{
label: '身份证号',
value: patient.idCardNo ? maskIdCard(patient.idCardNo) : '—'
},
{ label: '紧急联系人', value: patient.emergencyContact || '—' },
{ label: '紧急联系电话', value: patient.emergencyPhone || '—' },
{ label: '临床诊断', value: patient.clinicalDiagnosis || '—', span: true }
]
},
{
title: '输注方案信息',
fields: [
{
label: '总疗程数',
value: plan.totalCourseCount != null ? String(plan.totalCourseCount) : '—'
},
{ label: '疗程间隔', value: plan.courseInterval || '—' },
{ label: '输注开始日期', value: plan.infusionStartDate || '—' }
]
},
{
title: '本次输注计划',
fields: [
{ label: '输注时间', value: schedule.infusionTime || '—' },
{ label: '输注地点', value: schedule.infusionLocation || '—' },
{ label: '输注方式', value: infusionMethodLabel(schedule.infusionMethod || '') },
{
label: '计划状态',
value: scheduleStatusLabel(schedule.scheduleStatus || ''),
tagType: scheduleStatusTagType(schedule.scheduleStatus || '')
},
{
label: '质检状态',
value: qcStatusLabel(schedule.qcStatus || ''),
tagType: qcStatusTagType(schedule.qcStatus || '')
}
]
},
{
title: '生产信息',
slot: 'production-info',
fields: []
},
{
title: '质检信息',
slot: 'qc-info',
fields: []
},
{
title: '出库物料准备',
slot: 'outbound-material',
fields: []
},
{
title: '放行审核',
slot: 'review-attachments',
fields: [
{
label: '审核结论',
value: reviewConclusionDisplay,
tagType: reviewConclusionTag
},
{ label: '审核人', value: review.reviewByName || '—' },
{ label: '审核时间', value: review.reviewTime || '—' },
{ label: '审核备注', value: review.reviewRemark || '—', span: true }
]
}
]
})
function attachmentFullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
function handleAttachmentPreview(file: Record<string, unknown>) {
const url = attachmentFullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
}
</script>
<style scoped lang="scss">
.view-section-slot :deep(.sample-card__section-label) {
display: none;
}
.view-section-slot :deep(.sample-card__section--qc),
.view-section-slot :deep(.sample-card__section--production) {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.outbound-material-view {
width: 100%;
}
.outbound-material-empty {
font-size: 13px;
color: #94a3b8;
}
.outbound-material-block {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.outbound-material-subtitle {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.outbound-material-remark {
margin: 12px 0 0;
font-size: 13px;
color: #64748b;
}
.paper-att-cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.paper-att-card {
width: 100px;
height: 72px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 4px;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
margin-bottom: 2px;
}
.paper-att-card__name {
width: 100%;
font-size: 11px;
color: #334155;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.paper-att-card__actions {
position: absolute;
top: 2px;
right: 2px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.15s;
}
.paper-att-card:hover .paper-att-card__actions {
opacity: 1;
}
.paper-att-card__action {
width: 18px;
height: 18px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.35);
cursor: pointer;
&:hover {
background: #10b981;
}
}
.preview-container {
text-align: center;
}
</style>
@@ -0,0 +1,376 @@
<!-- 出库放行审核 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>出库放行审核</h1>
<p class="page-desc">审核出库放行记录,管理中试服务出库流程。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2>审核列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="申请单号/患者姓名"
clearable
/>
</el-form-item>
<el-form-item label="审核结论">
<el-select
v-model="searchForm.reviewConclusion"
clearable
placeholder="全部"
style="width: 100%"
>
<el-option label="待审核" value="" />
<el-option
v-for="item in reviewConclusionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按申请单号、患者姓名搜索。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button
type="primary"
class="btn-main"
:disabled="!canBatchReview"
@click="openReviewDialog()"
>
<el-icon><CircleCheck /></el-icon>
批量审核
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table outbound-release-review-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="50" align="center" header-align="center" />
<el-table-column
prop="applicationNo"
label="申请单号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="patientName"
label="患者姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="bizType"
label="类型"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.bizType"
:type="bizTypeTagType(row.bizType) as any"
size="small"
>
{{ row.bizTypeLabel || bizTypeLabel(row.bizType) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="infusionTime"
label="输注时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
prop="scheduleStatus"
label="计划状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.scheduleStatus"
:type="scheduleStatusTagType(row.scheduleStatus)"
size="small"
>
{{ scheduleStatusLabel(row.scheduleStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="qcStatus"
label="质检状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag v-if="row.qcStatus" :type="qcStatusTagType(row.qcStatus)" size="small">
{{ qcStatusLabel(row.qcStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="reviewConclusion"
label="审核结论"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.reviewConclusion"
:type="storageReleaseConclusionTagType(row.reviewConclusion)"
size="small"
>
{{ storageReleaseConclusionLabel(row.reviewConclusion) }}
</el-tag>
<el-tag v-else type="info" size="small">待审核</el-tag>
</template>
</el-table-column>
<el-table-column
prop="reviewByName"
label="审核人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="reviewTime"
label="审核时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button class="btn-op-item" @click="openMaterialDialog(row)">
<el-icon><Box /></el-icon>出库物料
</el-button>
<el-button class="btn-op-item" @click="openReviewDialog(row)">
<el-icon><CircleCheck /></el-icon>审核
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<OutboundReleaseViewDialog v-model:visible="viewVisible" :schedule-id="viewScheduleId" />
<OutboundReleaseReviewDialog
v-model="reviewDialogVisible"
:schedule-ids="reviewDialogIds"
@submitted="onReviewSubmitted"
/>
<OutboundReleaseMaterialDialog
v-model="materialDialogVisible"
:schedule-id="materialScheduleId"
:schedule-summary="materialScheduleSummary"
@saved="loadData"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { View, Search, RefreshLeft, CircleCheck, Box } from '@element-plus/icons-vue'
import OutboundReleaseViewDialog from './OutboundReleaseViewDialog.vue'
import OutboundReleaseReviewDialog from './OutboundReleaseReviewDialog.vue'
import OutboundReleaseMaterialDialog from './OutboundReleaseMaterialDialog.vue'
import {
fetchOutboundReleaseReviewList,
type OutboundReleaseReview
} from '@/api/outbound-release-review'
import {
storageReleaseConclusionLabel,
storageReleaseConclusionTagType,
scheduleStatusLabel,
scheduleStatusTagType,
qcStatusLabel,
qcStatusTagType,
bizTypeLabel,
bizTypeTagType
} from '@/utils/status-tag-registry'
defineOptions({ name: 'PilotServiceOutboundReleaseReview' })
const reviewConclusionOptions = [
{ value: 'tg', label: '通过' },
{ value: 'btg', label: '不通过' }
]
const searchForm = reactive({
reviewConclusion: '',
keyword: ''
})
const tableData = ref<OutboundReleaseReview[]>([])
const selectedRows = ref<OutboundReleaseReview[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const handleSelectionChange = (rows: OutboundReleaseReview[]) => {
selectedRows.value = rows
}
const canBatchReview = computed(() => selectedRows.value.length > 0)
async function loadData() {
loading.value = true
try {
const res = await fetchOutboundReleaseReviewList({
page: pagination.page,
pageSize: pagination.pageSize,
reviewConclusion: searchForm.reviewConclusion || undefined,
keyword: searchForm.keyword || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.reviewConclusion = ''
searchForm.keyword = ''
pagination.page = 1
loadData()
}
const viewVisible = ref(false)
const viewScheduleId = ref('')
function handleView(row: OutboundReleaseReview) {
viewScheduleId.value = String(row.scheduleId || row.id)
viewVisible.value = true
}
const reviewDialogVisible = ref(false)
const reviewDialogIds = ref<string[]>([])
function openReviewDialog(row?: OutboundReleaseReview) {
const rows = row ? [row] : selectedRows.value
if (!rows.length) {
ElMessage.warning('请先选择要审核的记录')
return
}
reviewDialogIds.value = rows.map((r) => String(r.scheduleId || r.id))
reviewDialogVisible.value = true
}
function onReviewSubmitted() {
selectedRows.value = []
loadData()
}
const materialDialogVisible = ref(false)
const materialScheduleId = ref('')
const materialScheduleSummary = ref<{
applicationNo?: string
patientName?: string
infusionTime?: string
}>()
function openMaterialDialog(row: OutboundReleaseReview) {
materialScheduleId.value = String(row.scheduleId || row.id)
materialScheduleSummary.value = {
applicationNo: row.applicationNo,
patientName: row.patientName,
infusionTime: row.infusionTime
}
materialDialogVisible.value = true
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.outbound-release-review-table {
:deep(td.el-table__cell:has(.btn-op-item)),
:deep(td.el-table-fixed-column--right:has(.btn-op-item)),
:deep(th.el-table-fixed-column--right) {
min-width: 260px !important;
}
}
</style>
@@ -568,6 +568,12 @@
</span>
</div>
</div>
<InfusionScheduleOutboundChainView
v-if="schedule.id"
:schedule-id="schedule.id"
:active="!!schedule._expanded"
/>
</div>
</div>
</div>
@@ -766,6 +772,7 @@
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import ServiceOrderTxnTable from '@/views/pilot-service/channel-service-order/ServiceOrderTxnTable.vue'
import InfusionScheduleOutboundChainView from '@/components/business/infusion-schedule-outbound-chain-view/index.vue'
import {
fetchPilotApplicationDetail,
fetchPilotPatient,
@@ -187,7 +187,7 @@
</template>
</el-table-column>
<el-table-column
v-if="activeTypeTab !== 'inbound'"
v-if="activeTypeTab !== 'inbound' && activeTypeTab !== 'outbound'"
prop="qcBizTypeLabel"
label="业务类型"
min-width="100"