feat: add admin file health filters

This commit is contained in:
Lan
2026-06-03 05:55:18 +08:00
parent 167d5cce3f
commit 6970d0e467
5 changed files with 205 additions and 28 deletions
+117 -24
View File
@@ -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<HealthFilterOption[]>(() => [
{ 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<DetailPolicyActionOption[]>(() => [
{
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,
+11
View File
@@ -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',
+11
View File
@@ -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} 次',
+26 -4
View File
@@ -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[]
}
+40
View File
@@ -98,6 +98,24 @@
</button>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]">
<ActivityIcon class="mr-1 h-3.5 w-3.5" />
{{ t('fileManage.healthLabel') }}
</span>
<button
v-for="option in healthFilterOptions"
:key="option.value"
type="button"
class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors"
:class="getHealthFilterClass(option.value)"
@click="setHealthFilter(option.value)"
>
<span>{{ option.label }}</span>
<span v-if="typeof option.count === 'number'">({{ option.count }})</span>
</button>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]">
<ArchiveIcon class="mr-1 h-3.5 w-3.5" />
@@ -297,6 +315,22 @@
<p class="mt-0.5 truncate text-xs" :class="[mutedTextClass]">
{{ file.suffix || t('fileManage.textType') }}
</p>
<div class="mt-2 flex max-w-sm flex-wrap items-center gap-1.5">
<span
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
:class="getInsightBadgeClass(file.statusInsightSeverity)"
:title="file.displayHealthAction"
>
{{ file.displayHealthState }}
</span>
<span
class="max-w-[16rem] truncate text-xs"
:class="[mutedTextClass]"
:title="file.displayHealthAction"
>
{{ file.displayHealthAction }}
</span>
</div>
</div>
</div>
</td>
@@ -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