feat: add admin operational insights
This commit is contained in:
@@ -7,6 +7,9 @@ import type {
|
|||||||
DashboardActivityViewItem,
|
DashboardActivityViewItem,
|
||||||
DashboardData,
|
DashboardData,
|
||||||
DashboardHealthSummary,
|
DashboardHealthSummary,
|
||||||
|
DashboardInsightSeverity,
|
||||||
|
DashboardOperationalInsight,
|
||||||
|
DashboardOperationalInsightViewItem,
|
||||||
DashboardViewData
|
DashboardViewData
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
import { formatFileSize, getErrorMessage } from '@/utils/common'
|
import { formatFileSize, getErrorMessage } from '@/utils/common'
|
||||||
@@ -49,6 +52,7 @@ const emptyDashboardData = (): DashboardViewData => ({
|
|||||||
topSuffixes: [],
|
topSuffixes: [],
|
||||||
recentFiles: [],
|
recentFiles: [],
|
||||||
recentActivities: [],
|
recentActivities: [],
|
||||||
|
operationalInsights: [],
|
||||||
storageUsedText: '0 Bytes',
|
storageUsedText: '0 Bytes',
|
||||||
yesterdaySizeText: '0 Bytes',
|
yesterdaySizeText: '0 Bytes',
|
||||||
todaySizeText: '0 Bytes',
|
todaySizeText: '0 Bytes',
|
||||||
@@ -109,6 +113,45 @@ const normalizeActivityOptions = (
|
|||||||
count: toNumber(option.count)
|
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)[] = [
|
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
|
||||||
'healthAttentionCount',
|
'healthAttentionCount',
|
||||||
'healthDangerCount',
|
'healthDangerCount',
|
||||||
@@ -210,6 +253,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
|
|||||||
dashboardData.recentActivities = normalizeRecentActivities(
|
dashboardData.recentActivities = normalizeRecentActivities(
|
||||||
detail.recentActivities ?? detail.recent_activities ?? []
|
detail.recentActivities ?? detail.recent_activities ?? []
|
||||||
)
|
)
|
||||||
|
dashboardData.operationalInsights = normalizeOperationalInsights(
|
||||||
|
detail.operationalInsights ?? detail.operational_insights ?? detail.insights ?? []
|
||||||
|
)
|
||||||
|
|
||||||
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
||||||
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ export default {
|
|||||||
textType: 'Text',
|
textType: 'Text',
|
||||||
recentActivities: 'Recent Activity',
|
recentActivities: 'Recent Activity',
|
||||||
recentActivitiesDesc: 'Key admin changes for quick operational traceability.',
|
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',
|
viewAllActivities: 'View all',
|
||||||
activityTimelineTitle: 'Activity Timeline',
|
activityTimelineTitle: 'Activity Timeline',
|
||||||
activityTimelineFiltered: 'Current Results',
|
activityTimelineFiltered: 'Current Results',
|
||||||
@@ -132,6 +143,43 @@ export default {
|
|||||||
local_file: 'Local file',
|
local_file: 'Local file',
|
||||||
system: 'System'
|
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',
|
recentFiles: 'Recent Shares',
|
||||||
recentFilesDesc: 'Recently created share records for quick status checks.',
|
recentFilesDesc: 'Recently created share records for quick status checks.',
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
@@ -875,6 +923,17 @@ export default {
|
|||||||
textType: 'Text',
|
textType: 'Text',
|
||||||
recentActivities: 'Recent Activity',
|
recentActivities: 'Recent Activity',
|
||||||
recentActivitiesDesc: 'Key admin changes for quick operational traceability.',
|
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',
|
viewAllActivities: 'View all',
|
||||||
activityTimelineTitle: 'Activity Timeline',
|
activityTimelineTitle: 'Activity Timeline',
|
||||||
activityTimelineFiltered: 'Current Results',
|
activityTimelineFiltered: 'Current Results',
|
||||||
@@ -917,6 +976,43 @@ export default {
|
|||||||
local_file: 'Local file',
|
local_file: 'Local file',
|
||||||
system: 'System'
|
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',
|
recentFiles: 'Recent Shares',
|
||||||
recentFilesDesc: 'Recently created share records for quick status checks.',
|
recentFilesDesc: 'Recently created share records for quick status checks.',
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ export default {
|
|||||||
textType: '文本',
|
textType: '文本',
|
||||||
recentActivities: '最近操作',
|
recentActivities: '最近操作',
|
||||||
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
||||||
|
operationalInsightsTitle: '运营建议',
|
||||||
|
operationalInsightsDesc: '根据文件健康、上传策略和近期容量自动生成下一步动作。',
|
||||||
|
operationalInsightActionSettings: '调整设置',
|
||||||
|
operationalInsightActionFileQueue: '查看队列',
|
||||||
|
operationalInsightSeverity: {
|
||||||
|
danger: '高风险',
|
||||||
|
warning: '需处理',
|
||||||
|
success: '稳定',
|
||||||
|
neutral: '建议'
|
||||||
|
},
|
||||||
viewAllActivities: '查看全部',
|
viewAllActivities: '查看全部',
|
||||||
activityTimelineTitle: '操作时间线',
|
activityTimelineTitle: '操作时间线',
|
||||||
activityTimelineFiltered: '当前结果',
|
activityTimelineFiltered: '当前结果',
|
||||||
@@ -132,6 +142,36 @@ export default {
|
|||||||
local_file: '本地文件',
|
local_file: '本地文件',
|
||||||
system: '系统'
|
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: '最近分享',
|
recentFiles: '最近分享',
|
||||||
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
||||||
available: '可取件',
|
available: '可取件',
|
||||||
@@ -837,6 +877,16 @@ export default {
|
|||||||
textType: '文本',
|
textType: '文本',
|
||||||
recentActivities: '最近操作',
|
recentActivities: '最近操作',
|
||||||
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
||||||
|
operationalInsightsTitle: '运营建议',
|
||||||
|
operationalInsightsDesc: '根据文件健康、上传策略和近期容量自动生成下一步动作。',
|
||||||
|
operationalInsightActionSettings: '调整设置',
|
||||||
|
operationalInsightActionFileQueue: '查看队列',
|
||||||
|
operationalInsightSeverity: {
|
||||||
|
danger: '高风险',
|
||||||
|
warning: '需处理',
|
||||||
|
success: '稳定',
|
||||||
|
neutral: '建议'
|
||||||
|
},
|
||||||
viewAllActivities: '查看全部',
|
viewAllActivities: '查看全部',
|
||||||
activityTimelineTitle: '操作时间线',
|
activityTimelineTitle: '操作时间线',
|
||||||
activityTimelineFiltered: '当前结果',
|
activityTimelineFiltered: '当前结果',
|
||||||
@@ -879,6 +929,36 @@ export default {
|
|||||||
local_file: '本地文件',
|
local_file: '本地文件',
|
||||||
system: '系统'
|
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: '最近分享',
|
recentFiles: '最近分享',
|
||||||
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
||||||
available: '可取件',
|
available: '可取件',
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export interface DashboardData {
|
|||||||
recentFiles?: DashboardRecentFile[]
|
recentFiles?: DashboardRecentFile[]
|
||||||
recentActivities?: DashboardActivity[]
|
recentActivities?: DashboardActivity[]
|
||||||
recent_activities?: DashboardActivity[]
|
recent_activities?: DashboardActivity[]
|
||||||
|
operationalInsights?: DashboardOperationalInsight[]
|
||||||
|
operational_insights?: DashboardOperationalInsight[]
|
||||||
|
insights?: DashboardOperationalInsight[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardSuffixStat {
|
export interface DashboardSuffixStat {
|
||||||
@@ -120,6 +123,36 @@ export interface DashboardActivitiesResponse {
|
|||||||
target_type_options?: DashboardActivityOption[]
|
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<
|
export type DashboardViewData = Omit<
|
||||||
DashboardData,
|
DashboardData,
|
||||||
| keyof DashboardHealthSummary
|
| keyof DashboardHealthSummary
|
||||||
@@ -138,6 +171,9 @@ export type DashboardViewData = Omit<
|
|||||||
| 'recentFiles'
|
| 'recentFiles'
|
||||||
| 'recentActivities'
|
| 'recentActivities'
|
||||||
| 'recent_activities'
|
| 'recent_activities'
|
||||||
|
| 'operationalInsights'
|
||||||
|
| 'operational_insights'
|
||||||
|
| 'insights'
|
||||||
| 'storageUsed'
|
| 'storageUsed'
|
||||||
| 'yesterdaySize'
|
| 'yesterdaySize'
|
||||||
| 'todaySize'
|
| 'todaySize'
|
||||||
@@ -158,6 +194,7 @@ export type DashboardViewData = Omit<
|
|||||||
topSuffixes: DashboardSuffixStat[]
|
topSuffixes: DashboardSuffixStat[]
|
||||||
recentFiles: DashboardRecentFile[]
|
recentFiles: DashboardRecentFile[]
|
||||||
recentActivities: DashboardActivityViewItem[]
|
recentActivities: DashboardActivityViewItem[]
|
||||||
|
operationalInsights: DashboardOperationalInsightViewItem[]
|
||||||
storageUsed: number
|
storageUsed: number
|
||||||
yesterdaySize: number
|
yesterdaySize: number
|
||||||
todaySize: number
|
todaySize: number
|
||||||
|
|||||||
@@ -84,6 +84,56 @@
|
|||||||
</StatCard>
|
</StatCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="dashboardData.hasExtendedStats && dashboardData.operationalInsights.length > 0"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ t('admin.dashboard.operationalInsightsTitle') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('admin.dashboard.operationalInsightsDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<LightbulbIcon
|
||||||
|
class="hidden h-5 w-5 sm:block"
|
||||||
|
:class="[isDarkMode ? 'text-amber-300' : 'text-amber-500']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<button
|
||||||
|
v-for="insight in dashboardData.operationalInsights"
|
||||||
|
:key="insight.key"
|
||||||
|
type="button"
|
||||||
|
class="group flex min-h-40 flex-col rounded-lg border p-4 text-left transition-colors"
|
||||||
|
:class="getOperationalInsightClass(insight.severity)"
|
||||||
|
@click="openOperationalInsight(insight)"
|
||||||
|
>
|
||||||
|
<span class="flex items-start justify-between gap-3">
|
||||||
|
<span class="rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-current/15">
|
||||||
|
{{ getOperationalInsightSeverityLabel(insight.severity) }}
|
||||||
|
</span>
|
||||||
|
<component :is="getOperationalInsightIcon(insight.severity)" class="h-5 w-5 shrink-0" />
|
||||||
|
</span>
|
||||||
|
<strong class="mt-4 text-base">
|
||||||
|
{{ getOperationalInsightTitle(insight) }}
|
||||||
|
</strong>
|
||||||
|
<span class="mt-2 line-clamp-3 text-sm opacity-80">
|
||||||
|
{{ getOperationalInsightDescription(insight) }}
|
||||||
|
</span>
|
||||||
|
<span class="mt-auto flex items-center justify-between gap-2 pt-4 text-sm font-medium">
|
||||||
|
<span>{{ getOperationalInsightActionLabel(insight) }}</span>
|
||||||
|
<ArrowRightIcon
|
||||||
|
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||||
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-5 flex items-center justify-between">
|
||||||
@@ -669,6 +719,7 @@ import {
|
|||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
HardDriveIcon,
|
HardDriveIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
|
LightbulbIcon,
|
||||||
ListFilterIcon,
|
ListFilterIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
@@ -684,7 +735,12 @@ import { useDashboardStats, useInjectedDarkMode } from '@/composables'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { formatFileSize, formatTimestamp } from '@/utils/common'
|
import { formatFileSize, formatTimestamp } from '@/utils/common'
|
||||||
import { ROUTES } from '@/constants'
|
import { ROUTES } from '@/constants'
|
||||||
import type { DashboardActivityViewItem, DashboardHealthAction } from '@/types'
|
import type {
|
||||||
|
DashboardActivityViewItem,
|
||||||
|
DashboardHealthAction,
|
||||||
|
DashboardInsightSeverity,
|
||||||
|
DashboardOperationalInsightViewItem
|
||||||
|
} from '@/types'
|
||||||
|
|
||||||
const isDarkMode = useInjectedDarkMode()
|
const isDarkMode = useInjectedDarkMode()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -796,6 +852,66 @@ const healthActions = computed<DashboardHealthAction[]>(() => [
|
|||||||
|
|
||||||
const getSuffixRatio = (count: number) => Math.round((count / maxSuffixCount.value) * 100)
|
const getSuffixRatio = (count: number) => Math.round((count / maxSuffixCount.value) * 100)
|
||||||
|
|
||||||
|
const operationalInsightIconMap: Record<DashboardInsightSeverity, Component> = {
|
||||||
|
danger: AlertTriangleIcon,
|
||||||
|
warning: ClockIcon,
|
||||||
|
success: CheckCircleIcon,
|
||||||
|
neutral: ShieldCheckIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperationalInsightIcon = (severity: DashboardInsightSeverity) =>
|
||||||
|
operationalInsightIconMap[severity]
|
||||||
|
|
||||||
|
const getOperationalInsightClass = (severity: DashboardInsightSeverity) => {
|
||||||
|
const darkClasses: Record<DashboardInsightSeverity, string> = {
|
||||||
|
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<DashboardInsightSeverity, string> = {
|
||||||
|
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) => {
|
const formatCreatedAt = (value: string | null) => {
|
||||||
if (!value) return '-'
|
if (!value) return '-'
|
||||||
return formatTimestamp(value, 'datetime')
|
return formatTimestamp(value, 'datetime')
|
||||||
|
|||||||
Reference in New Issue
Block a user