Create 2026 admin theme interactions
This commit is contained in:
@@ -7,13 +7,16 @@
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
@click.stop="handleBackdropClick"
|
||||
></div>
|
||||
|
||||
<!-- 模态框容器 -->
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="relative transform overflow-hidden rounded-lg shadow-xl transition-all"
|
||||
class="relative transform overflow-hidden rounded-[30px] shadow-xl transition-all"
|
||||
:class="[
|
||||
sizeClasses,
|
||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
||||
@@ -100,8 +103,8 @@ const sizeClasses = computed(() => {
|
||||
return sizes[props.size]
|
||||
})
|
||||
|
||||
const handleBackdropClick = (event: MouseEvent) => {
|
||||
if (props.closeOnBackdrop && event.target === event.currentTarget) {
|
||||
const handleBackdropClick = () => {
|
||||
if (props.closeOnBackdrop) {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
@@ -127,4 +130,4 @@ const handleBackdropClick = (event: MouseEvent) => {
|
||||
.modal-leave-to .relative {
|
||||
transform: scale(0.95) translateY(-20px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-[30px] border" :class="panelClass">
|
||||
<div class="relative overflow-visible 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"
|
||||
@@ -13,12 +13,12 @@
|
||||
/>
|
||||
<div
|
||||
v-if="tooltip"
|
||||
class="pointer-events-none absolute z-10 min-w-32 rounded-lg border px-3 py-2 text-xs shadow-lg"
|
||||
class="pointer-events-none absolute z-30 w-max min-w-36 max-w-80 whitespace-nowrap rounded-xl border px-3 py-2 text-xs shadow-xl"
|
||||
:class="tooltipClass"
|
||||
:style="{
|
||||
left: `${tooltip.left}px`,
|
||||
top: `${tooltip.top}px`,
|
||||
transform: 'translate(-50%, -112%)'
|
||||
transform: tooltip.transform
|
||||
}"
|
||||
>
|
||||
<p class="font-semibold">{{ tooltip.date }}</p>
|
||||
@@ -56,6 +56,7 @@ type TooltipState = {
|
||||
uploads: number
|
||||
downloadTraffic: string
|
||||
uploadTraffic: string
|
||||
transform: string
|
||||
}
|
||||
|
||||
type ChartPoint = {
|
||||
@@ -74,12 +75,14 @@ const rafId = ref<number | null>(null)
|
||||
const tooltip = ref<TooltipState | null>(null)
|
||||
|
||||
const panelClass = computed(() =>
|
||||
props.isDarkMode ? 'border-gray-700 bg-gray-900/30' : 'border-gray-100 bg-gray-50'
|
||||
props.isDarkMode
|
||||
? 'border-[#40545c] bg-[linear-gradient(135deg,rgba(34,49,56,0.96),rgba(24,35,42,0.92))] shadow-md shadow-black/25'
|
||||
: 'border-[#d4e0e3] bg-[linear-gradient(135deg,#f8fbfb,#edf5f6)] shadow-md shadow-[#a9bdc2]/35'
|
||||
)
|
||||
const tooltipClass = computed(() =>
|
||||
props.isDarkMode
|
||||
? 'border-gray-700 bg-gray-800 text-gray-100'
|
||||
: 'border-gray-200 bg-white text-gray-900'
|
||||
? 'border-[#40545c] bg-[#203039] text-[#eef5f5]'
|
||||
: 'border-[#c9d9dd] bg-[#fbfdfd] text-[#263f48]'
|
||||
)
|
||||
|
||||
const formatBytes = (value: number | string | undefined) => {
|
||||
@@ -90,6 +93,10 @@ const formatBytes = (value: number | string | undefined) => {
|
||||
return `${(bytes / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
||||
}
|
||||
|
||||
const lerp = (start: number, end: number, amount: number) => start + (end - start) * amount
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value))
|
||||
|
||||
const getDefaultWindow = () => {
|
||||
const width = canvasRef.value?.getBoundingClientRect().width || window.innerWidth
|
||||
return Math.max(2, Math.min(props.rows.length || 2, width < 640 ? 14 : 60))
|
||||
@@ -145,8 +152,8 @@ const draw = () => {
|
||||
)
|
||||
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
||||
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
||||
const downloadColor = '#4f46e5'
|
||||
const uploadColor = '#10b981'
|
||||
const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3'
|
||||
const uploadColor = props.isDarkMode ? '#a9c7b8' : '#759f8e'
|
||||
|
||||
ctx.strokeStyle = border
|
||||
ctx.lineWidth = 1
|
||||
@@ -209,7 +216,7 @@ const draw = () => {
|
||||
|
||||
const points = downloadPoints
|
||||
|
||||
const drawHoverPoint = (point: ChartPoint, color: string) => {
|
||||
const drawHoverPoint = (point: Pick<ChartPoint, 'x' | 'y'>, color: string) => {
|
||||
ctx.fillStyle = props.isDarkMode ? '#111827' : '#fff'
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 3
|
||||
@@ -239,22 +246,54 @@ const draw = () => {
|
||||
: '无数据'
|
||||
)
|
||||
|
||||
if (hoverX.value !== null && points.length) {
|
||||
const nearest = points.reduce((current, point) =>
|
||||
Math.abs(point.x - hoverX.value!) < Math.abs(current.x - hoverX.value!) ? point : current
|
||||
if (hoverX.value !== null && points.length && total) {
|
||||
const hoverRatio = clamp((hoverX.value - padLeft) / width, 0, 1)
|
||||
const sourceIndex = clamp(offset.value + hoverRatio * denominator, 0, total - 1)
|
||||
const leftIndex = Math.floor(sourceIndex)
|
||||
const rightIndex = Math.min(total - 1, Math.ceil(sourceIndex))
|
||||
const mix = sourceIndex - leftIndex
|
||||
const leftRow = props.rows[leftIndex]
|
||||
const rightRow = props.rows[rightIndex] || leftRow
|
||||
const interpolatedX = padLeft + ((sourceIndex - offset.value) / denominator) * width
|
||||
const downloads = lerp(Number(leftRow.downloads || 0), Number(rightRow.downloads || 0), mix)
|
||||
const uploads = lerp(Number(leftRow.uploads || 0), Number(rightRow.uploads || 0), mix)
|
||||
const downloadTraffic = lerp(
|
||||
Number(leftRow.downloadTraffic || 0),
|
||||
Number(rightRow.downloadTraffic || 0),
|
||||
mix
|
||||
)
|
||||
const uploadPoint = uploadPoints.find((point) => point.row.date === nearest.row.date)
|
||||
const uploadTraffic = lerp(
|
||||
Number(leftRow.uploadTraffic || 0),
|
||||
Number(rightRow.uploadTraffic || 0),
|
||||
mix
|
||||
)
|
||||
const downloadY = padTop + height - (downloads / max) * height
|
||||
const uploadY = padTop + height - (uploads / max) * height
|
||||
const anchorY = Math.min(downloadY, uploadY)
|
||||
const placeBelow = anchorY < 104
|
||||
const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8))
|
||||
const dateText =
|
||||
leftRow.date === rightRow.date ? leftRow.date : `${leftRow.date} - ${rightRow.date}`
|
||||
|
||||
ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)'
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(interpolatedX, padTop)
|
||||
ctx.lineTo(interpolatedX, padTop + height)
|
||||
ctx.stroke()
|
||||
|
||||
tooltip.value = {
|
||||
left: Math.min(rect.width - 80, Math.max(80, nearest.x)),
|
||||
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),
|
||||
downloadTraffic: formatBytes(nearest.row.downloadTraffic),
|
||||
uploadTraffic: formatBytes(nearest.row.uploadTraffic)
|
||||
left: clamp(interpolatedX, tooltipInset, rect.width - tooltipInset),
|
||||
top: placeBelow ? anchorY + 16 : anchorY - 12,
|
||||
date: dateText,
|
||||
downloads: Math.round(downloads),
|
||||
uploads: Math.round(uploads),
|
||||
downloadTraffic: formatBytes(downloadTraffic),
|
||||
uploadTraffic: formatBytes(uploadTraffic),
|
||||
transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)'
|
||||
}
|
||||
drawHoverPoint(nearest, downloadColor)
|
||||
if (uploadPoint) drawHoverPoint(uploadPoint, uploadColor)
|
||||
drawHoverPoint({ x: interpolatedX, y: downloadY }, downloadColor)
|
||||
drawHoverPoint({ x: interpolatedX, y: uploadY }, uploadColor)
|
||||
} else {
|
||||
tooltip.value = null
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="group rounded-[30px] p-6 shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class="theme-2026-card theme-2026-card-hover 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'
|
||||
: 'bg-white hover:shadow-gray-200/80'
|
||||
? 'hover:bg-gray-800 hover:shadow-black/20'
|
||||
: 'hover:shadow-gray-200/80'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user