diff --git a/src/composables/useAdminFiles.ts b/src/composables/useAdminFiles.ts index 91a3b1e..77ff751 100644 --- a/src/composables/useAdminFiles.ts +++ b/src/composables/useAdminFiles.ts @@ -22,6 +22,10 @@ import type { AdminFileStatusFilter, AdminFileSummary, AdminFileTypeFilter, + AdminFileViewSummary, + AdminFileViewSummaryItem, + AdminFileViewSummaryViewData, + AdminFileViewSummaryViewItem, AdminFileViewPreset, AdminFileViewPresetParams, AdminFileViewPresetRequest, @@ -58,6 +62,18 @@ const emptySummary = (): AdminFileSummary => ({ usedCount: 0 }) +const emptyViewSummary = (): AdminFileViewSummaryViewData => ({ + total: 0, + filteredTotal: 0, + allTotal: 0, + activeFilterCount: 0, + hasFilters: false, + strongestSeverity: 'success', + summary: emptySummary(), + cards: [], + actions: [] +}) + const normalizeCount = (value: number | string | null | undefined) => Number(value || 0) const isExpiredByDate = (value: string | null | undefined) => { @@ -212,6 +228,7 @@ export function useAdminFiles() { const isLoading = ref(false) const isSaving = ref(false) const summary = ref(emptySummary()) + const viewSummary = ref(emptyViewSummary()) const params = ref({ page: 1, size: 10, @@ -523,7 +540,7 @@ export function useAdminFiles() { } const normalizeInsightSeverity = ( - severity: AdminFileInsightSeverity | undefined + severity: AdminFileInsightSeverity | string | undefined ): AdminFileInsightSeverity => { if (severity === 'success' || severity === 'warning' || severity === 'danger') { return severity @@ -534,6 +551,131 @@ export function useAdminFiles() { return 'neutral' } + const normalizeViewSummaryItems = ( + items: AdminFileViewSummaryItem[] = [] + ): AdminFileViewSummaryViewItem[] => + items + .filter((item) => item && item.key) + .map((item) => { + const actionTypeValue = item.actionType ?? item.action_type ?? 'filter' + const sourceKeyValue = item.sourceKey ?? item.source_key ?? item.key + const suggestedActionValue = item.suggestedAction ?? item.suggested_action ?? sourceKeyValue + const targetHealthValue = item.targetHealth ?? item.target_health ?? item.health ?? 'all' + + return { + ...item, + count: normalizeCount(item.count), + priority: normalizeCount(item.priority), + severity: normalizeInsightSeverity(item.severity), + actionTypeValue, + sourceKeyValue, + suggestedActionValue, + targetHealthValue + } + }) + + const buildFallbackViewSummary = ( + files: AdminFileViewItem[], + total: number, + rawSummary?: Partial + ): AdminFileViewSummaryViewData => { + const normalizedSummary = normalizeSummary(rawSummary, files, total) + const cards: AdminFileViewSummaryItem[] = [ + { + key: 'storage_issue', + severity: 'danger', + priority: 100, + count: normalizedSummary.storageIssueCount, + targetHealth: 'storage_issue' + }, + { + key: 'expired', + severity: normalizedSummary.expiredCount >= 10 ? 'danger' : 'warning', + priority: 90, + count: normalizedSummary.expiredCount, + targetHealth: 'expired' + }, + { + key: 'expiring_soon', + severity: 'warning', + priority: 80, + count: normalizedSummary.expiringSoonCount, + targetHealth: 'expiring_soon' + }, + { + key: 'never_retrieved', + severity: 'neutral', + priority: 60, + count: normalizedSummary.neverRetrievedCount, + targetHealth: 'never_retrieved' + } + ].filter((item) => item.count > 0) + + if (cards.length === 0) { + cards.push({ + key: normalizedSummary.totalFiles > 0 ? 'healthy' : 'empty', + severity: normalizedSummary.totalFiles > 0 ? 'success' : 'neutral', + priority: 10, + count: normalizedSummary.totalFiles, + targetHealth: normalizedSummary.totalFiles > 0 ? 'healthy' : 'all' + }) + } + + const normalizedCards = normalizeViewSummaryItems(cards) + return { + total, + filteredTotal: total, + allTotal: summary.value.totalFiles || total, + activeFilterCount: hasActiveFilters.value ? 1 : 0, + hasFilters: hasActiveFilters.value, + strongestSeverity: normalizedCards[0]?.severity ?? 'success', + summary: normalizedSummary, + cards: normalizedCards, + actions: normalizedCards.filter((item) => item.severity !== 'success') + } + } + + const normalizeViewSummary = ( + rawViewSummary: AdminFileViewSummary | undefined, + files: AdminFileViewItem[], + total: number, + rawSummary?: Partial + ): AdminFileViewSummaryViewData => { + if (!rawViewSummary) { + return buildFallbackViewSummary(files, total, rawSummary) + } + + const cards = normalizeViewSummaryItems(rawViewSummary.cards ?? rawViewSummary.items ?? []) + const actions = normalizeViewSummaryItems(rawViewSummary.actions ?? []) + const rawSummarySource = + rawViewSummary.summary ?? + rawViewSummary.healthSummary ?? + rawViewSummary.health_summary ?? + rawSummary + const filteredTotal = rawViewSummary.filteredTotal ?? rawViewSummary.filtered_total ?? total + const allTotal = rawViewSummary.allTotal ?? rawViewSummary.all_total ?? summary.value.totalFiles + const strongestSeverity = + rawViewSummary.strongestSeverity ?? + rawViewSummary.strongest_severity ?? + cards[0]?.severity ?? + 'success' + const normalizedSummary = normalizeSummary(rawSummarySource, files, total) + + return { + total: normalizeCount(rawViewSummary.total ?? total), + filteredTotal: normalizeCount(filteredTotal), + allTotal: normalizeCount(allTotal), + activeFilterCount: normalizeCount( + rawViewSummary.activeFilterCount ?? rawViewSummary.active_filter_count + ), + hasFilters: Boolean(rawViewSummary.hasFilters ?? rawViewSummary.has_filters), + strongestSeverity: normalizeInsightSeverity(strongestSeverity), + summary: normalizedSummary, + cards: cards.length > 0 ? cards : buildFallbackViewSummary(files, total, rawSummary).cards, + actions + } + } + const createFileViewItem = (file: FileListItem): AdminFileViewItem => { const isTextFile = inferIsText(file) const isExpiredFile = inferIsExpired(file) @@ -1100,6 +1242,17 @@ export function useAdminFiles() { tableData.value = res.detail.data.map(createFileViewItem) params.value.total = res.detail.total summary.value = normalizeSummary(res.detail.summary, tableData.value, res.detail.total) + viewSummary.value = normalizeViewSummary( + res.detail.viewSummary ?? + res.detail.view_summary ?? + res.detail.currentViewSummary ?? + res.detail.current_view_summary ?? + res.detail.actionSummary ?? + res.detail.action_summary, + tableData.value, + res.detail.total, + res.detail.summary + ) syncSelectedFilesWithCurrentPage() } catch (error) { hasLoadError.value = true @@ -1667,6 +1820,7 @@ export function useAdminFiles() { selectedViewPresetId, storageUsedText, summary, + viewSummary, showBatchEditModal, showEditModal, showFileDetailModal, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 5c248a1..5b50aab 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -576,6 +576,57 @@ export default { viewPresetDeleteSuccess: 'View deleted', viewPresetDeleteFailed: 'Failed to delete view', viewPresetLoadFailed: 'Failed to load saved views; using built-in views', + viewSummary: { + title: 'Current View Action Summary', + description: + 'Groups risks and next actions from the current filters. Click an item to open its queue.', + filteredTotal: 'Current results', + allTotal: 'All records', + activeFilters: 'Filters', + action: 'View queue', + reset: 'Clear filters', + severity: { + success: 'Stable', + warning: 'Action needed', + danger: 'High risk', + info: 'Info', + neutral: 'Watch' + }, + items: { + storage_issue: { + title: '{count} storage issues', + description: + 'Review records missing download metadata and verify storage paths, file names, and settings.' + }, + expired: { + title: '{count} expired items', + description: + 'Handle unavailable shares by deleting stale records or extending files that should remain available.' + }, + expiring_soon: { + title: '{count} expiring soon', + description: + 'These shares expire within 24 hours. Extend them early or notify recipients.' + }, + never_retrieved: { + title: '{count} never retrieved', + description: + 'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.' + }, + permanent: { + title: '{count} permanent shares', + description: 'Review long-retention shares to prevent unbounded historical growth.' + }, + healthy: { + title: 'Current view is stable', + description: 'No file in the current filters needs priority handling.' + }, + empty: { + title: 'No results in this view', + description: 'Adjust or clear filters to inspect other file queues.' + } + } + }, all: 'All', active: 'Available', expired: 'Expired', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 2dd0e81..f753dc1 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -559,6 +559,52 @@ export default { viewPresetDeleteSuccess: '视图已删除', viewPresetDeleteFailed: '视图删除失败', viewPresetLoadFailed: '视图预设加载失败,已使用内置视图', + viewSummary: { + title: '当前视图处置摘要', + description: '基于当前筛选结果聚合风险和下一步动作,点击可切换到对应队列。', + filteredTotal: '当前结果', + allTotal: '全部记录', + activeFilters: '筛选条件', + action: '查看队列', + reset: '清空筛选', + severity: { + success: '稳定', + warning: '需处理', + danger: '高风险', + info: '提示', + neutral: '观察' + }, + items: { + storage_issue: { + title: '存储异常 {count} 项', + description: '优先检查缺少下载元数据的记录,确认存储路径、文件名和配置。' + }, + expired: { + title: '已过期 {count} 项', + description: '集中处理已不可取件的分享,删除无效记录或续期仍需保留的文件。' + }, + expiring_soon: { + title: '即将过期 {count} 项', + description: '这些分享将在 24 小时内失效,适合提前续期或提醒使用方。' + }, + never_retrieved: { + title: '未取件 {count} 项', + description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。' + }, + permanent: { + title: '永久有效 {count} 项', + description: '复核长期保留的分享,避免历史文件无限增长。' + }, + healthy: { + title: '当前视图稳定', + description: '当前筛选结果未发现需要优先处理的文件。' + }, + empty: { + title: '当前视图无结果', + description: '调整筛选条件或清空筛选,查看其他文件队列。' + } + } + }, all: '全部', active: '可取件', expired: '已过期', diff --git a/src/types/file.ts b/src/types/file.ts index 50fc5a4..92bde02 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -102,6 +102,64 @@ export interface AdminFileListParams { sortOrder?: AdminFileSortOrder } +export interface AdminFileViewSummaryItem { + key: string + severity: AdminFileInsightSeverity | string + priority?: number + count: number + actionType?: string + action_type?: string + health?: AdminFileHealthFilter | string + targetHealth?: AdminFileHealthFilter | string + target_health?: AdminFileHealthFilter | string + suggestedAction?: string + suggested_action?: string + sourceKey?: string + source_key?: string +} + +export interface AdminFileViewSummary { + total?: number + filteredTotal?: number + filtered_total?: number + allTotal?: number + all_total?: number + activeFilterCount?: number + active_filter_count?: number + hasFilters?: boolean + has_filters?: boolean + strongestSeverity?: AdminFileInsightSeverity | string + strongest_severity?: AdminFileInsightSeverity | string + filters?: Record + summary?: Partial + healthSummary?: Partial + health_summary?: Partial + cards?: AdminFileViewSummaryItem[] + items?: AdminFileViewSummaryItem[] + actions?: AdminFileViewSummaryItem[] +} + +export interface AdminFileViewSummaryViewItem extends AdminFileViewSummaryItem { + severity: AdminFileInsightSeverity + priority: number + actionTypeValue: string + sourceKeyValue: string + suggestedActionValue: string + targetHealthValue: AdminFileHealthFilter | string +} + +export interface AdminFileViewSummaryViewData { + total: number + filteredTotal: number + allTotal: number + activeFilterCount: number + hasFilters: boolean + strongestSeverity: AdminFileInsightSeverity + summary: AdminFileSummary + cards: AdminFileViewSummaryViewItem[] + actions: AdminFileViewSummaryViewItem[] +} + export interface AdminFileViewPresetParams { keyword: string status: AdminFileStatusFilter @@ -189,6 +247,12 @@ export interface FileListResponse { page: number size: number summary?: AdminFileSummary + viewSummary?: AdminFileViewSummary + view_summary?: AdminFileViewSummary + currentViewSummary?: AdminFileViewSummary + current_view_summary?: AdminFileViewSummary + actionSummary?: AdminFileViewSummary + action_summary?: AdminFileViewSummary } export interface AdminFilePreviewResponse { diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 2fad4c3..74ed785 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -51,6 +51,83 @@ +
+
+
+
+
+ +
+
+

+ {{ t('fileManage.viewSummary.title') }} +

+

+ {{ t('fileManage.viewSummary.description') }} +

+
+
+
+ +
+
+

{{ metric.label }}

+

+ {{ metric.value }} +

+
+
+
+ +
+ +
+
+
@@ -1173,12 +1250,16 @@ import type { AdminFileSortOrder, AdminFileStatusFilter, AdminFileTypeFilter, + AdminFileViewSummaryViewItem, AdminFileViewItem } from '@/types' import { ActivityIcon, + AlertTriangleIcon, ArchiveIcon, CheckIcon, + CheckCircleIcon, + ChevronRightIcon, ClockIcon, CopyIcon, DownloadIcon, @@ -1192,6 +1273,7 @@ import { RefreshCwIcon, RotateCcwIcon, SearchIcon, + ShieldCheckIcon, TrashIcon, XIcon } from 'lucide-vue-next' @@ -1255,6 +1337,7 @@ const { selectedViewPresetId, storageUsedText, summary, + viewSummary, showBatchEditModal, showEditModal, showFileDetailModal, @@ -1484,6 +1567,21 @@ const summaryCards = computed(() => [ } ]) +const viewSummaryMetrics = computed(() => [ + { + label: t('fileManage.viewSummary.filteredTotal'), + value: viewSummary.value.filteredTotal + }, + { + label: t('fileManage.viewSummary.allTotal'), + value: viewSummary.value.allTotal + }, + { + label: t('fileManage.viewSummary.activeFilters'), + value: viewSummary.value.activeFilterCount + } +]) + const statusFilterOptions = computed<{ value: AdminFileStatusFilter; label: string }[]>(() => [ { value: 'all', label: t('fileManage.all') }, { value: 'active', label: t('fileManage.active') }, @@ -1524,10 +1622,78 @@ const healthFilterValues: AdminFileHealthFilter[] = [ 'permanent' ] +const isHealthFilterValue = (value: string): value is AdminFileHealthFilter => + healthFilterValues.includes(value as AdminFileHealthFilter) + +const getViewSummaryItemKey = (item: AdminFileViewSummaryViewItem) => + item.sourceKeyValue || item.key + +const getViewSummaryTitle = (item: AdminFileViewSummaryViewItem) => + t(`fileManage.viewSummary.items.${getViewSummaryItemKey(item)}.title`, { + count: item.count + }) + +const getViewSummaryDescription = (item: AdminFileViewSummaryViewItem) => + t(`fileManage.viewSummary.items.${getViewSummaryItemKey(item)}.description`, { + count: item.count + }) + +const getViewSummaryActionLabel = (item: AdminFileViewSummaryViewItem) => + item.targetHealthValue === 'all' + ? t('fileManage.viewSummary.reset') + : t('fileManage.viewSummary.action') + +const getViewSummarySeverityLabel = (item: AdminFileViewSummaryViewItem) => + t(`fileManage.viewSummary.severity.${item.severity}`) + +const getViewSummaryIcon = (item: AdminFileViewSummaryViewItem) => { + if (item.severity === 'success') return CheckCircleIcon + if (item.severity === 'danger' || item.severity === 'warning') return AlertTriangleIcon + return ShieldCheckIcon +} + +const getViewSummaryItemClass = (item: AdminFileViewSummaryViewItem) => { + const darkClasses: Record = { + success: 'border-emerald-500/20 bg-emerald-500/10 hover:border-emerald-500/40', + warning: 'border-amber-500/20 bg-amber-500/10 hover:border-amber-500/40', + danger: 'border-red-500/20 bg-red-500/10 hover:border-red-500/40', + info: 'border-blue-500/20 bg-blue-500/10 hover:border-blue-500/40', + neutral: 'border-gray-700 bg-gray-700/30 hover:border-gray-600' + } + const lightClasses: Record = { + success: 'border-emerald-100 bg-emerald-50 hover:border-emerald-200', + warning: 'border-amber-100 bg-amber-50 hover:border-amber-200', + danger: 'border-red-100 bg-red-50 hover:border-red-200', + info: 'border-blue-100 bg-blue-50 hover:border-blue-200', + neutral: 'border-gray-200 bg-gray-50 hover:border-gray-300' + } + + return isDarkMode.value ? darkClasses[item.severity] : lightClasses[item.severity] +} + +const getViewSummaryIconClass = (item: AdminFileViewSummaryViewItem) => { + const darkClasses: Record = { + success: 'bg-emerald-900/40 text-emerald-200', + warning: 'bg-amber-900/40 text-amber-200', + danger: 'bg-red-900/40 text-red-200', + info: 'bg-blue-900/40 text-blue-200', + neutral: 'bg-gray-800 text-gray-300' + } + const lightClasses: Record = { + success: 'bg-emerald-100 text-emerald-700', + warning: 'bg-amber-100 text-amber-700', + danger: 'bg-red-100 text-red-700', + info: 'bg-blue-100 text-blue-700', + neutral: 'bg-gray-100 text-gray-700' + } + + return isDarkMode.value ? darkClasses[item.severity] : lightClasses[item.severity] +} + const getRouteHealthFilter = (): AdminFileHealthFilter => { const health = route.query.health - if (typeof health === 'string' && healthFilterValues.includes(health as AdminFileHealthFilter)) { - return health as AdminFileHealthFilter + if (typeof health === 'string' && isHealthFilterValue(health)) { + return health } return 'all' } @@ -1552,6 +1718,16 @@ const handleResetFilters = async () => { await syncHealthFilterQuery('all') } +const openViewSummaryItem = async (item: AdminFileViewSummaryViewItem) => { + const health = String(item.targetHealthValue || 'all') + if (health !== 'all' && isHealthFilterValue(health)) { + await handleHealthFilterChange(health) + return + } + + await handleResetFilters() +} + const handleViewPresetChange = async (event: Event) => { const target = event.target as HTMLSelectElement if (!target.value) {