From 350e2142aef079afd33cdfa9e5ad8c0d14898d74 Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 07:19:56 +0800 Subject: [PATCH] feat: add admin activity stream --- src/composables/useDashboardStats.ts | 26 +++++- src/i18n/locales/en-US.ts | 42 +++++++++ src/i18n/locales/zh-CN.ts | 42 +++++++++ src/types/dashboard.ts | 26 ++++++ src/views/manage/DashboardView.vue | 127 ++++++++++++++++++++++++++- 5 files changed, 260 insertions(+), 3 deletions(-) diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index 4aec9d6..943a37e 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -1,6 +1,12 @@ import { computed, ref, reactive } from 'vue' import { StatsService } from '@/services' -import type { DashboardData, DashboardHealthSummary, DashboardViewData } from '@/types' +import type { + DashboardActivity, + DashboardActivityViewItem, + DashboardData, + DashboardHealthSummary, + DashboardViewData +} from '@/types' import { formatFileSize, getErrorMessage } from '@/utils/common' type UseDashboardStatsOptions = { @@ -37,6 +43,7 @@ const emptyDashboardData = (): DashboardViewData => ({ permanentCount: 0, topSuffixes: [], recentFiles: [], + recentActivities: [], storageUsedText: '0 Bytes', yesterdaySizeText: '0 Bytes', todaySizeText: '0 Bytes', @@ -72,6 +79,20 @@ const normalizeRecentFiles = (recentFiles: DashboardViewData['recentFiles']) => usedCount: toNumber(file.usedCount) })) +const normalizeRecentActivities = ( + activities: DashboardActivity[] = [] +): DashboardActivityViewItem[] => + activities + .filter((activity) => activity && activity.id && activity.action) + .map((activity) => ({ + ...activity, + count: toNumber(activity.count) || 1, + targetTypeValue: activity.targetType ?? activity.target_type ?? 'system', + targetNameValue: activity.targetName ?? activity.target_name ?? '', + createdAtValue: activity.createdAt ?? activity.created_at ?? null, + meta: activity.meta && typeof activity.meta === 'object' ? activity.meta : {} + })) + const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [ 'healthAttentionCount', 'healthDangerCount', @@ -147,6 +168,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { }) dashboardData.topSuffixes = detail.topSuffixes || [] dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || []) + dashboardData.recentActivities = normalizeRecentActivities( + detail.recentActivities ?? detail.recent_activities ?? [] + ) dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed) dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize) diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 1bc0861..18684d6 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -88,6 +88,27 @@ export default { todayCapacityReference: 'Today Size / Single File Limit', fileTypeDistribution: 'Type Distribution', textType: 'Text', + recentActivities: 'Recent Activity', + recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + noActivities: 'No activity yet', + activityUnknownTarget: 'Unnamed target', + activityDescription: 'Target: {target}', + activityBatchDescription: 'Target: {target}, {count} items', + activityActions: { + 'file.delete': 'Deleted file', + 'files.batch_delete': 'Batch deleted files', + 'file.update': 'Edited file', + 'files.batch_update': 'Batch updated files', + 'file.policy_action': 'Updated file policy', + 'files.batch_policy_action': 'Batch updated policies', + 'file.metadata_update': 'Updated operations notes', + 'file.view_preset_create': 'Created view preset', + 'file.view_preset_update': 'Updated view preset', + 'file.view_preset_delete': 'Deleted view preset', + 'config.update': 'Updated system settings', + 'local_file.delete': 'Deleted local file', + 'local_file.share': 'Shared local file' + }, recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', @@ -829,6 +850,27 @@ export default { todayCapacityReference: 'Today Size / Single File Limit', fileTypeDistribution: 'Type Distribution', textType: 'Text', + recentActivities: 'Recent Activity', + recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + noActivities: 'No activity yet', + activityUnknownTarget: 'Unnamed target', + activityDescription: 'Target: {target}', + activityBatchDescription: 'Target: {target}, {count} items', + activityActions: { + 'file.delete': 'Deleted file', + 'files.batch_delete': 'Batch deleted files', + 'file.update': 'Edited file', + 'files.batch_update': 'Batch updated files', + 'file.policy_action': 'Updated file policy', + 'files.batch_policy_action': 'Batch updated policies', + 'file.metadata_update': 'Updated operations notes', + 'file.view_preset_create': 'Created view preset', + 'file.view_preset_update': 'Updated view preset', + 'file.view_preset_delete': 'Deleted view preset', + 'config.update': 'Updated system settings', + 'local_file.delete': 'Deleted local file', + 'local_file.share': 'Shared local file' + }, recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index f755645..3864eb5 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -88,6 +88,27 @@ export default { todayCapacityReference: '今日容量 / 单文件上限', fileTypeDistribution: '类型分布', textType: '文本', + recentActivities: '最近操作', + recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + noActivities: '暂无操作记录', + activityUnknownTarget: '未命名对象', + activityDescription: '对象:{target}', + activityBatchDescription: '对象:{target},共 {count} 项', + activityActions: { + 'file.delete': '删除文件', + 'files.batch_delete': '批量删除文件', + 'file.update': '编辑文件', + 'files.batch_update': '批量更新文件', + 'file.policy_action': '更新文件策略', + 'files.batch_policy_action': '批量更新策略', + 'file.metadata_update': '更新运营备注', + 'file.view_preset_create': '创建视图预设', + 'file.view_preset_update': '覆盖视图预设', + 'file.view_preset_delete': '删除视图预设', + 'config.update': '更新系统配置', + 'local_file.delete': '删除本地文件', + 'local_file.share': '分享本地文件' + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', @@ -791,6 +812,27 @@ export default { todayCapacityReference: '今日容量 / 单文件上限', fileTypeDistribution: '类型分布', textType: '文本', + recentActivities: '最近操作', + recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + noActivities: '暂无操作记录', + activityUnknownTarget: '未命名对象', + activityDescription: '对象:{target}', + activityBatchDescription: '对象:{target},共 {count} 项', + activityActions: { + 'file.delete': '删除文件', + 'files.batch_delete': '批量删除文件', + 'file.update': '编辑文件', + 'files.batch_update': '批量更新文件', + 'file.policy_action': '更新文件策略', + 'files.batch_policy_action': '批量更新策略', + 'file.metadata_update': '更新运营备注', + 'file.view_preset_create': '创建视图预设', + 'file.view_preset_update': '覆盖视图预设', + 'file.view_preset_delete': '删除视图预设', + 'config.update': '更新系统配置', + 'local_file.delete': '删除本地文件', + 'local_file.share': '分享本地文件' + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index 5043372..b308a83 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -41,6 +41,8 @@ export interface DashboardData { healthSummary?: Partial topSuffixes?: DashboardSuffixStat[] recentFiles?: DashboardRecentFile[] + recentActivities?: DashboardActivity[] + recent_activities?: DashboardActivity[] } export interface DashboardSuffixStat { @@ -62,6 +64,27 @@ export interface DashboardRecentFile { isExpired: boolean } +export interface DashboardActivity { + id: string + action: string + targetType?: string + target_type?: string + targetId?: string | number | null + target_id?: string | number | null + targetName?: string + target_name?: string + count: number + meta?: Record + createdAt?: string | null + created_at?: string | null +} + +export interface DashboardActivityViewItem extends DashboardActivity { + targetTypeValue: string + targetNameValue: string + createdAtValue: string | null +} + export type DashboardViewData = Omit< DashboardData, | keyof DashboardHealthSummary @@ -78,6 +101,8 @@ export type DashboardViewData = Omit< | 'maxSaveSeconds' | 'topSuffixes' | 'recentFiles' + | 'recentActivities' + | 'recent_activities' | 'storageUsed' | 'yesterdaySize' | 'todaySize' @@ -97,6 +122,7 @@ export type DashboardViewData = Omit< maxSaveSeconds: number topSuffixes: DashboardSuffixStat[] recentFiles: DashboardRecentFile[] + recentActivities: DashboardActivityViewItem[] storageUsed: number yesterdaySize: number todaySize: number diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index 8fabbd0..7a5cc0d 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -222,7 +222,7 @@ -
+

{{ t('admin.dashboard.fileTypeDistribution') }} @@ -255,6 +255,59 @@

+
+
+
+

+ {{ t('admin.dashboard.recentActivities') }} +

+

+ {{ t('admin.dashboard.recentActivitiesDesc') }} +

+
+ +
+ +
+ {{ t('admin.dashboard.noActivities') }} +
+
+
+
+ + + +
+

+ {{ getActivityTitle(activity.action) }} +

+

+ {{ getActivityDescription(activity) }} +

+

+ {{ formatCreatedAt(activity.createdAtValue) }} +

+
+
+
+
+
+
@@ -369,13 +422,18 @@ import { AlertTriangleIcon, ArrowRightIcon, CheckCircleIcon, + ClockIcon, DownloadCloudIcon, FileIcon, FilesIcon, FileTextIcon, HardDriveIcon, + HistoryIcon, + PencilIcon, RefreshCwIcon, ShieldCheckIcon, + SlidersHorizontalIcon, + TrashIcon, UploadCloudIcon } from 'lucide-vue-next' import StatCard from '@/components/common/StatCard.vue' @@ -383,7 +441,7 @@ import { useDashboardStats, useInjectedDarkMode } from '@/composables' import { useI18n } from 'vue-i18n' import { formatFileSize, formatTimestamp } from '@/utils/common' import { ROUTES } from '@/constants' -import type { DashboardHealthAction } from '@/types' +import type { DashboardActivityViewItem, DashboardHealthAction } from '@/types' const isDarkMode = useInjectedDarkMode() const { t } = useI18n() @@ -463,6 +521,71 @@ const formatCreatedAt = (value: string | null) => { return formatTimestamp(value, 'datetime') } +type ActivityTone = 'danger' | 'warning' | 'success' | 'neutral' + +const activityIconMap: Record = { + 'file.delete': TrashIcon, + 'files.batch_delete': TrashIcon, + 'file.update': PencilIcon, + 'files.batch_update': PencilIcon, + 'file.policy_action': ClockIcon, + 'files.batch_policy_action': ClockIcon, + 'file.metadata_update': FileTextIcon, + 'file.view_preset_create': SlidersHorizontalIcon, + 'file.view_preset_update': SlidersHorizontalIcon, + 'file.view_preset_delete': SlidersHorizontalIcon, + 'config.update': SlidersHorizontalIcon, + 'local_file.delete': TrashIcon, + 'local_file.share': UploadCloudIcon +} + +const getActivityIcon = (action: string) => activityIconMap[action] || HistoryIcon + +const getActivityTone = (action: string): ActivityTone => { + if (action.includes('delete')) return 'danger' + if (action.includes('policy') || action.includes('update')) return 'warning' + if (action.includes('share') || action.includes('create')) return 'success' + return 'neutral' +} + +const getActivityIconClass = (action: string) => { + const tone = getActivityTone(action) + const darkClasses: Record = { + danger: 'bg-red-500/10 text-red-300', + warning: 'bg-amber-500/10 text-amber-300', + success: 'bg-emerald-500/10 text-emerald-300', + neutral: 'bg-gray-700 text-gray-300' + } + const lightClasses: Record = { + danger: 'bg-red-50 text-red-700', + warning: 'bg-amber-50 text-amber-700', + success: 'bg-emerald-50 text-emerald-700', + neutral: 'bg-gray-100 text-gray-700' + } + + return isDarkMode.value ? darkClasses[tone] : lightClasses[tone] +} + +const getActivityTitle = (action: string) => { + const key = `admin.dashboard.activityActions.${action}` + const title = t(key) + return title === key ? action : title +} + +const getActivityDescription = (activity: DashboardActivityViewItem) => { + const targetName = activity.targetNameValue || t('admin.dashboard.activityUnknownTarget') + if (activity.count > 1) { + return t('admin.dashboard.activityBatchDescription', { + target: targetName, + count: activity.count + }) + } + + return t('admin.dashboard.activityDescription', { + target: targetName + }) +} + const healthActionIconMap: Record = { danger: AlertTriangleIcon, warning: AlertTriangleIcon,