Add admin analytics trend views

This commit is contained in:
2026-06-05 11:13:50 +08:00
parent a58fc43d94
commit 105efc40d5
8 changed files with 869 additions and 45 deletions
+197 -6
View File
@@ -34,8 +34,8 @@
<div
v-for="card in summaryCards"
:key="card.label"
class="rounded-lg border p-4"
:class="[panelClass]"
class="group rounded-lg border p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
:class="[panelClass, cardHoverClass]"
>
<div class="flex items-start justify-between gap-3">
<div>
@@ -44,7 +44,10 @@
{{ card.value }}
</p>
</div>
<div class="rounded-lg p-2" :class="[card.iconClass]">
<div
class="rounded-lg p-2 transition-transform duration-200 group-hover:scale-105"
:class="[card.iconClass]"
>
<component :is="card.icon" class="h-5 w-5" />
</div>
</div>
@@ -52,7 +55,22 @@
</div>
<section class="mb-6 rounded-lg border p-4" :class="[panelClass]">
<div class="grid gap-4">
<div class="flex items-center justify-between gap-3">
<div>
<h3 class="text-base font-semibold" :class="[primaryTextClass]">条件筛选</h3>
<p class="mt-1 text-xs" :class="[mutedTextClass]">
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
</p>
</div>
<BaseButton variant="secondary" @click="isFilterPanelOpen = !isFilterPanelOpen">
<template #icon>
<FilterIcon class="mr-2 h-4 w-4" />
</template>
{{ filterBodyVisible ? '收起筛选' : '展开筛选' }}
</BaseButton>
</div>
<div v-show="filterBodyVisible" class="mt-4 grid gap-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center">
<label class="min-w-0 flex-1">
<span class="sr-only">{{ t('fileManage.searchPlaceholder') }}</span>
@@ -556,10 +574,146 @@
:page-size="params.size"
:total="params.total"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</template>
</DataTable>
<section class="mt-6 rounded-lg border p-4" :class="[panelClass]">
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">历史文件</h3>
<p class="text-sm" :class="[mutedTextClass]">包含已删除已过期和现存文件的下载统计</p>
</div>
<BaseButton variant="secondary" :loading="isHistoryLoading" @click="loadHistoryFiles">
<template #icon>
<RefreshCwIcon class="mr-2 h-4 w-4" />
</template>
刷新历史
</BaseButton>
</div>
<div class="overflow-x-auto">
<table
class="min-w-full divide-y"
:class="[isDarkMode ? 'divide-gray-700' : 'divide-gray-200']"
>
<thead>
<tr>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
文件
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
大小
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
取件码
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
上传时间
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
过期时间
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
下载
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
流量
</th>
<th
class="px-4 py-3 text-left text-xs font-semibold uppercase"
:class="[mutedTextClass]"
>
状态
</th>
</tr>
</thead>
<tbody class="divide-y" :class="[isDarkMode ? 'divide-gray-700' : 'divide-gray-100']">
<tr v-if="isHistoryLoading">
<td colspan="8" class="px-4 py-8 text-center text-sm" :class="[mutedTextClass]">
<RefreshCwIcon class="mr-2 inline h-4 w-4 animate-spin" />
{{ t('common.loading') }}
</td>
</tr>
<tr v-else-if="historyPageRows.length === 0">
<td colspan="8" class="px-4 py-8 text-center text-sm" :class="[mutedTextClass]">
暂无历史文件
</td>
</tr>
<template v-else>
<tr
v-for="file in historyPageRows"
:key="file.code"
:class="[
file.current ? '' : 'opacity-70',
isDarkMode ? 'hover:bg-gray-700/60' : 'hover:bg-gray-50'
]"
>
<td
class="max-w-xs truncate px-4 py-3 text-sm font-medium"
:class="[primaryTextClass]"
:title="file.name"
>
{{ file.name }}
</td>
<td class="px-4 py-3 text-sm" :class="[primaryTextClass]">
{{ formatHistoryBytes(file.size) }}
</td>
<td class="px-4 py-3 text-sm font-mono" :class="[primaryTextClass]">
{{ file.code }}
</td>
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
{{ formatHistoryDate(file.uploaded_at || file.created_at) }}
</td>
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
{{ formatHistoryDate(file.expired_at) }}
</td>
<td class="px-4 py-3 text-sm" :class="[primaryTextClass]">
{{ file.download_count || 0 }}
</td>
<td class="px-4 py-3 text-sm" :class="[primaryTextClass]">
{{ formatHistoryBytes(file.download_traffic) }}
</td>
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
{{ file.deleted ? '已删除' : file.current ? '现存' : '历史' }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
<DataPagination
v-if="historyRows.length > 0"
:current-page="historyPage"
:page-size="historyPageSize"
:total="historyRows.length"
:page-size-options="[5, 10, 20, 50]"
@page-change="historyPage = $event"
@page-size-change="handleHistoryPageSizeChange"
/>
</section>
<BaseModal :show="showFileDetailModal" size="xl" @close="closeFileDetail">
<template #header>
<div class="flex min-w-0 items-center space-x-3">
@@ -1181,10 +1335,11 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch, type Component } from 'vue'
import { computed, onMounted, ref, watch, type Component } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import type {
AnalyticsFileRow,
AdminBatchEditMode,
AdminFileHealthFilter,
AdminFileInsightSeverity,
@@ -1222,7 +1377,8 @@ import FileEditField from '@/components/common/FileEditField.vue'
import BaseModal from '@/components/common/BaseModal.vue'
import BaseButton from '@/components/common/BaseButton.vue'
import { useAdminFiles, useInjectedDarkMode } from '@/composables'
import { formatTimestamp } from '@/utils/common'
import { StatsService } from '@/services'
import { formatFileSize, formatTimestamp } from '@/utils/common'
const { t } = useI18n()
const isDarkMode = useInjectedDarkMode()
@@ -1298,6 +1454,7 @@ const {
downloadFile,
exportPreviewText,
handlePageChange,
handlePageSizeChange,
handleSearch,
applyDetailPolicyAction,
applySelectedPolicyAction,
@@ -1321,6 +1478,34 @@ const {
toggleFileSelection
} = useAdminFiles()
const isFilterPanelOpen = ref(false)
const filterBodyVisible = computed(() => isFilterPanelOpen.value || hasActiveFilters.value)
const isHistoryLoading = ref(false)
const historyRows = ref<AnalyticsFileRow[]>([])
const historyPage = ref(1)
const historyPageSize = ref(10)
const historyPageRows = computed(() => {
const start = (historyPage.value - 1) * historyPageSize.value
return historyRows.value.slice(start, start + historyPageSize.value)
})
const loadHistoryFiles = async () => {
isHistoryLoading.value = true
try {
const response = await StatsService.getAnalytics()
historyRows.value = response.detail?.historyFiles || []
historyPage.value = 1
} finally {
isHistoryLoading.value = false
}
}
const handleHistoryPageSizeChange = (size: number) => {
historyPageSize.value = size
historyPage.value = 1
}
const formatHistoryBytes = (value: number | string | undefined) =>
formatFileSize(Number(value || 0), 1)
const formatHistoryDate = (value?: string) => (value ? formatTimestamp(value) : '-')
const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900'))
const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500'))
const panelClass = computed(() =>
@@ -1336,6 +1521,11 @@ const detailActionClass = computed(() =>
? 'border-gray-700 bg-gray-700/50 text-gray-300 hover:border-gray-600 hover:bg-gray-700'
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50'
)
const cardHoverClass = computed(() =>
isDarkMode.value
? 'hover:border-indigo-500/40 hover:bg-gray-800 hover:shadow-black/20'
: 'hover:border-indigo-200 hover:shadow-gray-200/80'
)
const detailPolicyActionClass = computed(() =>
isDarkMode.value
? 'border-gray-700 bg-gray-800/70 text-gray-200 hover:border-blue-500/40 hover:bg-blue-500/10'
@@ -1745,5 +1935,6 @@ onMounted(() => {
params.value.health = getRouteHealthFilter()
void loadViewPresets()
void loadFiles()
void loadHistoryFiles()
})
</script>