Add admin analytics trend views
This commit is contained in:
@@ -84,8 +84,166 @@
|
||||
</StatCard>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="mt-6 rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="mb-5 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">下载趋势</h3>
|
||||
<p class="text-sm" :class="[mutedTextClass]">
|
||||
{{ analyticsWindowText }},下载流量 {{ analyticsDownloadTrafficText }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
v-model="analyticsStart"
|
||||
type="date"
|
||||
min="1900-01-01"
|
||||
max="2100-12-31"
|
||||
class="h-11 rounded-lg border px-3 text-sm"
|
||||
:class="[fieldClass]"
|
||||
@change="fetchAnalyticsData"
|
||||
/>
|
||||
<input
|
||||
v-model="analyticsEnd"
|
||||
type="date"
|
||||
min="1900-01-01"
|
||||
max="2100-12-31"
|
||||
class="h-11 rounded-lg border px-3 text-sm"
|
||||
:class="[fieldClass]"
|
||||
@change="fetchAnalyticsData"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
||||
:class="detailActionClass"
|
||||
@click="trendCanvas?.zoom(0.75)"
|
||||
>
|
||||
放大
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
||||
:class="detailActionClass"
|
||||
@click="trendCanvas?.zoom(1.33)"
|
||||
>
|
||||
缩小
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
||||
:class="detailActionClass"
|
||||
@click="trendCanvas?.resetWindow()"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="analyticsError"
|
||||
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'
|
||||
]"
|
||||
>
|
||||
{{ analyticsError }}
|
||||
</div>
|
||||
<SmoothTrendCanvas
|
||||
ref="trendCanvas"
|
||||
:rows="analyticsDailyRows"
|
||||
:is-dark-mode="isDarkMode"
|
||||
@window-change="analyticsWindowText = $event"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="mt-6 rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">热门下载 Top 5</h3>
|
||||
<p class="text-sm" :class="[mutedTextClass]">统计当前日期范围内的下载次数和流量</p>
|
||||
</div>
|
||||
<DownloadCloudIcon
|
||||
class="h-5 w-5"
|
||||
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-500']"
|
||||
/>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y" :class="[isDarkMode ? 'divide-gray-700' : 'divide-gray-100']">
|
||||
<tr v-if="analyticsTopFiles.length === 0">
|
||||
<td colspan="4" class="px-4 py-8 text-center text-sm" :class="[mutedTextClass]">
|
||||
暂无下载记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="file in analyticsTopFiles"
|
||||
:key="file.code"
|
||||
:class="[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]">
|
||||
{{ file.download_count || 0 }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" :class="[primaryTextClass]">
|
||||
{{ formatBytes(file.download_traffic) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
|
||||
{{ file.current ? '现存' : '历史' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||
<section
|
||||
class="rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg xl:col-span-2"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||
@@ -148,7 +306,10 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="rounded-lg border p-4" :class="[subtlePanelClass]">
|
||||
<div
|
||||
class="rounded-lg border p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
||||
:class="[subtlePanelClass, cardHoverClass]"
|
||||
>
|
||||
<p class="text-sm" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.expiredFiles') }}
|
||||
</p>
|
||||
@@ -162,7 +323,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4" :class="[subtlePanelClass]">
|
||||
<div
|
||||
class="rounded-lg border p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
||||
:class="[subtlePanelClass, cardHoverClass]"
|
||||
>
|
||||
<p class="text-sm" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.chunkedFiles') }}
|
||||
</p>
|
||||
@@ -178,7 +342,10 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||
<section
|
||||
class="rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:class="[panelClass, cardHoverClass]"
|
||||
>
|
||||
<div class="mb-5">
|
||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('admin.dashboard.storagePolicy') }}
|
||||
@@ -245,7 +412,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, onMounted } from 'vue'
|
||||
import { computed, defineComponent, h, onMounted, ref } from 'vue'
|
||||
import type { Component, PropType } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -262,11 +429,14 @@ import {
|
||||
UploadCloudIcon
|
||||
} from 'lucide-vue-next'
|
||||
import StatCard from '@/components/common/StatCard.vue'
|
||||
import SmoothTrendCanvas from '@/components/common/SmoothTrendCanvas.vue'
|
||||
import { useDashboardStats, useInjectedDarkMode } from '@/composables'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ROUTES } from '@/constants'
|
||||
import { useConfigStore } from '@/stores/configStore'
|
||||
import type { DashboardHealthAction } from '@/types'
|
||||
import { StatsService } from '@/services'
|
||||
import { formatFileSize } from '@/utils/common'
|
||||
import type { AnalyticsData, AnalyticsFileRow, DashboardHealthAction } from '@/types'
|
||||
|
||||
const isDarkMode = useInjectedDarkMode()
|
||||
const { t } = useI18n()
|
||||
@@ -284,9 +454,24 @@ const versionText = computed(() => appVersion.value || t('admin.dashboard.versio
|
||||
const panelClass = computed(() =>
|
||||
isDarkMode.value ? 'bg-gray-800/80 border border-gray-700' : 'bg-white border border-gray-100'
|
||||
)
|
||||
const fieldClass = computed(() =>
|
||||
isDarkMode.value
|
||||
? 'border-gray-600 bg-gray-700 text-white'
|
||||
: 'border-gray-300 bg-white text-gray-900'
|
||||
)
|
||||
const detailActionClass = computed(() =>
|
||||
isDarkMode.value
|
||||
? '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 subtlePanelClass = computed(() =>
|
||||
isDarkMode.value ? 'border-gray-700 bg-gray-900/30' : 'border-gray-100 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 maxSaveTimeText = computed(() => {
|
||||
if (!dashboardData.maxSaveSeconds) return t('admin.dashboard.noSaveLimit')
|
||||
const days = Math.floor(dashboardData.maxSaveSeconds / 86400)
|
||||
@@ -296,6 +481,35 @@ const maxSaveTimeText = computed(() => {
|
||||
return `${Math.floor(dashboardData.maxSaveSeconds / 60)}${t('common.minute')}`
|
||||
})
|
||||
|
||||
const today = () => new Date().toISOString().slice(0, 10)
|
||||
const daysAgo = (days: number) => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - days)
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
const trendCanvas = ref<InstanceType<typeof SmoothTrendCanvas> | null>(null)
|
||||
const analyticsStart = ref(daysAgo(120))
|
||||
const analyticsEnd = ref(today())
|
||||
const analyticsData = ref<AnalyticsData | null>(null)
|
||||
const analyticsError = ref('')
|
||||
const analyticsWindowText = ref('无数据')
|
||||
const analyticsDailyRows = computed(() => analyticsData.value?.daily || [])
|
||||
const analyticsTopFiles = computed<AnalyticsFileRow[]>(() => analyticsData.value?.topFiles || [])
|
||||
const analyticsDownloadTrafficText = computed(() =>
|
||||
formatBytes(analyticsData.value?.totals?.downloadTraffic || 0)
|
||||
)
|
||||
const formatBytes = (value: number | string | undefined) => formatFileSize(Number(value || 0), 1)
|
||||
|
||||
const fetchAnalyticsData = async () => {
|
||||
analyticsError.value = ''
|
||||
try {
|
||||
const response = await StatsService.getAnalytics(analyticsStart.value, analyticsEnd.value)
|
||||
analyticsData.value = response.detail || null
|
||||
} catch (error) {
|
||||
analyticsError.value = error instanceof Error ? error.message : '统计加载失败'
|
||||
}
|
||||
}
|
||||
|
||||
const healthActions = computed<DashboardHealthAction[]>(() => [
|
||||
{
|
||||
key: 'attention',
|
||||
@@ -394,19 +608,26 @@ const MetricProgress = defineComponent({
|
||||
})
|
||||
|
||||
return () =>
|
||||
h('div', { class: 'rounded-lg border p-4 border-gray-200/60 dark:border-gray-700' }, [
|
||||
h('div', { class: 'mb-2 flex items-center justify-between text-sm' }, [
|
||||
h('span', { class: 'text-gray-500 dark:text-gray-400' }, props.label),
|
||||
h('span', { class: 'font-medium text-gray-900 dark:text-white' }, `${props.value}%`)
|
||||
]),
|
||||
h('div', { class: 'h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700' }, [
|
||||
h('div', {
|
||||
class: ['h-full rounded-full', toneClass.value],
|
||||
style: { width: `${props.value}%` }
|
||||
})
|
||||
]),
|
||||
h('p', { class: 'mt-2 text-sm text-gray-500 dark:text-gray-400' }, props.detail)
|
||||
])
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class:
|
||||
'rounded-lg border border-gray-200/60 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:border-indigo-200 hover:shadow-md hover:shadow-gray-200/80 dark:border-gray-700 dark:hover:border-indigo-500/40 dark:hover:bg-gray-800 dark:hover:shadow-black/20'
|
||||
},
|
||||
[
|
||||
h('div', { class: 'mb-2 flex items-center justify-between text-sm' }, [
|
||||
h('span', { class: 'text-gray-500 dark:text-gray-400' }, props.label),
|
||||
h('span', { class: 'font-medium text-gray-900 dark:text-white' }, `${props.value}%`)
|
||||
]),
|
||||
h('div', { class: 'h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700' }, [
|
||||
h('div', {
|
||||
class: ['h-full rounded-full', toneClass.value],
|
||||
style: { width: `${props.value}%` }
|
||||
})
|
||||
]),
|
||||
h('p', { class: 'mt-2 text-sm text-gray-500 dark:text-gray-400' }, props.detail)
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -433,6 +654,6 @@ const PolicyRow = defineComponent({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void fetchDashboardData()
|
||||
void Promise.all([fetchDashboardData(), fetchAnalyticsData()])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user