From a573f1f5cc1872721c2717b9eed160330c854708 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 05:55:18 +0800 Subject: [PATCH] feat: add admin file health filters --- src/composables/useAdminFiles.ts | 141 +++++++++++++++++++++++----- src/i18n/locales/en-US.ts | 11 +++ src/i18n/locales/zh-CN.ts | 11 +++ src/types/file.ts | 30 +++++- src/views/manage/FileManageView.vue | 40 ++++++++ 5 files changed, 205 insertions(+), 28 deletions(-) diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index b0d46c0..6e544df 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -12,6 +12,7 @@ import type { AdminFileDetailViewItem, AdminFileInsightSeverity, AdminFilePatchPayload, + AdminFileHealthFilter, AdminFileListParams, AdminFilePolicyAction, AdminFilePolicyActionRequest, @@ -39,6 +40,12 @@ const emptySummary = (): AdminFileSummary => ({ textCount: 0, fileCount: 0, chunkedCount: 0, + healthAttentionCount: 0, + healthDangerCount: 0, + healthWarningCount: 0, + expiringSoonCount: 0, + storageIssueCount: 0, + neverRetrievedCount: 0, storageUsed: 0, usedCount: 0 }) @@ -72,6 +79,12 @@ type DetailPolicyActionOption = { description: string } +type HealthFilterOption = { + value: AdminFileHealthFilter + label: string + count?: number +} + const emptyBatchEditForm = (): AdminBatchEditForm => ({ mode: 'expiresAt', expired_at: '', @@ -106,6 +119,7 @@ export function useAdminFiles() { keyword: '', status: 'all', type: 'all', + health: 'all', sortBy: 'created_at', sortOrder: 'desc' }) @@ -157,6 +171,7 @@ export function useAdminFiles() { keyword: params.value.keyword, status: params.value.status === 'all' ? undefined : params.value.status, type: params.value.type === 'all' ? undefined : params.value.type, + health: params.value.health === 'all' ? undefined : params.value.health, sortBy: params.value.sortBy, sortOrder: params.value.sortOrder })) @@ -165,9 +180,49 @@ export function useAdminFiles() { () => Boolean(params.value.keyword?.trim()) || params.value.status !== 'all' || - params.value.type !== 'all' + params.value.type !== 'all' || + params.value.health !== 'all' ) + const healthFilterOptions = computed(() => [ + { value: 'all', label: t('fileManage.healthFilters.all'), count: summary.value.totalFiles }, + { + value: 'attention', + label: t('fileManage.healthFilters.attention'), + count: summary.value.healthAttentionCount + }, + { + value: 'danger', + label: t('fileManage.healthFilters.danger'), + count: summary.value.healthDangerCount + }, + { + value: 'warning', + label: t('fileManage.healthFilters.warning'), + count: summary.value.healthWarningCount + }, + { + value: 'expiring_soon', + label: t('fileManage.healthFilters.expiringSoon'), + count: summary.value.expiringSoonCount + }, + { + value: 'storage_issue', + label: t('fileManage.healthFilters.storageIssue'), + count: summary.value.storageIssueCount + }, + { + value: 'never_retrieved', + label: t('fileManage.healthFilters.neverRetrieved'), + count: summary.value.neverRetrievedCount + }, + { + value: 'healthy', + label: t('fileManage.healthFilters.healthy'), + count: Math.max(summary.value.totalFiles - summary.value.healthAttentionCount, 0) + } + ]) + const detailPolicyActionOptions = computed(() => [ { action: 'extend_24h', @@ -237,6 +292,19 @@ export function useAdminFiles() { textCount: files.filter((file) => file.isTextFile).length, fileCount: files.filter((file) => !file.isTextFile).length, chunkedCount: files.filter((file) => file.isChunkedFile).length, + healthAttentionCount: files.filter((file) => + ['danger', 'warning'].includes(file.statusInsightSeverity) + ).length, + healthDangerCount: files.filter((file) => file.statusInsightSeverity === 'danger').length, + healthWarningCount: files.filter((file) => file.statusInsightSeverity === 'warning').length, + expiringSoonCount: files.filter((file) => file.statusInsightReasons.includes('expires_soon')) + .length, + storageIssueCount: files.filter((file) => + file.statusInsightReasons.includes('storage_metadata_incomplete') + ).length, + neverRetrievedCount: files.filter((file) => + file.statusInsightReasons.includes('never_retrieved') + ).length, storageUsed: files.reduce((totalSize, file) => totalSize + normalizeCount(file.size), 0), usedCount: files.reduce( (totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count), @@ -244,29 +312,6 @@ export function useAdminFiles() { ) }) - const createFileViewItem = (file: FileListItem): AdminFileViewItem => { - const isTextFile = inferIsText(file) - const isExpiredFile = inferIsExpired(file) - const isChunkedFile = inferIsChunked(file) - const remainingDownloadsValue = getRemainingDownloads(file) - const usedCount = normalizeCount(file.usedCount ?? file.used_count) - - return { - ...file, - displayName: file.name || `${file.prefix}${file.suffix}` || file.code, - displaySize: formatFileSize(file.size), - displayExpiredAt: file.expired_at - ? formatTimestamp(file.expired_at) - : t('send.expiration.units.forever'), - displayUsage: `${usedCount} ${t('common.times')}`, - isTextFile, - isExpiredFile, - isChunkedFile, - remainingDownloadsValue, - canPreviewText: isTextFile - } - } - const normalizeInsightSeverity = ( severity: AdminFileInsightSeverity | undefined ): AdminFileInsightSeverity => { @@ -279,6 +324,45 @@ export function useAdminFiles() { return 'neutral' } + const createFileViewItem = (file: FileListItem): AdminFileViewItem => { + const isTextFile = inferIsText(file) + const isExpiredFile = inferIsExpired(file) + const isChunkedFile = inferIsChunked(file) + const remainingDownloadsValue = getRemainingDownloads(file) + const usedCount = normalizeCount(file.usedCount ?? file.used_count) + const statusInsights = file.statusInsights ?? file.status_insights + const isPermanentFile = + !file.expired_at && + (file.expired_count === null || file.expired_count === undefined || file.expired_count < 0) + const statusInsightState = + statusInsights?.state ?? + (isExpiredFile ? 'expired' : isPermanentFile ? 'permanent' : 'available') + const statusInsightNextAction = + statusInsights?.nextAction ?? statusInsights?.next_action ?? 'monitor' + const statusInsightSeverity = normalizeInsightSeverity(statusInsights?.severity) + + return { + ...file, + displayName: file.name || `${file.prefix}${file.suffix}` || file.code, + displaySize: formatFileSize(file.size), + displayExpiredAt: file.expired_at + ? formatTimestamp(file.expired_at) + : t('send.expiration.units.forever'), + displayUsage: `${usedCount} ${t('common.times')}`, + displayHealthState: t(`fileManage.insightStates.${statusInsightState}`), + displayHealthAction: t(`fileManage.insightActions.${statusInsightNextAction}`), + isTextFile, + isExpiredFile, + isChunkedFile, + remainingDownloadsValue, + canPreviewText: isTextFile, + statusInsightSeverity, + statusInsightState, + statusInsightNextAction, + statusInsightReasons: statusInsights?.reasons || [] + } + } + const formatTimelineValue = (item: AdminFileDetailTimelineItem) => { if (item.key === 'download_limit') { if (item.status === 'unlimited') return t('fileManage.unlimited') @@ -627,6 +711,7 @@ export function useAdminFiles() { params.value.keyword = '' params.value.status = 'all' params.value.type = 'all' + params.value.health = 'all' params.value.sortBy = 'created_at' params.value.sortOrder = 'desc' clearSelection() @@ -645,6 +730,12 @@ export function useAdminFiles() { await loadFiles() } + const setHealthFilter = async (health: AdminFileHealthFilter) => { + params.value.health = health + params.value.page = 1 + await loadFiles() + } + const handlePageChange = async (page: number | string) => { if (typeof page === 'string') return if (page < 1 || page > totalPages.value) return @@ -1010,6 +1101,7 @@ export function useAdminFiles() { batchEditForm, detailPolicyActionOptions, downloadingFileId, + healthFilterOptions, hasSelectedFiles, params, previewFile, @@ -1050,6 +1142,7 @@ export function useAdminFiles() { openTextPreview, refreshFiles, resetFilters, + setHealthFilter, setStatusFilter, setTypeFilter, toggleCurrentPageSelection, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index c737179..6c1402e 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -395,12 +395,23 @@ export default { storageUsed: 'Storage Used', statusLabel: 'Status', typeLabel: 'Type', + healthLabel: 'Health', all: 'All', active: 'Available', expired: 'Expired', fileType: 'File', textType: 'Text', chunkedType: 'Chunked', + healthFilters: { + all: 'All', + attention: 'Needs Attention', + danger: 'Issues', + warning: 'Warnings', + expiringSoon: 'Expiring Soon', + storageIssue: 'Storage Issues', + neverRetrieved: 'Never Retrieved', + healthy: 'Healthy' + }, sortBy: 'Sort', unlimited: 'Unlimited', remaining: '{count} left', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index b42cae8..043149d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -393,12 +393,23 @@ export default { storageUsed: '占用空间', statusLabel: '状态', typeLabel: '类型', + healthLabel: '健康', all: '全部', active: '可取件', expired: '已过期', fileType: '文件', textType: '文本', chunkedType: '分片', + healthFilters: { + all: '全部', + attention: '需关注', + danger: '异常', + warning: '预警', + expiringSoon: '即将过期', + storageIssue: '存储异常', + neverRetrieved: '未取件', + healthy: '健康' + }, sortBy: '排序', unlimited: '不限次数', remaining: '剩余 {count} 次', diff --git a/src/types/file.ts b/src/types/file.ts index 876962c..73b406c 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -28,6 +28,8 @@ export interface FileListItem { is_expired?: boolean isChunked?: boolean is_chunked?: boolean + statusInsights?: AdminFileDetailStatusInsights + status_insights?: AdminFileDetailStatusInsights remainingDownloads?: number | null remaining_downloads?: number | null usedCount?: number @@ -41,11 +43,17 @@ export interface AdminFileViewItem extends FileListItem { displaySize: string displayExpiredAt: string displayUsage: string + displayHealthState: string + displayHealthAction: string isTextFile: boolean isExpiredFile: boolean isChunkedFile: boolean remainingDownloadsValue: number | null canPreviewText: boolean + statusInsightSeverity: AdminFileInsightSeverity + statusInsightState: string + statusInsightNextAction: string + statusInsightReasons: string[] } export interface AdminFileSummary { @@ -55,12 +63,29 @@ export interface AdminFileSummary { textCount: number fileCount: number chunkedCount: number + healthAttentionCount: number + healthDangerCount: number + healthWarningCount: number + expiringSoonCount: number + storageIssueCount: number + neverRetrievedCount: number storageUsed: number usedCount: number } export type AdminFileStatusFilter = 'all' | 'active' | 'expired' export type AdminFileTypeFilter = 'all' | 'file' | 'text' | 'chunked' +export type AdminFileHealthFilter = + | 'all' + | 'attention' + | 'danger' + | 'warning' + | 'healthy' + | 'expired' + | 'expiring_soon' + | 'storage_issue' + | 'never_retrieved' + | 'permanent' export type AdminFileSortBy = 'created_at' | 'expired_at' | 'name' | 'size' | 'used_count' | 'code' export type AdminFileSortOrder = 'asc' | 'desc' @@ -70,6 +95,7 @@ export interface AdminFileListParams { keyword?: string status?: AdminFileStatusFilter type?: AdminFileTypeFilter + health?: AdminFileHealthFilter sortBy?: AdminFileSortBy sortOrder?: AdminFileSortOrder } @@ -241,10 +267,6 @@ export interface AdminFileDetailViewItem extends AdminFileViewItem { filePathValue?: string | null uuidFileNameValue?: string | null uploadIdValue?: string | null - statusInsightSeverity: AdminFileInsightSeverity - statusInsightState: string - statusInsightNextAction: string - statusInsightReasons: string[] statusInsightMetrics?: AdminFileDetailInsightMetrics detailTimeline: AdminFileDetailTimelineViewItem[] } diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 669677e..1715d69 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -98,6 +98,24 @@ +
+ + + {{ t('fileManage.healthLabel') }} + + +
+
@@ -297,6 +315,22 @@

{{ file.suffix || t('fileManage.textType') }}

+
+ + {{ file.displayHealthState }} + + + {{ file.displayHealthAction }} + +
@@ -997,6 +1031,7 @@ import { computed, onMounted, type Component } from 'vue' import { useI18n } from 'vue-i18n' import type { AdminBatchEditMode, + AdminFileHealthFilter, AdminFileInsightSeverity, AdminFilePolicyAction, AdminFileSortBy, @@ -1064,6 +1099,7 @@ const { batchEditForm, detailPolicyActionOptions, downloadingFileId, + healthFilterOptions, hasSelectedFiles, params, previewFile, @@ -1103,6 +1139,7 @@ const { openTextPreview, refreshFiles, resetFilters, + setHealthFilter, setStatusFilter, setTypeFilter, toggleCurrentPageSelection, @@ -1353,6 +1390,9 @@ const getStatusFilterClass = (value: AdminFileStatusFilter) => const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value) +const getHealthFilterClass = (value: AdminFileHealthFilter) => + getPillClass(params.value.health === value) + const getBatchEditModeClass = (value: AdminBatchEditMode) => { if (batchEditForm.value.mode === value) { return isDarkMode.value