Reorder dashboard cards and expand trend metrics
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-lg border" :class="panelClass">
|
||||
<div class="relative overflow-hidden rounded-[30px] border" :class="panelClass">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
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>下载:{{ tooltip.downloads }}</p>
|
||||
<p>上传:{{ tooltip.uploads }}</p>
|
||||
<p>下载次数:{{ tooltip.downloads }}</p>
|
||||
<p>上传次数:{{ tooltip.uploads }}</p>
|
||||
<p>下载流量:{{ tooltip.downloadTraffic }}</p>
|
||||
<p>上传流量:{{ tooltip.uploadTraffic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,6 +54,14 @@ type TooltipState = {
|
||||
date: string
|
||||
downloads: number
|
||||
uploads: number
|
||||
downloadTraffic: string
|
||||
uploadTraffic: string
|
||||
}
|
||||
|
||||
type ChartPoint = {
|
||||
x: number
|
||||
y: number
|
||||
row: AnalyticsDailyRow
|
||||
}
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -72,6 +82,14 @@ const tooltipClass = computed(() =>
|
||||
: '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 width = canvasRef.value?.getBoundingClientRect().width || window.innerWidth
|
||||
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 width = Math.max(1, rect.width - padLeft - padRight)
|
||||
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 border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
||||
const downloadColor = '#4f46e5'
|
||||
const uploadColor = '#10b981'
|
||||
|
||||
ctx.strokeStyle = border
|
||||
ctx.lineWidth = 1
|
||||
@@ -139,33 +162,62 @@ const draw = () => {
|
||||
}
|
||||
|
||||
const denominator = Math.max(1, windowSize.value - 1)
|
||||
const points = visible
|
||||
.map((row, index) => {
|
||||
const sourceIndex = start + index
|
||||
return {
|
||||
x: padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
||||
y: padTop + height - (Number(row.downloads || 0) / max) * height,
|
||||
row
|
||||
}
|
||||
})
|
||||
.filter((point) => point.x >= padLeft - 50 && point.x <= padLeft + width + 50)
|
||||
const buildPoints = (field: 'downloads' | 'uploads') =>
|
||||
visible
|
||||
.map((row, index) => {
|
||||
const sourceIndex = start + index
|
||||
return {
|
||||
x: padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
||||
y: padTop + height - (Number(row[field] || 0) / max) * height,
|
||||
row
|
||||
}
|
||||
})
|
||||
.filter((point) => point.x >= padLeft - 50 && point.x <= padLeft + width + 50)
|
||||
|
||||
ctx.strokeStyle = '#4f46e5'
|
||||
ctx.lineWidth = 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
points.forEach((point, index) => {
|
||||
if (index) ctx.lineTo(point.x, point.y)
|
||||
else ctx.moveTo(point.x, point.y)
|
||||
})
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = '#4f46e5'
|
||||
points.forEach((point) => {
|
||||
const drawSeries = (points: ChartPoint[], color: string) => {
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
ctx.arc(point.x, point.y, mobile ? 4.5 : 3.5, 0, Math.PI * 2)
|
||||
points.forEach((point, index) => {
|
||||
if (index) ctx.lineTo(point.x, point.y)
|
||||
else ctx.moveTo(point.x, point.y)
|
||||
})
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = color
|
||||
points.forEach((point) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(point.x, point.y, mobile ? 4.5 : 3.5, 0, Math.PI * 2)
|
||||
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 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) =>
|
||||
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 = {
|
||||
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,
|
||||
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'
|
||||
ctx.strokeStyle = '#4f46e5'
|
||||
ctx.lineWidth = 3
|
||||
ctx.beginPath()
|
||||
ctx.arc(nearest.x, nearest.y, mobile ? 8 : 6, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
drawHoverPoint(nearest, downloadColor)
|
||||
if (uploadPoint) drawHoverPoint(uploadPoint, uploadColor)
|
||||
} else {
|
||||
tooltip.value = null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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="[
|
||||
isDarkMode
|
||||
? 'bg-gray-800/70 hover:bg-gray-800 hover:shadow-black/20'
|
||||
|
||||
Reference in New Issue
Block a user