diff --git a/src/composables/useDashboardStats.ts b/src/composables/useDashboardStats.ts index 943a37e..8b42d87 100644 --- a/src/composables/useDashboardStats.ts +++ b/src/composables/useDashboardStats.ts @@ -1,6 +1,8 @@ import { computed, ref, reactive } from 'vue' import { StatsService } from '@/services' import type { + DashboardActivitiesParams, + DashboardActivityOption, DashboardActivity, DashboardActivityViewItem, DashboardData, @@ -11,8 +13,11 @@ import { formatFileSize, getErrorMessage } from '@/utils/common' type UseDashboardStatsOptions = { loadFailedMessage?: string + activityLoadFailedMessage?: string } +const activityTimelineLimit = 40 + const emptyDashboardData = (): DashboardViewData => ({ hasExtendedStats: false, totalFiles: 0, @@ -93,6 +98,17 @@ const normalizeRecentActivities = ( meta: activity.meta && typeof activity.meta === 'object' ? activity.meta : {} })) +const normalizeActivityOptions = ( + options: DashboardActivityOption[] = [] +): DashboardActivityOption[] => + options + .filter((option) => option && option.value) + .map((option) => ({ + value: option.value, + label: option.label || option.value, + count: toNumber(option.count) + })) + const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [ 'healthAttentionCount', 'healthDangerCount', @@ -126,9 +142,32 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { const isLoading = ref(false) const errorMessage = ref('') const lastUpdatedAt = ref(null) + const isActivityTimelineOpen = ref(false) + const isActivityTimelineLoading = ref(false) + const activityTimelineError = ref('') + const activityTimeline = ref([]) + const activityTimelineTotal = ref(0) + const activityTimelineStoredTotal = ref(0) + const activityActionOptions = ref([]) + const activityTargetTypeOptions = ref([]) + const activityFilters = reactive({ + action: '', + targetType: '', + keyword: '' + }) const lastUpdatedText = computed(() => lastUpdatedAt.value ? lastUpdatedAt.value.toLocaleString() : '-' ) + const hasActivityFilters = computed(() => + Boolean(activityFilters.action || activityFilters.targetType || activityFilters.keyword) + ) + + const buildActivityRequestParams = (): DashboardActivitiesParams => ({ + limit: activityTimelineLimit, + action: activityFilters.action || undefined, + targetType: activityFilters.targetType || undefined, + keyword: activityFilters.keyword.trim() || undefined + }) const fetchDashboardData = async () => { isLoading.value = true @@ -206,11 +245,93 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) { } } + const fetchActivityTimeline = async () => { + isActivityTimelineLoading.value = true + activityTimelineError.value = '' + + try { + const response = await StatsService.getActivities(buildActivityRequestParams()) + if (!response.detail) { + throw new Error('No activity data') + } + + const detail = response.detail + activityTimeline.value = normalizeRecentActivities(detail.activities ?? detail.items ?? []) + activityTimelineTotal.value = toNumber(detail.total) + activityTimelineStoredTotal.value = toNumber(detail.storedTotal ?? detail.stored_total) + activityActionOptions.value = normalizeActivityOptions( + detail.actionOptions ?? detail.action_options ?? [] + ) + activityTargetTypeOptions.value = normalizeActivityOptions( + detail.targetTypeOptions ?? detail.target_type_options ?? [] + ) + } catch (error) { + activityTimelineError.value = getErrorMessage( + error, + options.activityLoadFailedMessage || 'Failed to load activity timeline' + ) + } finally { + isActivityTimelineLoading.value = false + } + } + + const openActivityTimeline = async () => { + isActivityTimelineOpen.value = true + await fetchActivityTimeline() + } + + const closeActivityTimeline = () => { + isActivityTimelineOpen.value = false + } + + const setActivityActionFilter = async (action: string) => { + activityFilters.action = action + await fetchActivityTimeline() + } + + const setActivityTargetTypeFilter = async (targetType: string) => { + activityFilters.targetType = targetType + await fetchActivityTimeline() + } + + const setActivityKeywordFilter = (keyword: string) => { + activityFilters.keyword = keyword + } + + const applyActivityFilters = async () => { + await fetchActivityTimeline() + } + + const resetActivityFilters = async () => { + activityFilters.action = '' + activityFilters.targetType = '' + activityFilters.keyword = '' + await fetchActivityTimeline() + } + return { + activityActionOptions, + activityFilters, + activityTargetTypeOptions, + activityTimeline, + activityTimelineError, + activityTimelineStoredTotal, + activityTimelineTotal, + applyActivityFilters, + closeActivityTimeline, dashboardData, errorMessage, fetchDashboardData, + fetchActivityTimeline, + hasActivityFilters, + isActivityTimelineLoading, + isActivityTimelineOpen, isLoading, - lastUpdatedText + lastUpdatedText, + openActivityTimeline, + resetActivityFilters, + setActivityActionFilter, + setActivityKeywordFilter, + setActivityTargetTypeFilter } } diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 18684d6..76f62ea 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -90,6 +90,22 @@ export default { textType: 'Text', recentActivities: 'Recent Activity', recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + viewAllActivities: 'View all', + activityTimelineTitle: 'Activity Timeline', + activityTimelineFiltered: 'Current Results', + activityTimelineStoredTotal: 'Retained Records', + activityKeyword: 'Keyword', + activityKeywordPlaceholder: 'Search target, action, or ID', + activityActionFilter: 'Action', + activityAllActions: 'All actions', + activityTargetTypeFilter: 'Target type', + activityAllTargets: 'All targets', + activityResetFilters: 'Reset', + activityRefresh: 'Refresh', + activityApplyFilters: 'Apply', + activityLoading: 'Loading activity records...', + activityNoFiltered: 'No matching activity records', + activityLoadFailed: 'Failed to load activity timeline', noActivities: 'No activity yet', activityUnknownTarget: 'Unnamed target', activityDescription: 'Target: {target}', @@ -109,6 +125,13 @@ export default { 'local_file.delete': 'Deleted local file', 'local_file.share': 'Shared local file' }, + activityTargetTypes: { + file: 'File', + view_preset: 'View preset', + config: 'System settings', + local_file: 'Local file', + system: 'System' + }, recentFiles: 'Recent Shares', recentFilesDesc: 'Recently created share records for quick status checks.', available: 'Available', @@ -852,6 +875,22 @@ export default { textType: 'Text', recentActivities: 'Recent Activity', recentActivitiesDesc: 'Key admin changes for quick operational traceability.', + viewAllActivities: 'View all', + activityTimelineTitle: 'Activity Timeline', + activityTimelineFiltered: 'Current Results', + activityTimelineStoredTotal: 'Retained Records', + activityKeyword: 'Keyword', + activityKeywordPlaceholder: 'Search target, action, or ID', + activityActionFilter: 'Action', + activityAllActions: 'All actions', + activityTargetTypeFilter: 'Target type', + activityAllTargets: 'All targets', + activityResetFilters: 'Reset', + activityRefresh: 'Refresh', + activityApplyFilters: 'Apply', + activityLoading: 'Loading activity records...', + activityNoFiltered: 'No matching activity records', + activityLoadFailed: 'Failed to load activity timeline', noActivities: 'No activity yet', activityUnknownTarget: 'Unnamed target', activityDescription: 'Target: {target}', @@ -871,6 +910,13 @@ export default { 'local_file.delete': 'Deleted local file', 'local_file.share': 'Shared local file' }, + activityTargetTypes: { + file: 'File', + view_preset: 'View preset', + config: 'System settings', + local_file: 'Local file', + system: 'System' + }, 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 3864eb5..cee7f8a 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -90,6 +90,22 @@ export default { textType: '文本', recentActivities: '最近操作', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + viewAllActivities: '查看全部', + activityTimelineTitle: '操作时间线', + activityTimelineFiltered: '当前结果', + activityTimelineStoredTotal: '保留记录', + activityKeyword: '关键词', + activityKeywordPlaceholder: '搜索对象、动作或 ID', + activityActionFilter: '操作类型', + activityAllActions: '全部操作', + activityTargetTypeFilter: '对象类型', + activityAllTargets: '全部对象', + activityResetFilters: '重置', + activityRefresh: '刷新', + activityApplyFilters: '筛选', + activityLoading: '正在加载操作记录...', + activityNoFiltered: '没有匹配的操作记录', + activityLoadFailed: '操作时间线加载失败', noActivities: '暂无操作记录', activityUnknownTarget: '未命名对象', activityDescription: '对象:{target}', @@ -109,6 +125,13 @@ export default { 'local_file.delete': '删除本地文件', 'local_file.share': '分享本地文件' }, + activityTargetTypes: { + file: '文件', + view_preset: '视图预设', + config: '系统配置', + local_file: '本地文件', + system: '系统' + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', @@ -814,6 +837,22 @@ export default { textType: '文本', recentActivities: '最近操作', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', + viewAllActivities: '查看全部', + activityTimelineTitle: '操作时间线', + activityTimelineFiltered: '当前结果', + activityTimelineStoredTotal: '保留记录', + activityKeyword: '关键词', + activityKeywordPlaceholder: '搜索对象、动作或 ID', + activityActionFilter: '操作类型', + activityAllActions: '全部操作', + activityTargetTypeFilter: '对象类型', + activityAllTargets: '全部对象', + activityResetFilters: '重置', + activityRefresh: '刷新', + activityApplyFilters: '筛选', + activityLoading: '正在加载操作记录...', + activityNoFiltered: '没有匹配的操作记录', + activityLoadFailed: '操作时间线加载失败', noActivities: '暂无操作记录', activityUnknownTarget: '未命名对象', activityDescription: '对象:{target}', @@ -833,6 +872,13 @@ export default { 'local_file.delete': '删除本地文件', 'local_file.share': '分享本地文件' }, + activityTargetTypes: { + file: '文件', + view_preset: '视图预设', + config: '系统配置', + local_file: '本地文件', + system: '系统' + }, recentFiles: '最近分享', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', available: '可取件', diff --git a/src/services/stats.ts b/src/services/stats.ts index c275a12..0eabfd6 100644 --- a/src/services/stats.ts +++ b/src/services/stats.ts @@ -1,8 +1,19 @@ import api from './client' -import type { ApiResponse, DashboardData } from '@/types' +import type { + ApiResponse, + DashboardActivitiesParams, + DashboardActivitiesResponse, + DashboardData +} from '@/types' export class StatsService { static async getDashboard(): Promise> { return api.get('/admin/dashboard') } + + static async getActivities( + params: DashboardActivitiesParams + ): Promise> { + return api.get('/admin/activities', { params }) + } } diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index b308a83..e472ab3 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -85,6 +85,41 @@ export interface DashboardActivityViewItem extends DashboardActivity { createdAtValue: string | null } +export interface DashboardActivityOption { + value: string + label?: string + count: number +} + +export interface DashboardActivityFilters { + action?: string + targetType?: string + target_type?: string + keyword?: string +} + +export interface DashboardActivitiesParams { + limit?: number + action?: string + targetType?: string + target_type?: string + keyword?: string +} + +export interface DashboardActivitiesResponse { + activities?: DashboardActivity[] + items?: DashboardActivity[] + total?: number + storedTotal?: number + stored_total?: number + limit?: number + filters?: DashboardActivityFilters + actionOptions?: DashboardActivityOption[] + action_options?: DashboardActivityOption[] + targetTypeOptions?: DashboardActivityOption[] + target_type_options?: DashboardActivityOption[] +} + export type DashboardViewData = Omit< DashboardData, | keyof DashboardHealthSummary diff --git a/src/views/manage/DashboardView.vue b/src/views/manage/DashboardView.vue index 7a5cc0d..12842d7 100644 --- a/src/views/manage/DashboardView.vue +++ b/src/views/manage/DashboardView.vue @@ -265,10 +265,19 @@ {{ t('admin.dashboard.recentActivitiesDesc') }}

- +
+ + +
+
+
+
+

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

+ + {{ activityTimelineTotal }} + +
+
+

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

+ + {{ activityTimelineStoredTotal }} + +
+
+ +
+ + +
+ + + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+ {{ activityTimelineError }} +
+ +
+ + {{ t('admin.dashboard.activityLoading') }} +
+ +
+ {{ + hasActivityFilters + ? t('admin.dashboard.activityNoFiltered') + : t('admin.dashboard.noActivities') + }} +
+ +
+
+
+ + + +
+
+
+

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

+

+ {{ getActivityDescription(activity) }} +

+
+ + {{ getActivityTargetTypeTitle(activity.targetTypeValue) }} + +
+
+ {{ formatCreatedAt(activity.createdAtValue) }} + · {{ activity.count }} {{ t('common.files') }} + + · ID {{ activity.targetId ?? activity.target_id }} + +
+
+
+
+
+
+
+