From 5c3eb2d1445fb4779b72a6a765c6fc5aaec8812a Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 07:47:33 +0800 Subject: [PATCH] feat: add admin operational insights --- src/composables/useDashboardStats.ts | 46 +++++++++++ src/i18n/locales/en-US.ts | 96 ++++++++++++++++++++++ src/i18n/locales/zh-CN.ts | 80 ++++++++++++++++++ src/types/dashboard.ts | 37 +++++++++ src/views/manage/DashboardView.vue | 118 ++++++++++++++++++++++++++- 5 files changed, 376 insertions(+), 1 deletion(-) diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index 8b42d87..59bcc48 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -7,6 +7,9 @@ import type { DashboardActivityViewItem, DashboardData, DashboardHealthSummary, + DashboardInsightSeverity, + DashboardOperationalInsight, + DashboardOperationalInsightViewItem, DashboardViewData } from '@/types' import { formatFileSize, getErrorMessage } from '@/utils/common' @@ -49,6 +52,7 @@ const emptyDashboardData = (): DashboardViewData => ({ topSuffixes: [], recentFiles: [], recentActivities: [], + operationalInsights: [], storageUsedText: '0 Bytes', yesterdaySizeText: '0 Bytes', todaySizeText: '0 Bytes', @@ -109,6 +113,45 @@ const normalizeActivityOptions = ( count: toNumber(option.count) })) +const insightSeverities: DashboardInsightSeverity[] = ['danger', 'warning', 'success', 'neutral'] + +const normalizeInsightSeverity = (severity: string | undefined): DashboardInsightSeverity => + insightSeverities.includes(severity as DashboardInsightSeverity) + ? (severity as DashboardInsightSeverity) + : 'neutral' + +const normalizeOperationalInsights = ( + insights: DashboardOperationalInsight[] = [] +): DashboardOperationalInsightViewItem[] => + insights + .filter((insight) => insight && insight.key) + .map((insight) => { + const action = insight.action && typeof insight.action === 'object' ? insight.action : {} + const actionTypeValue = + insight.actionType ?? + insight.action_type ?? + action.actionType ?? + action.action_type ?? + action.type ?? + 'file_queue' + const targetHealthValue = + insight.targetHealth ?? + insight.target_health ?? + action.targetHealth ?? + action.target_health ?? + action.health ?? + null + + return { + ...insight, + count: toNumber(insight.count), + priority: toNumber(insight.priority), + severity: normalizeInsightSeverity(insight.severity), + actionTypeValue, + targetHealthValue + } + }) + const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [ 'healthAttentionCount', 'healthDangerCount', @@ -210,6 +253,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { dashboardData.recentActivities = normalizeRecentActivities( detail.recentActivities ?? detail.recent_activities ?? [] ) + dashboardData.operationalInsights = normalizeOperationalInsights( + detail.operationalInsights ?? detail.operational_insights ?? detail.insights ?? [] + ) dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed) dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize) diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 76f62ea..9d11baa 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -90,6 +90,17 @@ export default { textType: 'Text', recentActivities: 'Recent Activity', recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + operationalInsightsTitle: 'Operational Insights', + operationalInsightsDesc: + 'Suggested next actions generated from file health, upload policy, and recent capacity.', + operationalInsightActionSettings: 'Adjust settings', + operationalInsightActionFileQueue: 'View queue', + operationalInsightSeverity: { + danger: 'High risk', + warning: 'Action needed', + success: 'Stable', + neutral: 'Suggestion' + }, viewAllActivities: 'View all', activityTimelineTitle: 'Activity Timeline', activityTimelineFiltered: 'Current Results', @@ -132,6 +143,43 @@ export default { local_file: 'Local file', system: 'System' }, + operationalInsights: { + storage_issue: { + title: 'Fix {count} storage issues', + description: + 'Some records are missing download metadata. Review storage settings and file details first.' + }, + expired_cleanup: { + title: 'Clean up {count} expired files', + description: + 'Expired records add management noise. Delete them in bulk or extend the files that should remain available.' + }, + expiring_soon: { + title: '{count} files expiring soon', + description: + 'These shares will become unavailable soon. Extend them early or notify the recipients.' + }, + never_retrieved: { + title: '{count} files never retrieved', + description: + 'Shares that were never retrieved can reveal ineffective uploads or follow-up gaps.' + }, + guest_upload_retention: { + title: 'Guest uploads lack retention limits', + description: + 'Guest upload is enabled with unlimited retention. Set a default retention window to reduce storage pressure.' + }, + chunking_disabled: { + title: 'Consider enabling chunked upload', + description: + "Today's added capacity is near the single-file limit. Chunked upload can improve large-file reliability." + }, + healthy: { + title: 'Operations look stable', + description: + 'No high-priority operational risk was detected. Keep watching the file health trend.' + } + }, recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', @@ -875,6 +923,17 @@ export default { textType: 'Text', recentActivities: 'Recent Activity', recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + operationalInsightsTitle: 'Operational Insights', + operationalInsightsDesc: + 'Suggested next actions generated from file health, upload policy, and recent capacity.', + operationalInsightActionSettings: 'Adjust settings', + operationalInsightActionFileQueue: 'View queue', + operationalInsightSeverity: { + danger: 'High risk', + warning: 'Action needed', + success: 'Stable', + neutral: 'Suggestion' + }, viewAllActivities: 'View all', activityTimelineTitle: 'Activity Timeline', activityTimelineFiltered: 'Current Results', @@ -917,6 +976,43 @@ export default { local_file: 'Local file', system: 'System' }, + operationalInsights: { + storage_issue: { + title: 'Fix {count} storage issues', + description: + 'Some records are missing download metadata. Review storage settings and file details first.' + }, + expired_cleanup: { + title: 'Clean up {count} expired files', + description: + 'Expired records add management noise. Delete them in bulk or extend the files that should remain available.' + }, + expiring_soon: { + title: '{count} files expiring soon', + description: + 'These shares will become unavailable soon. Extend them early or notify the recipients.' + }, + never_retrieved: { + title: '{count} files never retrieved', + description: + 'Shares that were never retrieved can reveal ineffective uploads or follow-up gaps.' + }, + guest_upload_retention: { + title: 'Guest uploads lack retention limits', + description: + 'Guest upload is enabled with unlimited retention. Set a default retention window to reduce storage pressure.' + }, + chunking_disabled: { + title: 'Consider enabling chunked upload', + description: + "Today's added capacity is near the single-file limit. Chunked upload can improve large-file reliability." + }, + healthy: { + title: 'Operations look stable', + description: + 'No high-priority operational risk was detected. Keep watching the file health trend.' + } + }, recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index cee7f8a..583d9c3 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -90,6 +90,16 @@ export default { textType: '文本', recentActivities: '最近操作', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + operationalInsightsTitle: '运营建议', + operationalInsightsDesc: '根据文件健康、上传策略和近期容量自动生成下一步动作。', + operationalInsightActionSettings: '调整设置', + operationalInsightActionFileQueue: '查看队列', + operationalInsightSeverity: { + danger: '高风险', + warning: '需处理', + success: '稳定', + neutral: '建议' + }, viewAllActivities: '查看全部', activityTimelineTitle: '操作时间线', activityTimelineFiltered: '当前结果', @@ -132,6 +142,36 @@ export default { local_file: '本地文件', system: '系统' }, + operationalInsights: { + storage_issue: { + title: '修复 {count} 个存储异常', + description: '部分记录缺少下载元数据,建议优先核对存储配置和文件详情。' + }, + expired_cleanup: { + title: '清理 {count} 个已过期文件', + description: '已过期记录会占用管理视野,建议批量删除或续期仍需保留的文件。' + }, + expiring_soon: { + title: '{count} 个文件即将过期', + description: '这些分享将在短时间内失效,适合提前续期或通知使用方。' + }, + never_retrieved: { + title: '{count} 个文件未被取件', + description: '创建后长期无人取件的分享可用于判断无效上传或触达效果。' + }, + guest_upload_retention: { + title: '游客上传缺少保留上限', + description: '游客上传已开启且保存时间不受限,建议设置默认保留期降低存储压力。' + }, + chunking_disabled: { + title: '建议开启分片上传', + description: '今日新增容量已接近单文件上限,开启分片上传可提升大文件体验。' + }, + healthy: { + title: '运行状态稳定', + description: '当前未发现优先级较高的运营风险,可继续观察文件健康趋势。' + } + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', @@ -837,6 +877,16 @@ export default { textType: '文本', recentActivities: '最近操作', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + operationalInsightsTitle: '运营建议', + operationalInsightsDesc: '根据文件健康、上传策略和近期容量自动生成下一步动作。', + operationalInsightActionSettings: '调整设置', + operationalInsightActionFileQueue: '查看队列', + operationalInsightSeverity: { + danger: '高风险', + warning: '需处理', + success: '稳定', + neutral: '建议' + }, viewAllActivities: '查看全部', activityTimelineTitle: '操作时间线', activityTimelineFiltered: '当前结果', @@ -879,6 +929,36 @@ export default { local_file: '本地文件', system: '系统' }, + operationalInsights: { + storage_issue: { + title: '修复 {count} 个存储异常', + description: '部分记录缺少下载元数据,建议优先核对存储配置和文件详情。' + }, + expired_cleanup: { + title: '清理 {count} 个已过期文件', + description: '已过期记录会占用管理视野,建议批量删除或续期仍需保留的文件。' + }, + expiring_soon: { + title: '{count} 个文件即将过期', + description: '这些分享将在短时间内失效,适合提前续期或通知使用方。' + }, + never_retrieved: { + title: '{count} 个文件未被取件', + description: '创建后长期无人取件的分享可用于判断无效上传或触达效果。' + }, + guest_upload_retention: { + title: '游客上传缺少保留上限', + description: '游客上传已开启且保存时间不受限,建议设置默认保留期降低存储压力。' + }, + chunking_disabled: { + title: '建议开启分片上传', + description: '今日新增容量已接近单文件上限,开启分片上传可提升大文件体验。' + }, + healthy: { + title: '运行状态稳定', + description: '当前未发现优先级较高的运营风险,可继续观察文件健康趋势。' + } + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index e472ab3..f7b0e18 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -43,6 +43,9 @@ export interface DashboardData { recentFiles?: DashboardRecentFile[] recentActivities?: DashboardActivity[] recent_activities?: DashboardActivity[] + operationalInsights?: DashboardOperationalInsight[] + operational_insights?: DashboardOperationalInsight[] + insights?: DashboardOperationalInsight[] } export interface DashboardSuffixStat { @@ -120,6 +123,36 @@ export interface DashboardActivitiesResponse { target_type_options?: DashboardActivityOption[] } +export type DashboardInsightSeverity = 'danger' | 'warning' | 'success' | 'neutral' + +export interface DashboardOperationalInsightAction { + type?: string + actionType?: string + action_type?: string + health?: AdminFileHealthFilter | string | null + targetHealth?: AdminFileHealthFilter | string | null + target_health?: AdminFileHealthFilter | string | null +} + +export interface DashboardOperationalInsight { + key: string + severity: DashboardInsightSeverity | string + priority?: number + count: number + action?: DashboardOperationalInsightAction + actionType?: string + action_type?: string + targetHealth?: AdminFileHealthFilter | string | null + target_health?: AdminFileHealthFilter | string | null +} + +export interface DashboardOperationalInsightViewItem extends DashboardOperationalInsight { + severity: DashboardInsightSeverity + priority: number + actionTypeValue: string + targetHealthValue: AdminFileHealthFilter | string | null +} + export type DashboardViewData = Omit< DashboardData, | keyof DashboardHealthSummary @@ -138,6 +171,9 @@ export type DashboardViewData = Omit< | 'recentFiles' | 'recentActivities' | 'recent_activities' + | 'operationalInsights' + | 'operational_insights' + | 'insights' | 'storageUsed' | 'yesterdaySize' | 'todaySize' @@ -158,6 +194,7 @@ export type DashboardViewData = Omit< topSuffixes: DashboardSuffixStat[] recentFiles: DashboardRecentFile[] recentActivities: DashboardActivityViewItem[] + operationalInsights: DashboardOperationalInsightViewItem[] storageUsed: number yesterdaySize: number todaySize: number diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index 12842d7..c60233d 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -84,6 +84,56 @@ +
+
+
+

+ {{ t('admin.dashboard.operationalInsightsTitle') }} +

+

+ {{ t('admin.dashboard.operationalInsightsDesc') }} +

+
+
+ +
+ +
+
+
@@ -669,6 +719,7 @@ import { FileTextIcon, HardDriveIcon, HistoryIcon, + LightbulbIcon, ListFilterIcon, PencilIcon, RefreshCwIcon, @@ -684,7 +735,12 @@ import { useDashboardStats, useInjectedDarkMode } from '@/composables' import { useI18n } from 'vue-i18n' import { formatFileSize, formatTimestamp } from '@/utils/common' import { ROUTES } from '@/constants' -import type { DashboardActivityViewItem, DashboardHealthAction } from '@/types' +import type { + DashboardActivityViewItem, + DashboardHealthAction, + DashboardInsightSeverity, + DashboardOperationalInsightViewItem +} from '@/types' const isDarkMode = useInjectedDarkMode() const { t } = useI18n() @@ -796,6 +852,66 @@ const healthActions = computed(() => [ const getSuffixRatio = (count: number) => Math.round((count / maxSuffixCount.value) * 100) +const operationalInsightIconMap: Record = { + danger: AlertTriangleIcon, + warning: ClockIcon, + success: CheckCircleIcon, + neutral: ShieldCheckIcon +} + +const getOperationalInsightIcon = (severity: DashboardInsightSeverity) => + operationalInsightIconMap[severity] + +const getOperationalInsightClass = (severity: DashboardInsightSeverity) => { + const darkClasses: Record = { + danger: 'border-red-500/25 bg-red-500/10 text-red-100 hover:border-red-400/60', + warning: 'border-amber-500/25 bg-amber-500/10 text-amber-100 hover:border-amber-400/60', + success: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100 hover:border-emerald-400/60', + neutral: 'border-gray-700 bg-gray-800/70 text-gray-100 hover:border-indigo-500/50' + } + const lightClasses: Record = { + danger: 'border-red-100 bg-red-50 text-red-800 hover:border-red-300', + warning: 'border-amber-100 bg-amber-50 text-amber-800 hover:border-amber-300', + success: 'border-emerald-100 bg-emerald-50 text-emerald-800 hover:border-emerald-300', + neutral: 'border-gray-100 bg-white text-gray-800 shadow-sm hover:border-indigo-200' + } + + return isDarkMode.value ? darkClasses[severity] : lightClasses[severity] +} + +const getOperationalInsightSeverityLabel = (severity: DashboardInsightSeverity) => + t(`admin.dashboard.operationalInsightSeverity.${severity}`) + +const getOperationalInsightTitle = (insight: DashboardOperationalInsightViewItem) => { + const key = `admin.dashboard.operationalInsights.${insight.key}.title` + const title = t(key, { count: insight.count }) + return title === key ? insight.key : title +} + +const getOperationalInsightDescription = (insight: DashboardOperationalInsightViewItem) => { + const key = `admin.dashboard.operationalInsights.${insight.key}.description` + const description = t(key, { count: insight.count }) + return description === key ? insight.key : description +} + +const getOperationalInsightActionLabel = (insight: DashboardOperationalInsightViewItem) => + insight.actionTypeValue === 'settings' + ? t('admin.dashboard.operationalInsightActionSettings') + : t('admin.dashboard.operationalInsightActionFileQueue') + +const openOperationalInsight = (insight: DashboardOperationalInsightViewItem) => { + if (insight.actionTypeValue === 'settings') { + void router.push({ path: ROUTES.SETTINGS }) + return + } + + const health = insight.targetHealthValue + void router.push({ + path: ROUTES.FILE_MANAGE, + query: health ? { health } : {} + }) +} + const formatCreatedAt = (value: string | null) => { if (!value) return '-' return formatTimestamp(value, 'datetime')