Reorder dashboard cards and expand trend metrics
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative overflow-hidden rounded-lg border" :class="panelClass">
|
<div class="relative overflow-hidden rounded-[30px] border" :class="panelClass">
|
||||||
<canvas
|
<canvas
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
class="block h-80 min-h-80 w-full touch-none select-none md:h-72 md:min-h-72"
|
class="block h-80 min-h-80 w-full touch-none select-none md:h-72 md:min-h-72"
|
||||||
@@ -22,8 +22,10 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<p class="font-semibold">{{ tooltip.date }}</p>
|
<p class="font-semibold">{{ tooltip.date }}</p>
|
||||||
<p>下载:{{ tooltip.downloads }}</p>
|
<p>下载次数:{{ tooltip.downloads }}</p>
|
||||||
<p>上传:{{ tooltip.uploads }}</p>
|
<p>上传次数:{{ tooltip.uploads }}</p>
|
||||||
|
<p>下载流量:{{ tooltip.downloadTraffic }}</p>
|
||||||
|
<p>上传流量:{{ tooltip.uploadTraffic }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,6 +54,14 @@ type TooltipState = {
|
|||||||
date: string
|
date: string
|
||||||
downloads: number
|
downloads: number
|
||||||
uploads: number
|
uploads: number
|
||||||
|
downloadTraffic: string
|
||||||
|
uploadTraffic: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartPoint = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
row: AnalyticsDailyRow
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
@@ -72,6 +82,14 @@ const tooltipClass = computed(() =>
|
|||||||
: 'border-gray-200 bg-white text-gray-900'
|
: 'border-gray-200 bg-white text-gray-900'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const formatBytes = (value: number | string | undefined) => {
|
||||||
|
const bytes = Number(value || 0)
|
||||||
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||||
|
return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
||||||
|
}
|
||||||
|
|
||||||
const getDefaultWindow = () => {
|
const getDefaultWindow = () => {
|
||||||
const width = canvasRef.value?.getBoundingClientRect().width || window.innerWidth
|
const width = canvasRef.value?.getBoundingClientRect().width || window.innerWidth
|
||||||
return Math.max(2, Math.min(props.rows.length || 2, width < 640 ? 14 : 60))
|
return Math.max(2, Math.min(props.rows.length || 2, width < 640 ? 14 : 60))
|
||||||
@@ -121,9 +139,14 @@ const draw = () => {
|
|||||||
const padBottom = mobile ? 42 : 36
|
const padBottom = mobile ? 42 : 36
|
||||||
const width = Math.max(1, rect.width - padLeft - padRight)
|
const width = Math.max(1, rect.width - padLeft - padRight)
|
||||||
const height = Math.max(1, rect.height - padTop - padBottom)
|
const height = Math.max(1, rect.height - padTop - padBottom)
|
||||||
const max = Math.max(1, ...visible.map((row) => Number(row.downloads || 0)))
|
const max = Math.max(
|
||||||
|
1,
|
||||||
|
...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)])
|
||||||
|
)
|
||||||
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
||||||
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
||||||
|
const downloadColor = '#4f46e5'
|
||||||
|
const uploadColor = '#10b981'
|
||||||
|
|
||||||
ctx.strokeStyle = border
|
ctx.strokeStyle = border
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
@@ -139,18 +162,20 @@ const draw = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const denominator = Math.max(1, windowSize.value - 1)
|
const denominator = Math.max(1, windowSize.value - 1)
|
||||||
const points = visible
|
const buildPoints = (field: 'downloads' | 'uploads') =>
|
||||||
|
visible
|
||||||
.map((row, index) => {
|
.map((row, index) => {
|
||||||
const sourceIndex = start + index
|
const sourceIndex = start + index
|
||||||
return {
|
return {
|
||||||
x: padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
x: padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
||||||
y: padTop + height - (Number(row.downloads || 0) / max) * height,
|
y: padTop + height - (Number(row[field] || 0) / max) * height,
|
||||||
row
|
row
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((point) => point.x >= padLeft - 50 && point.x <= padLeft + width + 50)
|
.filter((point) => point.x >= padLeft - 50 && point.x <= padLeft + width + 50)
|
||||||
|
|
||||||
ctx.strokeStyle = '#4f46e5'
|
const drawSeries = (points: ChartPoint[], color: string) => {
|
||||||
|
ctx.strokeStyle = color
|
||||||
ctx.lineWidth = 3
|
ctx.lineWidth = 3
|
||||||
ctx.lineCap = 'round'
|
ctx.lineCap = 'round'
|
||||||
ctx.lineJoin = 'round'
|
ctx.lineJoin = 'round'
|
||||||
@@ -160,12 +185,39 @@ const draw = () => {
|
|||||||
else ctx.moveTo(point.x, point.y)
|
else ctx.moveTo(point.x, point.y)
|
||||||
})
|
})
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.fillStyle = '#4f46e5'
|
ctx.fillStyle = color
|
||||||
points.forEach((point) => {
|
points.forEach((point) => {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(point.x, point.y, mobile ? 4.5 : 3.5, 0, Math.PI * 2)
|
ctx.arc(point.x, point.y, mobile ? 4.5 : 3.5, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadPoints = buildPoints('downloads')
|
||||||
|
const uploadPoints = buildPoints('uploads')
|
||||||
|
drawSeries(downloadPoints, downloadColor)
|
||||||
|
drawSeries(uploadPoints, uploadColor)
|
||||||
|
|
||||||
|
ctx.font = '12px system-ui'
|
||||||
|
ctx.fillStyle = muted
|
||||||
|
ctx.fillText('下载', padLeft, 16)
|
||||||
|
ctx.fillText('上传', padLeft + 58, 16)
|
||||||
|
ctx.fillStyle = downloadColor
|
||||||
|
ctx.fillRect(padLeft - 14, 8, 8, 8)
|
||||||
|
ctx.fillStyle = uploadColor
|
||||||
|
ctx.fillRect(padLeft + 44, 8, 8, 8)
|
||||||
|
|
||||||
|
const points = downloadPoints
|
||||||
|
|
||||||
|
const drawHoverPoint = (point: ChartPoint, color: string) => {
|
||||||
|
ctx.fillStyle = props.isDarkMode ? '#111827' : '#fff'
|
||||||
|
ctx.strokeStyle = color
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(point.x, point.y, mobile ? 8 : 6, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
const leftRow = props.rows[Math.max(0, Math.round(offset.value))]
|
const leftRow = props.rows[Math.max(0, Math.round(offset.value))]
|
||||||
const rightRow = props.rows[Math.min(total - 1, Math.round(offset.value + windowSize.value - 1))]
|
const rightRow = props.rows[Math.min(total - 1, Math.round(offset.value + windowSize.value - 1))]
|
||||||
@@ -191,20 +243,18 @@ const draw = () => {
|
|||||||
const nearest = points.reduce((current, point) =>
|
const nearest = points.reduce((current, point) =>
|
||||||
Math.abs(point.x - hoverX.value!) < Math.abs(current.x - hoverX.value!) ? point : current
|
Math.abs(point.x - hoverX.value!) < Math.abs(current.x - hoverX.value!) ? point : current
|
||||||
)
|
)
|
||||||
|
const uploadPoint = uploadPoints.find((point) => point.row.date === nearest.row.date)
|
||||||
tooltip.value = {
|
tooltip.value = {
|
||||||
left: Math.min(rect.width - 80, Math.max(80, nearest.x)),
|
left: Math.min(rect.width - 80, Math.max(80, nearest.x)),
|
||||||
top: Math.max(46, nearest.y),
|
top: Math.max(46, Math.min(nearest.y, uploadPoint?.y ?? nearest.y)),
|
||||||
date: nearest.row.date,
|
date: nearest.row.date,
|
||||||
downloads: Number(nearest.row.downloads || 0),
|
downloads: Number(nearest.row.downloads || 0),
|
||||||
uploads: Number(nearest.row.uploads || 0)
|
uploads: Number(nearest.row.uploads || 0),
|
||||||
|
downloadTraffic: formatBytes(nearest.row.downloadTraffic),
|
||||||
|
uploadTraffic: formatBytes(nearest.row.uploadTraffic)
|
||||||
}
|
}
|
||||||
ctx.fillStyle = props.isDarkMode ? '#111827' : '#fff'
|
drawHoverPoint(nearest, downloadColor)
|
||||||
ctx.strokeStyle = '#4f46e5'
|
if (uploadPoint) drawHoverPoint(uploadPoint, uploadColor)
|
||||||
ctx.lineWidth = 3
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(nearest.x, nearest.y, mobile ? 8 : 6, 0, Math.PI * 2)
|
|
||||||
ctx.fill()
|
|
||||||
ctx.stroke()
|
|
||||||
} else {
|
} else {
|
||||||
tooltip.value = null
|
tooltip.value = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="group rounded-lg p-6 shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
class="group rounded-[30px] p-6 shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'bg-gray-800/70 hover:bg-gray-800 hover:shadow-black/20'
|
? 'bg-gray-800/70 hover:bg-gray-800 hover:shadow-black/20'
|
||||||
|
|||||||
+167
-154
@@ -84,15 +84,166 @@
|
|||||||
</StatCard>
|
</StatCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||||
<section
|
<section
|
||||||
class="mt-6 rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="rounded-[30px] 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]">
|
||||||
|
{{ t('admin.dashboard.fileHealth') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('admin.dashboard.fileHealthDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ActivityIcon
|
||||||
|
class="h-5 w-5"
|
||||||
|
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-500']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<MetricProgress
|
||||||
|
:label="t('admin.dashboard.activeFileRatio')"
|
||||||
|
:value="dashboardData.activeRatio"
|
||||||
|
:detail="`${dashboardData.activeCount} / ${dashboardData.totalFiles}`"
|
||||||
|
tone="green"
|
||||||
|
/>
|
||||||
|
<MetricProgress
|
||||||
|
:label="t('admin.dashboard.fileShareRatio')"
|
||||||
|
:value="dashboardData.fileRatio"
|
||||||
|
:detail="t('admin.dashboard.binaryFiles', { count: dashboardData.fileCount })"
|
||||||
|
tone="indigo"
|
||||||
|
/>
|
||||||
|
<MetricProgress
|
||||||
|
:label="t('admin.dashboard.textShareRatio')"
|
||||||
|
:value="dashboardData.textRatio"
|
||||||
|
:detail="t('admin.dashboard.textShares', { count: dashboardData.textCount })"
|
||||||
|
tone="purple"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||||
|
<button
|
||||||
|
v-for="action in healthActions"
|
||||||
|
:key="action.key"
|
||||||
|
type="button"
|
||||||
|
class="group flex min-h-28 flex-col justify-between rounded-[30px] border p-4 text-left transition-colors"
|
||||||
|
:class="getHealthActionClass(action.tone)"
|
||||||
|
@click="openHealthQueue(action.health)"
|
||||||
|
>
|
||||||
|
<span class="flex items-start justify-between gap-3">
|
||||||
|
<span>
|
||||||
|
<span class="block text-2xl font-semibold">{{ action.count }}</span>
|
||||||
|
<span class="mt-1 block text-sm font-medium">{{ action.label }}</span>
|
||||||
|
</span>
|
||||||
|
<component :is="getHealthActionIcon(action.tone)" class="h-5 w-5 shrink-0" />
|
||||||
|
</span>
|
||||||
|
<span class="mt-3 flex items-center justify-between gap-2 text-xs">
|
||||||
|
<span class="line-clamp-2">{{ action.description }}</span>
|
||||||
|
<ArrowRightIcon
|
||||||
|
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div
|
||||||
|
class="rounded-[30px] 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>
|
||||||
|
<div class="mt-2 flex items-end justify-between">
|
||||||
|
<strong class="text-3xl" :class="[primaryTextClass]">
|
||||||
|
{{ dashboardData.expiredCount }}
|
||||||
|
</strong>
|
||||||
|
<span class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('admin.dashboard.needCleanup') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-[30px] 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>
|
||||||
|
<div class="mt-2 flex items-end justify-between">
|
||||||
|
<strong class="text-3xl" :class="[primaryTextClass]">
|
||||||
|
{{ dashboardData.chunkedCount }}
|
||||||
|
</strong>
|
||||||
|
<span class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ dashboardData.enableChunk ? t('common.enabled') : t('common.disabled') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="rounded-[30px] 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') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('admin.dashboard.storagePolicyDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PolicyRow
|
||||||
|
:label="t('admin.dashboard.storageBackend')"
|
||||||
|
:value="dashboardData.storageBackend"
|
||||||
|
/>
|
||||||
|
<PolicyRow
|
||||||
|
:label="t('admin.dashboard.singleFileLimit')"
|
||||||
|
:value="dashboardData.uploadSizeLimitText"
|
||||||
|
/>
|
||||||
|
<PolicyRow
|
||||||
|
:label="t('admin.dashboard.guestUpload')"
|
||||||
|
:value="dashboardData.openUpload ? t('common.enabled') : t('common.disabled')"
|
||||||
|
/>
|
||||||
|
<PolicyRow :label="t('admin.dashboard.maxSaveTime')" :value="maxSaveTimeText" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="mb-2 flex items-center justify-between text-sm">
|
||||||
|
<span :class="[mutedTextClass]">{{ t('admin.dashboard.todayCapacityReference') }}</span>
|
||||||
|
<span :class="[primaryTextClass]">{{ dashboardData.todaySizeRatio }}%</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-2 overflow-hidden rounded-full"
|
||||||
|
:class="[isDarkMode ? 'bg-gray-700' : 'bg-gray-100']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-indigo-500"
|
||||||
|
:style="{ width: `${dashboardData.todaySizeRatio}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
|
<section
|
||||||
|
class="rounded-[30px] p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div class="mb-5 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">下载趋势</h3>
|
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">下载/上传趋势</h3>
|
||||||
<p class="text-sm" :class="[mutedTextClass]">
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
{{ analyticsWindowText }},下载流量 {{ analyticsDownloadTrafficText }}
|
{{ analyticsSummaryText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -160,7 +311,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="mt-6 rounded-lg p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="rounded-[30px] p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-5 flex items-center justify-between">
|
||||||
@@ -238,155 +389,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
||||||
<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]">
|
|
||||||
{{ t('admin.dashboard.fileHealth') }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm" :class="[mutedTextClass]">
|
|
||||||
{{ t('admin.dashboard.fileHealthDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ActivityIcon
|
|
||||||
class="h-5 w-5"
|
|
||||||
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-500']"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
||||||
<MetricProgress
|
|
||||||
:label="t('admin.dashboard.activeFileRatio')"
|
|
||||||
:value="dashboardData.activeRatio"
|
|
||||||
:detail="`${dashboardData.activeCount} / ${dashboardData.totalFiles}`"
|
|
||||||
tone="green"
|
|
||||||
/>
|
|
||||||
<MetricProgress
|
|
||||||
:label="t('admin.dashboard.fileShareRatio')"
|
|
||||||
:value="dashboardData.fileRatio"
|
|
||||||
:detail="t('admin.dashboard.binaryFiles', { count: dashboardData.fileCount })"
|
|
||||||
tone="indigo"
|
|
||||||
/>
|
|
||||||
<MetricProgress
|
|
||||||
:label="t('admin.dashboard.textShareRatio')"
|
|
||||||
:value="dashboardData.textRatio"
|
|
||||||
:detail="t('admin.dashboard.textShares', { count: dashboardData.textCount })"
|
|
||||||
tone="purple"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
||||||
<button
|
|
||||||
v-for="action in healthActions"
|
|
||||||
:key="action.key"
|
|
||||||
type="button"
|
|
||||||
class="group flex min-h-28 flex-col justify-between rounded-lg border p-4 text-left transition-colors"
|
|
||||||
:class="getHealthActionClass(action.tone)"
|
|
||||||
@click="openHealthQueue(action.health)"
|
|
||||||
>
|
|
||||||
<span class="flex items-start justify-between gap-3">
|
|
||||||
<span>
|
|
||||||
<span class="block text-2xl font-semibold">{{ action.count }}</span>
|
|
||||||
<span class="mt-1 block text-sm font-medium">{{ action.label }}</span>
|
|
||||||
</span>
|
|
||||||
<component :is="getHealthActionIcon(action.tone)" class="h-5 w-5 shrink-0" />
|
|
||||||
</span>
|
|
||||||
<span class="mt-3 flex items-center justify-between gap-2 text-xs">
|
|
||||||
<span class="line-clamp-2">{{ action.description }}</span>
|
|
||||||
<ArrowRightIcon
|
|
||||||
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<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>
|
|
||||||
<div class="mt-2 flex items-end justify-between">
|
|
||||||
<strong class="text-3xl" :class="[primaryTextClass]">
|
|
||||||
{{ dashboardData.expiredCount }}
|
|
||||||
</strong>
|
|
||||||
<span class="text-sm" :class="[mutedTextClass]">
|
|
||||||
{{ t('admin.dashboard.needCleanup') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<div class="mt-2 flex items-end justify-between">
|
|
||||||
<strong class="text-3xl" :class="[primaryTextClass]">
|
|
||||||
{{ dashboardData.chunkedCount }}
|
|
||||||
</strong>
|
|
||||||
<span class="text-sm" :class="[mutedTextClass]">
|
|
||||||
{{ dashboardData.enableChunk ? t('common.enabled') : t('common.disabled') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<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') }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm" :class="[mutedTextClass]">
|
|
||||||
{{ t('admin.dashboard.storagePolicyDesc') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<PolicyRow
|
|
||||||
:label="t('admin.dashboard.storageBackend')"
|
|
||||||
:value="dashboardData.storageBackend"
|
|
||||||
/>
|
|
||||||
<PolicyRow
|
|
||||||
:label="t('admin.dashboard.singleFileLimit')"
|
|
||||||
:value="dashboardData.uploadSizeLimitText"
|
|
||||||
/>
|
|
||||||
<PolicyRow
|
|
||||||
:label="t('admin.dashboard.guestUpload')"
|
|
||||||
:value="dashboardData.openUpload ? t('common.enabled') : t('common.disabled')"
|
|
||||||
/>
|
|
||||||
<PolicyRow :label="t('admin.dashboard.maxSaveTime')" :value="maxSaveTimeText" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5">
|
|
||||||
<div class="mb-2 flex items-center justify-between text-sm">
|
|
||||||
<span :class="[mutedTextClass]">{{ t('admin.dashboard.todayCapacityReference') }}</span>
|
|
||||||
<span :class="[primaryTextClass]">{{ dashboardData.todaySizeRatio }}%</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="h-2 overflow-hidden rounded-full"
|
|
||||||
:class="[isDarkMode ? 'bg-gray-700' : 'bg-gray-100']"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-indigo-500"
|
|
||||||
:style="{ width: `${dashboardData.todaySizeRatio}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer
|
<footer
|
||||||
@@ -495,9 +497,20 @@ const analyticsError = ref('')
|
|||||||
const analyticsWindowText = ref('无数据')
|
const analyticsWindowText = ref('无数据')
|
||||||
const analyticsDailyRows = computed(() => analyticsData.value?.daily || [])
|
const analyticsDailyRows = computed(() => analyticsData.value?.daily || [])
|
||||||
const analyticsTopFiles = computed<AnalyticsFileRow[]>(() => analyticsData.value?.topFiles || [])
|
const analyticsTopFiles = computed<AnalyticsFileRow[]>(() => analyticsData.value?.topFiles || [])
|
||||||
|
const analyticsUploadTrafficText = computed(() =>
|
||||||
|
formatBytes(analyticsData.value?.totals?.uploadTraffic || 0)
|
||||||
|
)
|
||||||
const analyticsDownloadTrafficText = computed(() =>
|
const analyticsDownloadTrafficText = computed(() =>
|
||||||
formatBytes(analyticsData.value?.totals?.downloadTraffic || 0)
|
formatBytes(analyticsData.value?.totals?.downloadTraffic || 0)
|
||||||
)
|
)
|
||||||
|
const analyticsSummaryText = computed(
|
||||||
|
() =>
|
||||||
|
`${analyticsWindowText.value},下载 ${analyticsData.value?.totals?.totalDownloads || 0} 次 / ${
|
||||||
|
analyticsDownloadTrafficText.value
|
||||||
|
},上传 ${analyticsData.value?.totals?.totalUploads || 0} 次 / ${
|
||||||
|
analyticsUploadTrafficText.value
|
||||||
|
}`
|
||||||
|
)
|
||||||
const formatBytes = (value: number | string | undefined) => formatFileSize(Number(value || 0), 1)
|
const formatBytes = (value: number | string | undefined) => formatFileSize(Number(value || 0), 1)
|
||||||
|
|
||||||
const fetchAnalyticsData = async () => {
|
const fetchAnalyticsData = async () => {
|
||||||
@@ -611,7 +624,7 @@ const MetricProgress = defineComponent({
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class:
|
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'
|
'rounded-[30px] 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('div', { class: 'mb-2 flex items-center justify-between text-sm' }, [
|
||||||
|
|||||||
Reference in New Issue
Block a user