feat: add dashboard health actions

This commit is contained in:
Lan
2026-06-03 06:15:15 +08:00
parent 6970d0e467
commit 198f0b9d45
8 changed files with 394 additions and 39 deletions
+45 -2
View File
@@ -46,6 +46,8 @@ const emptySummary = (): AdminFileSummary => ({
expiringSoonCount: 0, expiringSoonCount: 0,
storageIssueCount: 0, storageIssueCount: 0,
neverRetrievedCount: 0, neverRetrievedCount: 0,
healthyCount: 0,
permanentCount: 0,
storageUsed: 0, storageUsed: 0,
usedCount: 0 usedCount: 0
}) })
@@ -219,7 +221,12 @@ export function useAdminFiles() {
{ {
value: 'healthy', value: 'healthy',
label: t('fileManage.healthFilters.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) => neverRetrievedCount: files.filter((file) =>
file.statusInsightReasons.includes('never_retrieved') file.statusInsightReasons.includes('never_retrieved')
).length, ).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), storageUsed: files.reduce((totalSize, file) => totalSize + normalizeCount(file.size), 0),
usedCount: files.reduce( usedCount: files.reduce(
(totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count), (totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count),
@@ -312,6 +321,40 @@ export function useAdminFiles() {
) )
}) })
const normalizeSummary = (
rawSummary: Partial<AdminFileSummary> | 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 = ( const normalizeInsightSeverity = (
severity: AdminFileInsightSeverity | undefined severity: AdminFileInsightSeverity | undefined
): AdminFileInsightSeverity => { ): AdminFileInsightSeverity => {
@@ -684,7 +727,7 @@ export function useAdminFiles() {
tableData.value = res.detail.data.map(createFileViewItem) tableData.value = res.detail.data.map(createFileViewItem)
params.value.total = res.detail.total 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() syncSelectedFilesWithCurrentPage()
} catch (error) { } catch (error) {
hasLoadError.value = true hasLoadError.value = true
+49 -1
View File
@@ -1,6 +1,6 @@
import { computed, ref, reactive } from 'vue' import { computed, ref, reactive } from 'vue'
import { StatsService } from '@/services' import { StatsService } from '@/services'
import type { DashboardViewData } from '@/types' import type { DashboardData, DashboardHealthSummary, DashboardViewData } from '@/types'
import { formatFileSize, getErrorMessage } from '@/utils/common' import { formatFileSize, getErrorMessage } from '@/utils/common'
type UseDashboardStatsOptions = { type UseDashboardStatsOptions = {
@@ -27,6 +27,14 @@ const emptyDashboardData = (): DashboardViewData => ({
openUpload: 0, openUpload: 0,
enableChunk: 0, enableChunk: 0,
maxSaveSeconds: 0, maxSaveSeconds: 0,
healthAttentionCount: 0,
healthDangerCount: 0,
healthWarningCount: 0,
expiringSoonCount: 0,
storageIssueCount: 0,
neverRetrievedCount: 0,
healthyCount: 0,
permanentCount: 0,
topSuffixes: [], topSuffixes: [],
recentFiles: [], recentFiles: [],
storageUsedText: '0 Bytes', storageUsedText: '0 Bytes',
@@ -37,6 +45,8 @@ const emptyDashboardData = (): DashboardViewData => ({
activeRatio: 0, activeRatio: 0,
textRatio: 0, textRatio: 0,
fileRatio: 0, fileRatio: 0,
healthyRatio: 0,
healthAttentionRatio: 0,
todaySizeRatio: 0 todaySizeRatio: 0
}) })
@@ -62,6 +72,34 @@ const normalizeRecentFiles = (recentFiles: DashboardViewData['recentFiles']) =>
usedCount: toNumber(file.usedCount) 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 = {}) { export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
const dashboardData = reactive<DashboardViewData>(emptyDashboardData()) const dashboardData = reactive<DashboardViewData>(emptyDashboardData())
const isLoading = ref(false) const isLoading = ref(false)
@@ -103,6 +141,10 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
dashboardData.openUpload = toNumber(detail.openUpload) dashboardData.openUpload = toNumber(detail.openUpload)
dashboardData.enableChunk = toNumber(detail.enableChunk) dashboardData.enableChunk = toNumber(detail.enableChunk)
dashboardData.maxSaveSeconds = toNumber(detail.maxSaveSeconds) dashboardData.maxSaveSeconds = toNumber(detail.maxSaveSeconds)
const healthSummary = normalizeHealthSummary(detail)
healthSummaryKeys.forEach((key) => {
dashboardData[key] = healthSummary[key]
})
dashboardData.topSuffixes = detail.topSuffixes || [] dashboardData.topSuffixes = detail.topSuffixes || []
dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || []) dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || [])
@@ -120,6 +162,12 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
dashboardData.fileRatio = dashboardData.totalFiles dashboardData.fileRatio = dashboardData.totalFiles
? clampRatio((dashboardData.fileCount / dashboardData.totalFiles) * 100) ? clampRatio((dashboardData.fileCount / dashboardData.totalFiles) * 100)
: 0 : 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 dashboardData.todaySizeRatio = dashboardData.uploadSizeLimit
? clampRatio((dashboardData.todaySize / dashboardData.uploadSizeLimit) * 100) ? clampRatio((dashboardData.todaySize / dashboardData.uploadSizeLimit) * 100)
: 0 : 0
+25 -2
View File
@@ -69,7 +69,7 @@ export default {
lastUpdated: 'Last updated: {time}', lastUpdated: 'Last updated: {time}',
loadFailed: 'Failed to load dashboard data', loadFailed: 'Failed to load dashboard data',
fileHealth: 'File Health', 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', activeFileRatio: 'Active Ratio',
fileShareRatio: 'File Ratio', fileShareRatio: 'File Ratio',
textShareRatio: 'Text Ratio', textShareRatio: 'Text Ratio',
@@ -91,6 +91,28 @@ export default {
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',
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: { table: {
file: 'File', file: 'File',
size: 'Size', size: 'Size',
@@ -410,7 +432,8 @@ export default {
expiringSoon: 'Expiring Soon', expiringSoon: 'Expiring Soon',
storageIssue: 'Storage Issues', storageIssue: 'Storage Issues',
neverRetrieved: 'Never Retrieved', neverRetrieved: 'Never Retrieved',
healthy: 'Healthy' healthy: 'Healthy',
permanent: 'Permanent'
}, },
sortBy: 'Sort', sortBy: 'Sort',
unlimited: 'Unlimited', unlimited: 'Unlimited',
+25 -2
View File
@@ -69,7 +69,7 @@ export default {
lastUpdated: '最近更新:{time}', lastUpdated: '最近更新:{time}',
loadFailed: '仪表盘数据加载失败', loadFailed: '仪表盘数据加载失败',
fileHealth: '文件健康', fileHealth: '文件健康',
fileHealthDesc: '基于真实文件记录统计有效、过期和类型分布。', fileHealthDesc: '基于状态洞察统计可取件、风险与待处理队列。',
activeFileRatio: '有效占比', activeFileRatio: '有效占比',
fileShareRatio: '文件占比', fileShareRatio: '文件占比',
textShareRatio: '文本占比', textShareRatio: '文本占比',
@@ -91,6 +91,28 @@ export default {
recentFiles: '最近分享', recentFiles: '最近分享',
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
available: '可取件', available: '可取件',
healthActions: {
attention: {
title: '需关注',
description: '集中处理异常和预警文件'
},
storageIssue: {
title: '存储异常',
description: '检查缺少下载信息的文件'
},
expiringSoon: {
title: '即将过期',
description: '优先续期仍需保留的分享'
},
neverRetrieved: {
title: '未取件',
description: '识别创建后尚未被取件的记录'
},
permanent: {
title: '永久有效',
description: '查看长期保留的分享记录'
}
},
table: { table: {
file: '文件', file: '文件',
size: '大小', size: '大小',
@@ -408,7 +430,8 @@ export default {
expiringSoon: '即将过期', expiringSoon: '即将过期',
storageIssue: '存储异常', storageIssue: '存储异常',
neverRetrieved: '未取件', neverRetrieved: '未取件',
healthy: '健康' healthy: '健康',
permanent: '永久有效'
}, },
sortBy: '排序', sortBy: '排序',
unlimited: '不限次数', unlimited: '不限次数',
+54 -1
View File
@@ -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 { export interface DashboardData {
totalFiles: number totalFiles: number
storageUsed: number | string storageUsed: number | string
@@ -17,6 +30,15 @@ export interface DashboardData {
openUpload?: number openUpload?: number
enableChunk?: number enableChunk?: number
maxSaveSeconds?: number maxSaveSeconds?: number
healthAttentionCount?: number
healthDangerCount?: number
healthWarningCount?: number
expiringSoonCount?: number
storageIssueCount?: number
neverRetrievedCount?: number
healthyCount?: number
permanentCount?: number
healthSummary?: Partial<DashboardHealthSummary>
topSuffixes?: DashboardSuffixStat[] topSuffixes?: DashboardSuffixStat[]
recentFiles?: DashboardRecentFile[] recentFiles?: DashboardRecentFile[]
} }
@@ -40,7 +62,27 @@ export interface DashboardRecentFile {
isExpired: boolean isExpired: boolean
} }
export interface DashboardViewData extends DashboardData { 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 hasExtendedStats: boolean
activeCount: number activeCount: number
expiredCount: number expiredCount: number
@@ -66,5 +108,16 @@ export interface DashboardViewData extends DashboardData {
activeRatio: number activeRatio: number
textRatio: number textRatio: number
fileRatio: number fileRatio: number
healthyRatio: number
healthAttentionRatio: number
todaySizeRatio: number todaySizeRatio: number
}
export interface DashboardHealthAction {
key: string
label: string
description: string
count: number
health: AdminFileHealthFilter
tone: 'danger' | 'warning' | 'success' | 'neutral'
} }
+2
View File
@@ -69,6 +69,8 @@ export interface AdminFileSummary {
expiringSoonCount: number expiringSoonCount: number
storageIssueCount: number storageIssueCount: number
neverRetrievedCount: number neverRetrievedCount: number
healthyCount: number
permanentCount: number
storageUsed: number storageUsed: number
usedCount: number usedCount: number
} }
+110 -1
View File
@@ -122,6 +122,31 @@
/> />
</div> </div>
<div class="mt-6 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
<button
v-for="action in healthActions"
:key="action.key"
type="button"
class="group flex min-h-28 flex-col justify-between rounded-lg border p-4 text-left transition-colors"
:class="getHealthActionClass(action.tone)"
@click="openHealthQueue(action.health)"
>
<span class="flex items-start justify-between gap-3">
<span>
<span class="block text-2xl font-semibold">{{ action.count }}</span>
<span class="mt-1 block text-sm font-medium">{{ action.label }}</span>
</span>
<component :is="getHealthActionIcon(action.tone)" class="h-5 w-5 shrink-0" />
</span>
<span class="mt-3 flex items-center justify-between gap-2 text-xs">
<span class="line-clamp-2">{{ action.description }}</span>
<ArrowRightIcon
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</span>
</button>
</div>
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="rounded-lg border p-4" :class="[subtlePanelClass]"> <div class="rounded-lg border p-4" :class="[subtlePanelClass]">
<p class="text-sm" :class="[mutedTextClass]"> <p class="text-sm" :class="[mutedTextClass]">
@@ -337,24 +362,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, h, onMounted } from 'vue' import { computed, defineComponent, h, onMounted } from 'vue'
import type { PropType } from 'vue' import type { Component, PropType } from 'vue'
import { useRouter } from 'vue-router'
import { import {
ActivityIcon, ActivityIcon,
AlertTriangleIcon,
ArrowRightIcon,
CheckCircleIcon,
DownloadCloudIcon, DownloadCloudIcon,
FileIcon, FileIcon,
FilesIcon, FilesIcon,
FileTextIcon, FileTextIcon,
HardDriveIcon, HardDriveIcon,
RefreshCwIcon, RefreshCwIcon,
ShieldCheckIcon,
UploadCloudIcon UploadCloudIcon
} from 'lucide-vue-next' } from 'lucide-vue-next'
import StatCard from '@/components/common/StatCard.vue' import StatCard from '@/components/common/StatCard.vue'
import { useDashboardStats, useInjectedDarkMode } from '@/composables' 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 type { DashboardHealthAction } from '@/types'
const isDarkMode = useInjectedDarkMode() const isDarkMode = useInjectedDarkMode()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter()
const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedText } = const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedText } =
useDashboardStats({ useDashboardStats({
loadFailedMessage: t('admin.dashboard.loadFailed') loadFailedMessage: t('admin.dashboard.loadFailed')
@@ -380,6 +413,49 @@ const maxSaveTimeText = computed(() => {
return `${Math.floor(dashboardData.maxSaveSeconds / 60)}${t('common.minute')}` return `${Math.floor(dashboardData.maxSaveSeconds / 60)}${t('common.minute')}`
}) })
const healthActions = computed<DashboardHealthAction[]>(() => [
{
key: 'attention',
label: t('admin.dashboard.healthActions.attention.title'),
description: t('admin.dashboard.healthActions.attention.description'),
count: dashboardData.healthAttentionCount,
health: 'attention',
tone: dashboardData.healthAttentionCount > 0 ? 'danger' : 'success'
},
{
key: 'storageIssue',
label: t('admin.dashboard.healthActions.storageIssue.title'),
description: t('admin.dashboard.healthActions.storageIssue.description'),
count: dashboardData.storageIssueCount,
health: 'storage_issue',
tone: dashboardData.storageIssueCount > 0 ? 'danger' : 'success'
},
{
key: 'expiringSoon',
label: t('admin.dashboard.healthActions.expiringSoon.title'),
description: t('admin.dashboard.healthActions.expiringSoon.description'),
count: dashboardData.expiringSoonCount,
health: 'expiring_soon',
tone: dashboardData.expiringSoonCount > 0 ? 'warning' : 'success'
},
{
key: 'neverRetrieved',
label: t('admin.dashboard.healthActions.neverRetrieved.title'),
description: t('admin.dashboard.healthActions.neverRetrieved.description'),
count: dashboardData.neverRetrievedCount,
health: 'never_retrieved',
tone: dashboardData.neverRetrievedCount > 0 ? 'neutral' : 'success'
},
{
key: 'permanent',
label: t('admin.dashboard.healthActions.permanent.title'),
description: t('admin.dashboard.healthActions.permanent.description'),
count: dashboardData.permanentCount,
health: 'permanent',
tone: 'success'
}
])
const getSuffixRatio = (count: number) => Math.round((count / maxSuffixCount.value) * 100) const getSuffixRatio = (count: number) => Math.round((count / maxSuffixCount.value) * 100)
const formatCreatedAt = (value: string | null) => { const formatCreatedAt = (value: string | null) => {
@@ -387,6 +463,39 @@ const formatCreatedAt = (value: string | null) => {
return formatTimestamp(value, 'datetime') return formatTimestamp(value, 'datetime')
} }
const healthActionIconMap: Record<DashboardHealthAction['tone'], Component> = {
danger: AlertTriangleIcon,
warning: AlertTriangleIcon,
success: CheckCircleIcon,
neutral: ShieldCheckIcon
}
const getHealthActionIcon = (tone: DashboardHealthAction['tone']) => healthActionIconMap[tone]
const getHealthActionClass = (tone: DashboardHealthAction['tone']) => {
const darkClasses: Record<DashboardHealthAction['tone'], string> = {
danger: 'border-red-500/20 bg-red-500/10 text-red-200 hover:border-red-400/40',
warning: 'border-amber-500/20 bg-amber-500/10 text-amber-200 hover:border-amber-400/40',
success: 'border-emerald-500/20 bg-emerald-500/10 text-emerald-200 hover:border-emerald-400/40',
neutral: 'border-gray-700 bg-gray-900/30 text-gray-300 hover:border-gray-600'
}
const lightClasses: Record<DashboardHealthAction['tone'], string> = {
danger: 'border-red-100 bg-red-50 text-red-700 hover:border-red-200',
warning: 'border-amber-100 bg-amber-50 text-amber-700 hover:border-amber-200',
success: 'border-emerald-100 bg-emerald-50 text-emerald-700 hover:border-emerald-200',
neutral: 'border-gray-100 bg-gray-50 text-gray-700 hover:border-gray-200'
}
return isDarkMode.value ? darkClasses[tone] : lightClasses[tone]
}
const openHealthQueue = (health: DashboardHealthAction['health']) => {
void router.push({
path: ROUTES.FILE_MANAGE,
query: { health }
})
}
const MetricProgress = defineComponent({ const MetricProgress = defineComponent({
name: 'MetricProgress', name: 'MetricProgress',
props: { props: {
+58 -4
View File
@@ -20,7 +20,7 @@
v-if="hasActiveFilters" v-if="hasActiveFilters"
variant="outline" variant="outline"
:disabled="isLoading" :disabled="isLoading"
@click="resetFilters" @click="handleResetFilters"
> >
<template #icon> <template #icon>
<XIcon class="mr-2 h-4 w-4" /> <XIcon class="mr-2 h-4 w-4" />
@@ -109,7 +109,7 @@
type="button" type="button"
class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors" class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors"
:class="getHealthFilterClass(option.value)" :class="getHealthFilterClass(option.value)"
@click="setHealthFilter(option.value)" @click="handleHealthFilterChange(option.value)"
> >
<span>{{ option.label }}</span> <span>{{ option.label }}</span>
<span v-if="typeof option.count === 'number'">({{ option.count }})</span> <span v-if="typeof option.count === 'number'">({{ option.count }})</span>
@@ -262,7 +262,7 @@
v-if="hasActiveFilters" v-if="hasActiveFilters"
class="mt-4" class="mt-4"
variant="secondary" variant="secondary"
@click="resetFilters" @click="handleResetFilters"
> >
<template #icon> <template #icon>
<XIcon class="mr-2 h-4 w-4" /> <XIcon class="mr-2 h-4 w-4" />
@@ -1027,8 +1027,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, type Component } from 'vue' import { computed, onMounted, watch, type Component } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import type { import type {
AdminBatchEditMode, AdminBatchEditMode,
AdminFileHealthFilter, AdminFileHealthFilter,
@@ -1069,6 +1070,8 @@ import { useAdminFiles, useInjectedDarkMode } from '@/composables'
const { t } = useI18n() const { t } = useI18n()
const isDarkMode = useInjectedDarkMode() const isDarkMode = useInjectedDarkMode()
const route = useRoute()
const router = useRouter()
const fileTableHeaders = computed(() => [ const fileTableHeaders = computed(() => [
t('fileManage.headers.select'), t('fileManage.headers.select'),
@@ -1348,6 +1351,47 @@ const sortOrderOptions = computed<{ value: AdminFileSortOrder; label: string }[]
{ value: 'asc', label: t('fileManage.sort.asc') } { value: 'asc', label: t('fileManage.sort.asc') }
]) ])
const healthFilterValues: AdminFileHealthFilter[] = [
'all',
'attention',
'danger',
'warning',
'healthy',
'expired',
'expiring_soon',
'storage_issue',
'never_retrieved',
'permanent'
]
const getRouteHealthFilter = (): AdminFileHealthFilter => {
const health = route.query.health
if (typeof health === 'string' && healthFilterValues.includes(health as AdminFileHealthFilter)) {
return health as AdminFileHealthFilter
}
return 'all'
}
const syncHealthFilterQuery = async (health: AdminFileHealthFilter) => {
const query = { ...route.query }
if (health === 'all') {
delete query.health
} else {
query.health = health
}
await router.replace({ query })
}
const handleHealthFilterChange = async (health: AdminFileHealthFilter) => {
await setHealthFilter(health)
await syncHealthFilterQuery(health)
}
const handleResetFilters = async () => {
await resetFilters()
await syncHealthFilterQuery('all')
}
const batchEditModeOptions = computed< const batchEditModeOptions = computed<
{ value: AdminBatchEditMode; label: string; icon: Component }[] { value: AdminBatchEditMode; label: string; icon: Component }[]
>(() => [ >(() => [
@@ -1484,7 +1528,17 @@ const getTimelineDotClass = (severity: AdminFileInsightSeverity) => {
return classes[severity] return classes[severity]
} }
watch(
() => route.query.health,
async () => {
const health = getRouteHealthFilter()
if (params.value.health === health) return
await setHealthFilter(health)
}
)
onMounted(() => { onMounted(() => {
params.value.health = getRouteHealthFilter()
void loadFiles() void loadFiles()
}) })
</script> </script>