feat: add admin activity timeline filters

This commit is contained in:
Lan
2026-06-03 07:36:48 +08:00
parent bb65c11826
commit f2bf124447
6 changed files with 555 additions and 10 deletions
+122 -1
View File
@@ -1,6 +1,8 @@
import { computed, ref, reactive } from 'vue' import { computed, ref, reactive } from 'vue'
import { StatsService } from '@/services' import { StatsService } from '@/services'
import type { import type {
DashboardActivitiesParams,
DashboardActivityOption,
DashboardActivity, DashboardActivity,
DashboardActivityViewItem, DashboardActivityViewItem,
DashboardData, DashboardData,
@@ -11,8 +13,11 @@ import { formatFileSize, getErrorMessage } from '@/utils/common'
type UseDashboardStatsOptions = { type UseDashboardStatsOptions = {
loadFailedMessage?: string loadFailedMessage?: string
activityLoadFailedMessage?: string
} }
const activityTimelineLimit = 40
const emptyDashboardData = (): DashboardViewData => ({ const emptyDashboardData = (): DashboardViewData => ({
hasExtendedStats: false, hasExtendedStats: false,
totalFiles: 0, totalFiles: 0,
@@ -93,6 +98,17 @@ const normalizeRecentActivities = (
meta: activity.meta && typeof activity.meta === 'object' ? activity.meta : {} 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)[] = [ const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
'healthAttentionCount', 'healthAttentionCount',
'healthDangerCount', 'healthDangerCount',
@@ -126,9 +142,32 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
const isLoading = ref(false) const isLoading = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const lastUpdatedAt = ref<Date | null>(null) const lastUpdatedAt = ref<Date | null>(null)
const isActivityTimelineOpen = ref(false)
const isActivityTimelineLoading = ref(false)
const activityTimelineError = ref('')
const activityTimeline = ref<DashboardActivityViewItem[]>([])
const activityTimelineTotal = ref(0)
const activityTimelineStoredTotal = ref(0)
const activityActionOptions = ref<DashboardActivityOption[]>([])
const activityTargetTypeOptions = ref<DashboardActivityOption[]>([])
const activityFilters = reactive({
action: '',
targetType: '',
keyword: ''
})
const lastUpdatedText = computed(() => const lastUpdatedText = computed(() =>
lastUpdatedAt.value ? lastUpdatedAt.value.toLocaleString() : '-' 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 () => { const fetchDashboardData = async () => {
isLoading.value = true 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 { return {
activityActionOptions,
activityFilters,
activityTargetTypeOptions,
activityTimeline,
activityTimelineError,
activityTimelineStoredTotal,
activityTimelineTotal,
applyActivityFilters,
closeActivityTimeline,
dashboardData, dashboardData,
errorMessage, errorMessage,
fetchDashboardData, fetchDashboardData,
fetchActivityTimeline,
hasActivityFilters,
isActivityTimelineLoading,
isActivityTimelineOpen,
isLoading, isLoading,
lastUpdatedText lastUpdatedText,
openActivityTimeline,
resetActivityFilters,
setActivityActionFilter,
setActivityKeywordFilter,
setActivityTargetTypeFilter
} }
} }
+46
View File
@@ -90,6 +90,22 @@ export default {
textType: 'Text', textType: 'Text',
recentActivities: 'Recent Activity', recentActivities: 'Recent Activity',
recentActivitiesDesc: 'Key admin changes for quick operational traceability.', 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', noActivities: 'No activity yet',
activityUnknownTarget: 'Unnamed target', activityUnknownTarget: 'Unnamed target',
activityDescription: 'Target: {target}', activityDescription: 'Target: {target}',
@@ -109,6 +125,13 @@ export default {
'local_file.delete': 'Deleted local file', 'local_file.delete': 'Deleted local file',
'local_file.share': 'Shared 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', recentFiles: 'Recent Shares',
recentFilesDesc: 'Recently created share records for quick status checks.', recentFilesDesc: 'Recently created share records for quick status checks.',
available: 'Available', available: 'Available',
@@ -852,6 +875,22 @@ export default {
textType: 'Text', textType: 'Text',
recentActivities: 'Recent Activity', recentActivities: 'Recent Activity',
recentActivitiesDesc: 'Key admin changes for quick operational traceability.', 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', noActivities: 'No activity yet',
activityUnknownTarget: 'Unnamed target', activityUnknownTarget: 'Unnamed target',
activityDescription: 'Target: {target}', activityDescription: 'Target: {target}',
@@ -871,6 +910,13 @@ export default {
'local_file.delete': 'Deleted local file', 'local_file.delete': 'Deleted local file',
'local_file.share': 'Shared 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', recentFiles: 'Recent Shares',
recentFilesDesc: 'Recently created share records for quick status checks.', recentFilesDesc: 'Recently created share records for quick status checks.',
available: 'Available', available: 'Available',
+46
View File
@@ -90,6 +90,22 @@ export default {
textType: '文本', textType: '文本',
recentActivities: '最近操作', recentActivities: '最近操作',
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
viewAllActivities: '查看全部',
activityTimelineTitle: '操作时间线',
activityTimelineFiltered: '当前结果',
activityTimelineStoredTotal: '保留记录',
activityKeyword: '关键词',
activityKeywordPlaceholder: '搜索对象、动作或 ID',
activityActionFilter: '操作类型',
activityAllActions: '全部操作',
activityTargetTypeFilter: '对象类型',
activityAllTargets: '全部对象',
activityResetFilters: '重置',
activityRefresh: '刷新',
activityApplyFilters: '筛选',
activityLoading: '正在加载操作记录...',
activityNoFiltered: '没有匹配的操作记录',
activityLoadFailed: '操作时间线加载失败',
noActivities: '暂无操作记录', noActivities: '暂无操作记录',
activityUnknownTarget: '未命名对象', activityUnknownTarget: '未命名对象',
activityDescription: '对象:{target}', activityDescription: '对象:{target}',
@@ -109,6 +125,13 @@ export default {
'local_file.delete': '删除本地文件', 'local_file.delete': '删除本地文件',
'local_file.share': '分享本地文件' 'local_file.share': '分享本地文件'
}, },
activityTargetTypes: {
file: '文件',
view_preset: '视图预设',
config: '系统配置',
local_file: '本地文件',
system: '系统'
},
recentFiles: '最近分享', recentFiles: '最近分享',
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
available: '可取件', available: '可取件',
@@ -814,6 +837,22 @@ export default {
textType: '文本', textType: '文本',
recentActivities: '最近操作', recentActivities: '最近操作',
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。', recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
viewAllActivities: '查看全部',
activityTimelineTitle: '操作时间线',
activityTimelineFiltered: '当前结果',
activityTimelineStoredTotal: '保留记录',
activityKeyword: '关键词',
activityKeywordPlaceholder: '搜索对象、动作或 ID',
activityActionFilter: '操作类型',
activityAllActions: '全部操作',
activityTargetTypeFilter: '对象类型',
activityAllTargets: '全部对象',
activityResetFilters: '重置',
activityRefresh: '刷新',
activityApplyFilters: '筛选',
activityLoading: '正在加载操作记录...',
activityNoFiltered: '没有匹配的操作记录',
activityLoadFailed: '操作时间线加载失败',
noActivities: '暂无操作记录', noActivities: '暂无操作记录',
activityUnknownTarget: '未命名对象', activityUnknownTarget: '未命名对象',
activityDescription: '对象:{target}', activityDescription: '对象:{target}',
@@ -833,6 +872,13 @@ export default {
'local_file.delete': '删除本地文件', 'local_file.delete': '删除本地文件',
'local_file.share': '分享本地文件' 'local_file.share': '分享本地文件'
}, },
activityTargetTypes: {
file: '文件',
view_preset: '视图预设',
config: '系统配置',
local_file: '本地文件',
system: '系统'
},
recentFiles: '最近分享', recentFiles: '最近分享',
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。', recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
available: '可取件', available: '可取件',
+12 -1
View File
@@ -1,8 +1,19 @@
import api from './client' import api from './client'
import type { ApiResponse, DashboardData } from '@/types' import type {
ApiResponse,
DashboardActivitiesParams,
DashboardActivitiesResponse,
DashboardData
} from '@/types'
export class StatsService { export class StatsService {
static async getDashboard(): Promise<ApiResponse<DashboardData>> { static async getDashboard(): Promise<ApiResponse<DashboardData>> {
return api.get('/admin/dashboard') return api.get('/admin/dashboard')
} }
static async getActivities(
params: DashboardActivitiesParams
): Promise<ApiResponse<DashboardActivitiesResponse>> {
return api.get('/admin/activities', { params })
}
} }
+35
View File
@@ -85,6 +85,41 @@ export interface DashboardActivityViewItem extends DashboardActivity {
createdAtValue: string | null 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< export type DashboardViewData = Omit<
DashboardData, DashboardData,
| keyof DashboardHealthSummary | keyof DashboardHealthSummary
+293 -7
View File
@@ -265,10 +265,19 @@
{{ t('admin.dashboard.recentActivitiesDesc') }} {{ t('admin.dashboard.recentActivitiesDesc') }}
</p> </p>
</div> </div>
<HistoryIcon <button
class="h-5 w-5 shrink-0" type="button"
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-500']" class="inline-flex shrink-0 items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium transition-colors"
/> :class="[
isDarkMode
? 'bg-gray-900 text-indigo-200 hover:bg-gray-700'
: 'bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
]"
@click="handleOpenActivityTimeline"
>
<HistoryIcon class="h-4 w-4" />
{{ t('admin.dashboard.viewAllActivities') }}
</button>
</div> </div>
<div <div
@@ -411,6 +420,237 @@
</section> </section>
</div> </div>
</div> </div>
<SideDrawer
:visible="isActivityTimelineOpen"
:title="t('admin.dashboard.activityTimelineTitle')"
@close="closeActivityTimeline"
>
<div class="flex min-h-0 flex-1 flex-col">
<div class="border-b p-5" :class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']">
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg border p-3" :class="[subtlePanelClass]">
<p class="text-xs" :class="[mutedTextClass]">
{{ t('admin.dashboard.activityTimelineFiltered') }}
</p>
<strong class="mt-1 block text-2xl" :class="[primaryTextClass]">
{{ activityTimelineTotal }}
</strong>
</div>
<div class="rounded-lg border p-3" :class="[subtlePanelClass]">
<p class="text-xs" :class="[mutedTextClass]">
{{ t('admin.dashboard.activityTimelineStoredTotal') }}
</p>
<strong class="mt-1 block text-2xl" :class="[primaryTextClass]">
{{ activityTimelineStoredTotal }}
</strong>
</div>
</div>
<form class="mt-4 space-y-3" @submit.prevent="handleApplyActivityFilters">
<label class="block">
<span class="mb-1 block text-xs font-medium" :class="[mutedTextClass]">
{{ t('admin.dashboard.activityKeyword') }}
</span>
<span class="relative block">
<SearchIcon
class="pointer-events-none absolute left-3 top-2.5 h-4 w-4"
:class="[mutedTextClass]"
/>
<input
v-model="activityFilters.keyword"
type="search"
class="w-full rounded-lg border py-2 pl-9 pr-3 text-sm outline-none transition-colors"
:class="[
isDarkMode
? 'border-gray-700 bg-gray-900 text-gray-100 placeholder:text-gray-500 focus:border-indigo-400'
: 'border-gray-200 bg-white text-gray-900 placeholder:text-gray-400 focus:border-indigo-400'
]"
:placeholder="t('admin.dashboard.activityKeywordPlaceholder')"
/>
</span>
</label>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label class="block">
<span class="mb-1 block text-xs font-medium" :class="[mutedTextClass]">
{{ t('admin.dashboard.activityActionFilter') }}
</span>
<select
v-model="activityFilters.action"
class="w-full rounded-lg border px-3 py-2 text-sm outline-none transition-colors"
:class="[
isDarkMode
? 'border-gray-700 bg-gray-900 text-gray-100 focus:border-indigo-400'
: 'border-gray-200 bg-white text-gray-900 focus:border-indigo-400'
]"
>
<option value="">{{ t('admin.dashboard.activityAllActions') }}</option>
<option
v-for="option in activityActionOptions"
:key="option.value"
:value="option.value"
>
{{ getActivityTitle(option.value) }} ({{ option.count }})
</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-xs font-medium" :class="[mutedTextClass]">
{{ t('admin.dashboard.activityTargetTypeFilter') }}
</span>
<select
v-model="activityFilters.targetType"
class="w-full rounded-lg border px-3 py-2 text-sm outline-none transition-colors"
:class="[
isDarkMode
? 'border-gray-700 bg-gray-900 text-gray-100 focus:border-indigo-400'
: 'border-gray-200 bg-white text-gray-900 focus:border-indigo-400'
]"
>
<option value="">{{ t('admin.dashboard.activityAllTargets') }}</option>
<option
v-for="option in activityTargetTypeOptions"
:key="option.value"
:value="option.value"
>
{{ getActivityTargetTypeTitle(option.value) }} ({{ option.count }})
</option>
</select>
</label>
</div>
<div class="flex flex-wrap items-center justify-between gap-2">
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60"
:class="[
isDarkMode
? 'bg-gray-800 text-gray-200 hover:bg-gray-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
:disabled="!hasActivityFilters || isActivityTimelineLoading"
@click="handleResetActivityFilters"
>
{{ t('admin.dashboard.activityResetFilters') }}
</button>
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60"
:class="[
isDarkMode
? 'bg-gray-800 text-gray-200 hover:bg-gray-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
:disabled="isActivityTimelineLoading"
@click="handleRefreshActivityTimeline"
>
<RefreshCwIcon
class="h-4 w-4"
:class="{ 'animate-spin': isActivityTimelineLoading }"
/>
{{ t('admin.dashboard.activityRefresh') }}
</button>
<button
type="submit"
class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="isActivityTimelineLoading"
>
<ListFilterIcon class="h-4 w-4" />
{{ t('admin.dashboard.activityApplyFilters') }}
</button>
</div>
</div>
</form>
</div>
<div class="min-h-0 flex-1 overflow-y-auto p-5 custom-scrollbar">
<div
v-if="activityTimelineError"
class="mb-4 rounded-lg border px-4 py-3 text-sm"
:class="[
isDarkMode
? 'border-red-900/50 bg-red-950/30 text-red-200'
: 'border-red-200 bg-red-50 text-red-700'
]"
>
{{ activityTimelineError }}
</div>
<div
v-if="isActivityTimelineLoading"
class="flex items-center justify-center gap-2 rounded-lg border px-4 py-8 text-sm"
:class="[subtlePanelClass, mutedTextClass]"
>
<RefreshCwIcon class="h-4 w-4 animate-spin" />
{{ t('admin.dashboard.activityLoading') }}
</div>
<div
v-else-if="activityTimeline.length === 0"
class="rounded-lg border px-4 py-8 text-center text-sm"
:class="[subtlePanelClass, mutedTextClass]"
>
{{
hasActivityFilters
? t('admin.dashboard.activityNoFiltered')
: t('admin.dashboard.noActivities')
}}
</div>
<div v-else class="space-y-4">
<div
v-for="activity in activityTimeline"
:key="activity.id"
class="rounded-lg border p-4"
:class="[subtlePanelClass]"
>
<div class="flex items-start gap-3">
<span
class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
:class="getActivityIconClass(activity.action)"
>
<component :is="getActivityIcon(activity.action)" class="h-4 w-4" />
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0">
<p class="text-sm font-semibold" :class="[primaryTextClass]">
{{ getActivityTitle(activity.action) }}
</p>
<p class="mt-1 text-sm" :class="[mutedTextClass]">
{{ getActivityDescription(activity) }}
</p>
</div>
<span
class="inline-flex shrink-0 rounded-full px-2.5 py-1 text-xs font-medium"
:class="[isDarkMode ? 'bg-gray-800 text-gray-300' : 'bg-white text-gray-600']"
>
{{ getActivityTargetTypeTitle(activity.targetTypeValue) }}
</span>
</div>
<div
class="mt-3 flex flex-wrap items-center gap-2 text-xs"
:class="[mutedTextClass]"
>
<span>{{ formatCreatedAt(activity.createdAtValue) }}</span>
<span v-if="activity.count > 1"
>· {{ activity.count }} {{ t('common.files') }}</span
>
<span v-if="activity.targetId ?? activity.target_id">
· ID {{ activity.targetId ?? activity.target_id }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</SideDrawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -429,14 +669,17 @@ import {
FileTextIcon, FileTextIcon,
HardDriveIcon, HardDriveIcon,
HistoryIcon, HistoryIcon,
ListFilterIcon,
PencilIcon, PencilIcon,
RefreshCwIcon, RefreshCwIcon,
SearchIcon,
ShieldCheckIcon, ShieldCheckIcon,
SlidersHorizontalIcon, SlidersHorizontalIcon,
TrashIcon, TrashIcon,
UploadCloudIcon UploadCloudIcon
} from 'lucide-vue-next' } from 'lucide-vue-next'
import StatCard from '@/components/common/StatCard.vue' import StatCard from '@/components/common/StatCard.vue'
import SideDrawer from '@/components/common/SideDrawer.vue'
import { useDashboardStats, useInjectedDarkMode } from '@/composables' import { useDashboardStats, useInjectedDarkMode } from '@/composables'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { formatFileSize, formatTimestamp } from '@/utils/common' import { formatFileSize, formatTimestamp } from '@/utils/common'
@@ -446,10 +689,47 @@ import type { DashboardActivityViewItem, DashboardHealthAction } from '@/types'
const isDarkMode = useInjectedDarkMode() const isDarkMode = useInjectedDarkMode()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedText } = const {
useDashboardStats({ activityActionOptions,
activityFilters,
activityTargetTypeOptions,
activityTimeline,
activityTimelineError,
activityTimelineStoredTotal,
activityTimelineTotal,
applyActivityFilters,
closeActivityTimeline,
dashboardData,
errorMessage,
fetchActivityTimeline,
fetchDashboardData,
hasActivityFilters,
isActivityTimelineLoading,
isActivityTimelineOpen,
isLoading,
lastUpdatedText,
openActivityTimeline,
resetActivityFilters
} = useDashboardStats({
activityLoadFailedMessage: t('admin.dashboard.activityLoadFailed'),
loadFailedMessage: t('admin.dashboard.loadFailed') loadFailedMessage: t('admin.dashboard.loadFailed')
}) })
const handleOpenActivityTimeline = () => {
void openActivityTimeline()
}
const handleApplyActivityFilters = () => {
void applyActivityFilters()
}
const handleResetActivityFilters = () => {
void resetActivityFilters()
}
const handleRefreshActivityTimeline = () => {
void fetchActivityTimeline()
}
const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900')) const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900'))
const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500')) const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500'))
@@ -572,6 +852,12 @@ const getActivityTitle = (action: string) => {
return title === key ? action : title return title === key ? action : title
} }
const getActivityTargetTypeTitle = (targetType: string) => {
const key = `admin.dashboard.activityTargetTypes.${targetType}`
const title = t(key)
return title === key ? targetType : title
}
const getActivityDescription = (activity: DashboardActivityViewItem) => { const getActivityDescription = (activity: DashboardActivityViewItem) => {
const targetName = activity.targetNameValue || t('admin.dashboard.activityUnknownTarget') const targetName = activity.targetNameValue || t('admin.dashboard.activityUnknownTarget')
if (activity.count > 1) { if (activity.count > 1) {