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, AdminFileDetailViewItem,
AdminFileInsightSeverity, AdminFileInsightSeverity,
AdminFilePatchPayload, AdminFilePatchPayload,
AdminFileHealthFilter,
AdminFileListParams, AdminFileListParams,
AdminFilePolicyAction, AdminFilePolicyAction,
AdminFilePolicyActionRequest, AdminFilePolicyActionRequest,
@@ -39,6 +40,12 @@ const emptySummary = (): AdminFileSummary => ({
textCount: 0, textCount: 0,
fileCount: 0, fileCount: 0,
chunkedCount: 0, chunkedCount: 0,
healthAttentionCount: 0,
healthDangerCount: 0,
healthWarningCount: 0,
expiringSoonCount: 0,
storageIssueCount: 0,
neverRetrievedCount: 0,
storageUsed: 0, storageUsed: 0,
usedCount: 0 usedCount: 0
}) })
@@ -72,6 +79,12 @@ type DetailPolicyActionOption = {
description: string description: string
} }
type HealthFilterOption = {
value: AdminFileHealthFilter
label: string
count?: number
}
const emptyBatchEditForm = (): AdminBatchEditForm => ({ const emptyBatchEditForm = (): AdminBatchEditForm => ({
mode: 'expiresAt', mode: 'expiresAt',
expired_at: '', expired_at: '',
@@ -106,6 +119,7 @@ export function useAdminFiles() {
keyword: '', keyword: '',
status: 'all', status: 'all',
type: 'all', type: 'all',
health: 'all',
sortBy: 'created_at', sortBy: 'created_at',
sortOrder: 'desc' sortOrder: 'desc'
}) })
@@ -157,6 +171,7 @@ export function useAdminFiles() {
keyword: params.value.keyword, keyword: params.value.keyword,
status: params.value.status === 'all' ? undefined : params.value.status, status: params.value.status === 'all' ? undefined : params.value.status,
type: params.value.type === 'all' ? undefined : params.value.type, type: params.value.type === 'all' ? undefined : params.value.type,
health: params.value.health === 'all' ? undefined : params.value.health,
sortBy: params.value.sortBy, sortBy: params.value.sortBy,
sortOrder: params.value.sortOrder sortOrder: params.value.sortOrder
})) }))
@@ -165,9 +180,49 @@ export function useAdminFiles() {
() => () =>
Boolean(params.value.keyword?.trim()) || Boolean(params.value.keyword?.trim()) ||
params.value.status !== 'all' || 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[]>(() => [ const detailPolicyActionOptions = computed<DetailPolicyActionOption[]>(() => [
{ {
action: 'extend_24h', action: 'extend_24h',
@@ -237,6 +292,19 @@ export function useAdminFiles() {
textCount: files.filter((file) => file.isTextFile).length, textCount: files.filter((file) => file.isTextFile).length,
fileCount: files.filter((file) => !file.isTextFile).length, fileCount: files.filter((file) => !file.isTextFile).length,
chunkedCount: files.filter((file) => file.isChunkedFile).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), 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),
@@ -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 = ( const normalizeInsightSeverity = (
severity: AdminFileInsightSeverity | undefined severity: AdminFileInsightSeverity | undefined
): AdminFileInsightSeverity => { ): AdminFileInsightSeverity => {
@@ -279,6 +324,45 @@ export function useAdminFiles() {
return 'neutral' 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) => { const formatTimelineValue = (item: AdminFileDetailTimelineItem) => {
if (item.key === 'download_limit') { if (item.key === 'download_limit') {
if (item.status === 'unlimited') return t('fileManage.unlimited') if (item.status === 'unlimited') return t('fileManage.unlimited')
@@ -627,6 +711,7 @@ export function useAdminFiles() {
params.value.keyword = '' params.value.keyword = ''
params.value.status = 'all' params.value.status = 'all'
params.value.type = 'all' params.value.type = 'all'
params.value.health = 'all'
params.value.sortBy = 'created_at' params.value.sortBy = 'created_at'
params.value.sortOrder = 'desc' params.value.sortOrder = 'desc'
clearSelection() clearSelection()
@@ -645,6 +730,12 @@ export function useAdminFiles() {
await loadFiles() await loadFiles()
} }
const setHealthFilter = async (health: AdminFileHealthFilter) => {
params.value.health = health
params.value.page = 1
await loadFiles()
}
const handlePageChange = async (page: number | string) => { const handlePageChange = async (page: number | string) => {
if (typeof page === 'string') return if (typeof page === 'string') return
if (page < 1 || page > totalPages.value) return if (page < 1 || page > totalPages.value) return
@@ -1010,6 +1101,7 @@ export function useAdminFiles() {
batchEditForm, batchEditForm,
detailPolicyActionOptions, detailPolicyActionOptions,
downloadingFileId, downloadingFileId,
healthFilterOptions,
hasSelectedFiles, hasSelectedFiles,
params, params,
previewFile, previewFile,
@@ -1050,6 +1142,7 @@ export function useAdminFiles() {
openTextPreview, openTextPreview,
refreshFiles, refreshFiles,
resetFilters, resetFilters,
setHealthFilter,
setStatusFilter, setStatusFilter,
setTypeFilter, setTypeFilter,
toggleCurrentPageSelection, toggleCurrentPageSelection,
+11
View File
@@ -395,12 +395,23 @@ export default {
storageUsed: 'Storage Used', storageUsed: 'Storage Used',
statusLabel: 'Status', statusLabel: 'Status',
typeLabel: 'Type', typeLabel: 'Type',
healthLabel: 'Health',
all: 'All', all: 'All',
active: 'Available', active: 'Available',
expired: 'Expired', expired: 'Expired',
fileType: 'File', fileType: 'File',
textType: 'Text', textType: 'Text',
chunkedType: 'Chunked', 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', sortBy: 'Sort',
unlimited: 'Unlimited', unlimited: 'Unlimited',
remaining: '{count} left', remaining: '{count} left',
+11
View File
@@ -393,12 +393,23 @@ export default {
storageUsed: '占用空间', storageUsed: '占用空间',
statusLabel: '状态', statusLabel: '状态',
typeLabel: '类型', typeLabel: '类型',
healthLabel: '健康',
all: '全部', all: '全部',
active: '可取件', active: '可取件',
expired: '已过期', expired: '已过期',
fileType: '文件', fileType: '文件',
textType: '文本', textType: '文本',
chunkedType: '分片', chunkedType: '分片',
healthFilters: {
all: '全部',
attention: '需关注',
danger: '异常',
warning: '预警',
expiringSoon: '即将过期',
storageIssue: '存储异常',
neverRetrieved: '未取件',
healthy: '健康'
},
sortBy: '排序', sortBy: '排序',
unlimited: '不限次数', unlimited: '不限次数',
remaining: '剩余 {count} 次', remaining: '剩余 {count} 次',
+26 -4
View File
@@ -28,6 +28,8 @@ export interface FileListItem {
is_expired?: boolean is_expired?: boolean
isChunked?: boolean isChunked?: boolean
is_chunked?: boolean is_chunked?: boolean
statusInsights?: AdminFileDetailStatusInsights
status_insights?: AdminFileDetailStatusInsights
remainingDownloads?: number | null remainingDownloads?: number | null
remaining_downloads?: number | null remaining_downloads?: number | null
usedCount?: number usedCount?: number
@@ -41,11 +43,17 @@ export interface AdminFileViewItem extends FileListItem {
displaySize: string displaySize: string
displayExpiredAt: string displayExpiredAt: string
displayUsage: string displayUsage: string
displayHealthState: string
displayHealthAction: string
isTextFile: boolean isTextFile: boolean
isExpiredFile: boolean isExpiredFile: boolean
isChunkedFile: boolean isChunkedFile: boolean
remainingDownloadsValue: number | null remainingDownloadsValue: number | null
canPreviewText: boolean canPreviewText: boolean
statusInsightSeverity: AdminFileInsightSeverity
statusInsightState: string
statusInsightNextAction: string
statusInsightReasons: string[]
} }
export interface AdminFileSummary { export interface AdminFileSummary {
@@ -55,12 +63,29 @@ export interface AdminFileSummary {
textCount: number textCount: number
fileCount: number fileCount: number
chunkedCount: number chunkedCount: number
healthAttentionCount: number
healthDangerCount: number
healthWarningCount: number
expiringSoonCount: number
storageIssueCount: number
neverRetrievedCount: number
storageUsed: number storageUsed: number
usedCount: number usedCount: number
} }
export type AdminFileStatusFilter = 'all' | 'active' | 'expired' export type AdminFileStatusFilter = 'all' | 'active' | 'expired'
export type AdminFileTypeFilter = 'all' | 'file' | 'text' | 'chunked' 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 AdminFileSortBy = 'created_at' | 'expired_at' | 'name' | 'size' | 'used_count' | 'code'
export type AdminFileSortOrder = 'asc' | 'desc' export type AdminFileSortOrder = 'asc' | 'desc'
@@ -70,6 +95,7 @@ export interface AdminFileListParams {
keyword?: string keyword?: string
status?: AdminFileStatusFilter status?: AdminFileStatusFilter
type?: AdminFileTypeFilter type?: AdminFileTypeFilter
health?: AdminFileHealthFilter
sortBy?: AdminFileSortBy sortBy?: AdminFileSortBy
sortOrder?: AdminFileSortOrder sortOrder?: AdminFileSortOrder
} }
@@ -241,10 +267,6 @@ export interface AdminFileDetailViewItem extends AdminFileViewItem {
filePathValue?: string | null filePathValue?: string | null
uuidFileNameValue?: string | null uuidFileNameValue?: string | null
uploadIdValue?: string | null uploadIdValue?: string | null
statusInsightSeverity: AdminFileInsightSeverity
statusInsightState: string
statusInsightNextAction: string
statusInsightReasons: string[]
statusInsightMetrics?: AdminFileDetailInsightMetrics statusInsightMetrics?: AdminFileDetailInsightMetrics
detailTimeline: AdminFileDetailTimelineViewItem[] detailTimeline: AdminFileDetailTimelineViewItem[]
} }
+40
View File
@@ -98,6 +98,24 @@
</button> </button>
</div> </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"> <div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]"> <span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]">
<ArchiveIcon class="mr-1 h-3.5 w-3.5" /> <ArchiveIcon class="mr-1 h-3.5 w-3.5" />
@@ -297,6 +315,22 @@
<p class="mt-0.5 truncate text-xs" :class="[mutedTextClass]"> <p class="mt-0.5 truncate text-xs" :class="[mutedTextClass]">
{{ file.suffix || t('fileManage.textType') }} {{ file.suffix || t('fileManage.textType') }}
</p> </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>
</div> </div>
</td> </td>
@@ -997,6 +1031,7 @@ import { computed, onMounted, type Component } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { import type {
AdminBatchEditMode, AdminBatchEditMode,
AdminFileHealthFilter,
AdminFileInsightSeverity, AdminFileInsightSeverity,
AdminFilePolicyAction, AdminFilePolicyAction,
AdminFileSortBy, AdminFileSortBy,
@@ -1064,6 +1099,7 @@ const {
batchEditForm, batchEditForm,
detailPolicyActionOptions, detailPolicyActionOptions,
downloadingFileId, downloadingFileId,
healthFilterOptions,
hasSelectedFiles, hasSelectedFiles,
params, params,
previewFile, previewFile,
@@ -1103,6 +1139,7 @@ const {
openTextPreview, openTextPreview,
refreshFiles, refreshFiles,
resetFilters, resetFilters,
setHealthFilter,
setStatusFilter, setStatusFilter,
setTypeFilter, setTypeFilter,
toggleCurrentPageSelection, toggleCurrentPageSelection,
@@ -1353,6 +1390,9 @@ const getStatusFilterClass = (value: AdminFileStatusFilter) =>
const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value) const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value)
const getHealthFilterClass = (value: AdminFileHealthFilter) =>
getPillClass(params.value.health === value)
const getBatchEditModeClass = (value: AdminBatchEditMode) => { const getBatchEditModeClass = (value: AdminBatchEditMode) => {
if (batchEditForm.value.mode === value) { if (batchEditForm.value.mode === value) {
return isDarkMode.value return isDarkMode.value