From 198f0b9d455a851b19dca300a99fd60ac62130e7 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 06:15:15 +0800 Subject: [PATCH] feat: add dashboard health actions --- src/composables/useAdminFiles.ts | 47 +++++++++++- src/composables/useDashboardStats.ts | 50 +++++++++++- src/i18n/locales/en-US.ts | 27 ++++++- src/i18n/locales/zh-CN.ts | 27 ++++++- src/types/dashboard.ts | 107 +++++++++++++++++++------- src/types/file.ts | 2 + src/views/manage/DashboardView.vue | 111 ++++++++++++++++++++++++++- src/views/manage/FileManageView.vue | 62 ++++++++++++++- 8 files changed, 394 insertions(+), 39 deletions(-) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index 6e544df..fbcb658 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -46,6 +46,8 @@ const emptySummary = (): AdminFileSummary => ({ expiringSoonCount: 0, storageIssueCount: 0, neverRetrievedCount: 0, + healthyCount: 0, + permanentCount: 0, storageUsed: 0, usedCount: 0 }) @@ -219,7 +221,12 @@ export function useAdminFiles() { { value: 'healthy', label: t('fileManage.healthFilters.healthy'), - count: Math.max(summary.value.totalFiles - summary.value.healthAttentionCount, 0) + count: summary.value.healthyCount + }, + { + value: 'permanent', + label: t('fileManage.healthFilters.permanent'), + count: summary.value.permanentCount } ]) @@ -305,6 +312,8 @@ export function useAdminFiles() { neverRetrievedCount: files.filter((file) => file.statusInsightReasons.includes('never_retrieved') ).length, + healthyCount: files.filter((file) => file.statusInsightSeverity === 'success').length, + permanentCount: files.filter((file) => file.statusInsightState === 'permanent').length, storageUsed: files.reduce((totalSize, file) => totalSize + normalizeCount(file.size), 0), usedCount: files.reduce( (totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count), @@ -312,6 +321,40 @@ export function useAdminFiles() { ) }) + const normalizeSummary = ( + rawSummary: Partial | undefined, + files: AdminFileViewItem[], + total: number + ): AdminFileSummary => { + const fallback = buildFallbackSummary(files, total) + if (!rawSummary) return fallback + + return { + totalFiles: normalizeCount(rawSummary.totalFiles ?? fallback.totalFiles), + activeCount: normalizeCount(rawSummary.activeCount ?? fallback.activeCount), + expiredCount: normalizeCount(rawSummary.expiredCount ?? fallback.expiredCount), + textCount: normalizeCount(rawSummary.textCount ?? fallback.textCount), + fileCount: normalizeCount(rawSummary.fileCount ?? fallback.fileCount), + chunkedCount: normalizeCount(rawSummary.chunkedCount ?? fallback.chunkedCount), + healthAttentionCount: normalizeCount( + rawSummary.healthAttentionCount ?? fallback.healthAttentionCount + ), + healthDangerCount: normalizeCount(rawSummary.healthDangerCount ?? fallback.healthDangerCount), + healthWarningCount: normalizeCount( + rawSummary.healthWarningCount ?? fallback.healthWarningCount + ), + expiringSoonCount: normalizeCount(rawSummary.expiringSoonCount ?? fallback.expiringSoonCount), + storageIssueCount: normalizeCount(rawSummary.storageIssueCount ?? fallback.storageIssueCount), + neverRetrievedCount: normalizeCount( + rawSummary.neverRetrievedCount ?? fallback.neverRetrievedCount + ), + healthyCount: normalizeCount(rawSummary.healthyCount ?? fallback.healthyCount), + permanentCount: normalizeCount(rawSummary.permanentCount ?? fallback.permanentCount), + storageUsed: normalizeCount(rawSummary.storageUsed ?? fallback.storageUsed), + usedCount: normalizeCount(rawSummary.usedCount ?? fallback.usedCount) + } + } + const normalizeInsightSeverity = ( severity: AdminFileInsightSeverity | undefined ): AdminFileInsightSeverity => { @@ -684,7 +727,7 @@ export function useAdminFiles() { tableData.value = res.detail.data.map(createFileViewItem) params.value.total = res.detail.total - summary.value = res.detail.summary || buildFallbackSummary(tableData.value, res.detail.total) + summary.value = normalizeSummary(res.detail.summary, tableData.value, res.detail.total) syncSelectedFilesWithCurrentPage() } catch (error) { hasLoadError.value = true diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index b28fa75..4aec9d6 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -1,6 +1,6 @@ import { computed, ref, reactive } from 'vue' import { StatsService } from '@/services' -import type { DashboardViewData } from '@/types' +import type { DashboardData, DashboardHealthSummary, DashboardViewData } from '@/types' import { formatFileSize, getErrorMessage } from '@/utils/common' type UseDashboardStatsOptions = { @@ -27,6 +27,14 @@ const emptyDashboardData = (): DashboardViewData => ({ openUpload: 0, enableChunk: 0, maxSaveSeconds: 0, + healthAttentionCount: 0, + healthDangerCount: 0, + healthWarningCount: 0, + expiringSoonCount: 0, + storageIssueCount: 0, + neverRetrievedCount: 0, + healthyCount: 0, + permanentCount: 0, topSuffixes: [], recentFiles: [], storageUsedText: '0 Bytes', @@ -37,6 +45,8 @@ const emptyDashboardData = (): DashboardViewData => ({ activeRatio: 0, textRatio: 0, fileRatio: 0, + healthyRatio: 0, + healthAttentionRatio: 0, todaySizeRatio: 0 }) @@ -62,6 +72,34 @@ const normalizeRecentFiles = (recentFiles: DashboardViewData['recentFiles']) => usedCount: toNumber(file.usedCount) })) +const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [ + 'healthAttentionCount', + 'healthDangerCount', + 'healthWarningCount', + 'expiringSoonCount', + 'storageIssueCount', + 'neverRetrievedCount', + 'healthyCount', + 'permanentCount' +] + +const normalizeHealthSummary = (detail: DashboardData): DashboardHealthSummary => ({ + healthAttentionCount: toNumber( + detail.healthSummary?.healthAttentionCount ?? detail.healthAttentionCount + ), + healthDangerCount: toNumber(detail.healthSummary?.healthDangerCount ?? detail.healthDangerCount), + healthWarningCount: toNumber( + detail.healthSummary?.healthWarningCount ?? detail.healthWarningCount + ), + expiringSoonCount: toNumber(detail.healthSummary?.expiringSoonCount ?? detail.expiringSoonCount), + storageIssueCount: toNumber(detail.healthSummary?.storageIssueCount ?? detail.storageIssueCount), + neverRetrievedCount: toNumber( + detail.healthSummary?.neverRetrievedCount ?? detail.neverRetrievedCount + ), + healthyCount: toNumber(detail.healthSummary?.healthyCount ?? detail.healthyCount), + permanentCount: toNumber(detail.healthSummary?.permanentCount ?? detail.permanentCount) +}) + export function useDashboardStats(options: UseDashboardStatsOptions = {}) { const dashboardData = reactive(emptyDashboardData()) const isLoading = ref(false) @@ -103,6 +141,10 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { dashboardData.openUpload = toNumber(detail.openUpload) dashboardData.enableChunk = toNumber(detail.enableChunk) dashboardData.maxSaveSeconds = toNumber(detail.maxSaveSeconds) + const healthSummary = normalizeHealthSummary(detail) + healthSummaryKeys.forEach((key) => { + dashboardData[key] = healthSummary[key] + }) dashboardData.topSuffixes = detail.topSuffixes || [] dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || []) @@ -120,6 +162,12 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { dashboardData.fileRatio = dashboardData.totalFiles ? clampRatio((dashboardData.fileCount / dashboardData.totalFiles) * 100) : 0 + dashboardData.healthyRatio = dashboardData.totalFiles + ? clampRatio((dashboardData.healthyCount / dashboardData.totalFiles) * 100) + : 0 + dashboardData.healthAttentionRatio = dashboardData.totalFiles + ? clampRatio((dashboardData.healthAttentionCount / dashboardData.totalFiles) * 100) + : 0 dashboardData.todaySizeRatio = dashboardData.uploadSizeLimit ? clampRatio((dashboardData.todaySize / dashboardData.uploadSizeLimit) * 100) : 0 diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 6c1402e..71cd77c 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -69,7 +69,7 @@ export default { lastUpdated: 'Last updated: {time}', loadFailed: 'Failed to load dashboard data', fileHealth: 'File Health', - fileHealthDesc: 'Real file records grouped by availability, expiry, and type.', + fileHealthDesc: 'Status insights for available, risky, and pending file queues.', activeFileRatio: 'Active Ratio', fileShareRatio: 'File Ratio', textShareRatio: 'Text Ratio', @@ -91,6 +91,28 @@ export default { recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', + healthActions: { + attention: { + title: 'Needs Attention', + description: 'Handle issue and warning files together' + }, + storageIssue: { + title: 'Storage Issues', + description: 'Review files missing download metadata' + }, + expiringSoon: { + title: 'Expiring Soon', + description: 'Extend shares that should stay available' + }, + neverRetrieved: { + title: 'Never Retrieved', + description: 'Find shares that have not been retrieved yet' + }, + permanent: { + title: 'Permanent', + description: 'View long-retention share records' + } + }, table: { file: 'File', size: 'Size', @@ -410,7 +432,8 @@ export default { expiringSoon: 'Expiring Soon', storageIssue: 'Storage Issues', neverRetrieved: 'Never Retrieved', - healthy: 'Healthy' + healthy: 'Healthy', + permanent: 'Permanent' }, sortBy: 'Sort', unlimited: 'Unlimited', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 043149d..65da2cc 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -69,7 +69,7 @@ export default { lastUpdated: '最近更新:{time}', loadFailed: '仪表盘数据加载失败', fileHealth: '文件健康', - fileHealthDesc: '基于真实文件记录统计有效、过期和类型分布。', + fileHealthDesc: '基于状态洞察统计可取件、风险与待处理队列。', activeFileRatio: '有效占比', fileShareRatio: '文件占比', textShareRatio: '文本占比', @@ -91,6 +91,28 @@ export default { recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', + healthActions: { + attention: { + title: '需关注', + description: '集中处理异常和预警文件' + }, + storageIssue: { + title: '存储异常', + description: '检查缺少下载信息的文件' + }, + expiringSoon: { + title: '即将过期', + description: '优先续期仍需保留的分享' + }, + neverRetrieved: { + title: '未取件', + description: '识别创建后尚未被取件的记录' + }, + permanent: { + title: '永久有效', + description: '查看长期保留的分享记录' + } + }, table: { file: '文件', size: '大小', @@ -408,7 +430,8 @@ export default { expiringSoon: '即将过期', storageIssue: '存储异常', neverRetrieved: '未取件', - healthy: '健康' + healthy: '健康', + permanent: '永久有效' }, sortBy: '排序', unlimited: '不限次数', diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index a200857..5043372 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -1,3 +1,16 @@ +import type { AdminFileHealthFilter } from './file' + +export interface DashboardHealthSummary { + healthAttentionCount: number + healthDangerCount: number + healthWarningCount: number + expiringSoonCount: number + storageIssueCount: number + neverRetrievedCount: number + healthyCount: number + permanentCount: number +} + export interface DashboardData { totalFiles: number storageUsed: number | string @@ -17,6 +30,15 @@ export interface DashboardData { openUpload?: number enableChunk?: number maxSaveSeconds?: number + healthAttentionCount?: number + healthDangerCount?: number + healthWarningCount?: number + expiringSoonCount?: number + storageIssueCount?: number + neverRetrievedCount?: number + healthyCount?: number + permanentCount?: number + healthSummary?: Partial topSuffixes?: DashboardSuffixStat[] recentFiles?: DashboardRecentFile[] } @@ -40,31 +62,62 @@ export interface DashboardRecentFile { isExpired: boolean } -export interface DashboardViewData extends DashboardData { - hasExtendedStats: boolean - activeCount: number - expiredCount: number - textCount: number - fileCount: number - chunkedCount: number - usedCount: number - storageBackend: string - uploadSizeLimit: number - openUpload: number - enableChunk: number - maxSaveSeconds: number - topSuffixes: DashboardSuffixStat[] - recentFiles: DashboardRecentFile[] - storageUsed: number - yesterdaySize: number - todaySize: number - storageUsedText: string - yesterdaySizeText: string - todaySizeText: string - uploadSizeLimitText: string - sysUptimeText: string - activeRatio: number - textRatio: number - fileRatio: number - todaySizeRatio: number +export type DashboardViewData = Omit< + DashboardData, + | keyof DashboardHealthSummary + | 'activeCount' + | 'expiredCount' + | 'textCount' + | 'fileCount' + | 'chunkedCount' + | 'usedCount' + | 'storageBackend' + | 'uploadSizeLimit' + | 'openUpload' + | 'enableChunk' + | 'maxSaveSeconds' + | 'topSuffixes' + | 'recentFiles' + | 'storageUsed' + | 'yesterdaySize' + | 'todaySize' +> & + DashboardHealthSummary & { + hasExtendedStats: boolean + activeCount: number + expiredCount: number + textCount: number + fileCount: number + chunkedCount: number + usedCount: number + storageBackend: string + uploadSizeLimit: number + openUpload: number + enableChunk: number + maxSaveSeconds: number + topSuffixes: DashboardSuffixStat[] + recentFiles: DashboardRecentFile[] + storageUsed: number + yesterdaySize: number + todaySize: number + storageUsedText: string + yesterdaySizeText: string + todaySizeText: string + uploadSizeLimitText: string + sysUptimeText: string + activeRatio: number + textRatio: number + fileRatio: number + healthyRatio: number + healthAttentionRatio: number + todaySizeRatio: number + } + +export interface DashboardHealthAction { + key: string + label: string + description: string + count: number + health: AdminFileHealthFilter + tone: 'danger' | 'warning' | 'success' | 'neutral' } diff --git a/src/types/file.ts b/src/types/file.ts index 73b406c..4b5d0f8 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -69,6 +69,8 @@ export interface AdminFileSummary { expiringSoonCount: number storageIssueCount: number neverRetrievedCount: number + healthyCount: number + permanentCount: number storageUsed: number usedCount: number } diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index b00bfde..8fabbd0 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -122,6 +122,31 @@ /> +
+ +
+

@@ -337,24 +362,32 @@