diff --git a/src/assets/style/main.css b/src/assets/style/main.css index c6d95bf..1f97750 100644 --- a/src/assets/style/main.css +++ b/src/assets/style/main.css @@ -24,3 +24,52 @@ min-height: 2.75rem; min-width: 2.75rem; } + +.theme-2026-card { + position: relative; + overflow: hidden; + border: 1px solid rgba(191, 208, 211, 0.72); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(234, 243, 245, 0.9)), + radial-gradient(circle at 14% 10%, rgba(122, 159, 171, 0.18), transparent 30%); + box-shadow: 0 14px 34px rgba(128, 151, 155, 0.22); +} + +.theme-2026-card::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(120deg, rgba(255, 255, 255, 0.48), transparent 42%); + opacity: 0.74; +} + +.theme-2026-card > * { + position: relative; +} + +.theme-2026-card-hover { + transition: + transform 220ms ease, + border-color 220ms ease, + box-shadow 220ms ease, + background-color 220ms ease; +} + +.theme-2026-card-hover:hover { + border-color: rgba(116, 153, 165, 0.86); + box-shadow: 0 20px 42px rgba(114, 143, 150, 0.32); +} + +.dark .theme-2026-card { + border-color: rgba(68, 86, 94, 0.82); + background: + linear-gradient(135deg, rgba(34, 49, 56, 0.96), rgba(24, 35, 42, 0.92)), + radial-gradient(circle at 14% 10%, rgba(102, 142, 156, 0.18), transparent 32%); + box-shadow: 0 16px 36px rgba(4, 10, 14, 0.34); +} + +.dark .theme-2026-card-hover:hover { + border-color: rgba(113, 153, 168, 0.68); + box-shadow: 0 20px 44px rgba(4, 10, 14, 0.46); +} diff --git a/src/components/common/BaseModal.vue b/src/components/common/BaseModal.vue index 31520ca..0e06628 100644 --- a/src/components/common/BaseModal.vue +++ b/src/components/common/BaseModal.vue @@ -7,13 +7,16 @@ @click="handleBackdropClick" > -
+
+

{{ tooltip.date }}

@@ -56,6 +56,7 @@ type TooltipState = { uploads: number downloadTraffic: string uploadTraffic: string + transform: string } type ChartPoint = { @@ -74,12 +75,14 @@ const rafId = ref(null) const tooltip = ref(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, 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 } diff --git a/src/components/common/StatCard.vue b/src/components/common/StatCard.vue index 1887284..2bed856 100644 --- a/src/components/common/StatCard.vue +++ b/src/components/common/StatCard.vue @@ -1,10 +1,10 @@