Main #2
@@ -2,7 +2,7 @@
|
|||||||
<div class="relative overflow-visible rounded-[30px] border" :class="panelClass">
|
<div class="relative overflow-visible 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-72 min-h-72 w-full touch-pan-y select-none sm:h-80 sm:min-h-80 md:h-72 md:min-h-72"
|
||||||
@wheel.prevent="handleWheel"
|
@wheel.prevent="handleWheel"
|
||||||
@pointerdown="handlePointerDown"
|
@pointerdown="handlePointerDown"
|
||||||
@pointermove="handlePointerMove"
|
@pointermove="handlePointerMove"
|
||||||
@@ -44,8 +44,11 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
type DragState = {
|
type DragState = {
|
||||||
|
startX: number
|
||||||
lastX: number
|
lastX: number
|
||||||
time: number
|
time: number
|
||||||
|
pointerType: string
|
||||||
|
moved: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type TooltipState = {
|
type TooltipState = {
|
||||||
@@ -381,36 +384,67 @@ const handleWheel = (event: WheelEvent) => {
|
|||||||
zoom(event.deltaY > 0 ? 1.16 : 0.86, rect ? (event.clientX - rect.left) / rect.width : 0.5)
|
zoom(event.deltaY > 0 ? 1.16 : 0.86, rect ? (event.clientX - rect.left) / rect.width : 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setHoverFromClientX = (clientX: number) => {
|
||||||
|
const rect = canvasRef.value?.getBoundingClientRect()
|
||||||
|
if (!rect) return
|
||||||
|
hoverX.value = clamp(clientX - rect.left, 0, rect.width)
|
||||||
|
scheduleDraw()
|
||||||
|
}
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
canvasRef.value?.setPointerCapture(event.pointerId)
|
canvasRef.value?.setPointerCapture(event.pointerId)
|
||||||
cancelInertia()
|
cancelInertia()
|
||||||
velocity.value = 0
|
velocity.value = 0
|
||||||
drag.value = { lastX: event.clientX, time: event.timeStamp || performance.now() }
|
setHoverFromClientX(event.clientX)
|
||||||
|
drag.value = {
|
||||||
|
startX: event.clientX,
|
||||||
|
lastX: event.clientX,
|
||||||
|
time: event.timeStamp || performance.now(),
|
||||||
|
pointerType: event.pointerType,
|
||||||
|
moved: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
const rect = canvasRef.value?.getBoundingClientRect()
|
const rect = canvasRef.value?.getBoundingClientRect()
|
||||||
const pointerEvents = event.getCoalescedEvents?.() || [event]
|
const coalescedEvents = event.getCoalescedEvents?.() || []
|
||||||
|
const pointerEvents = coalescedEvents.length ? coalescedEvents : [event]
|
||||||
const latestEvent = pointerEvents[pointerEvents.length - 1] || event
|
const latestEvent = pointerEvents[pointerEvents.length - 1] || event
|
||||||
if (rect) hoverX.value = latestEvent.clientX - rect.left
|
if (rect) hoverX.value = latestEvent.clientX - rect.left
|
||||||
if (!drag.value) {
|
if (!drag.value) {
|
||||||
scheduleDraw()
|
scheduleDraw()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (drag.value.pointerType === 'touch') {
|
||||||
|
scheduleDraw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDistance = Math.abs(latestEvent.clientX - drag.value.startX)
|
||||||
|
const threshold = 3
|
||||||
|
if (!drag.value.moved && totalDistance < threshold) {
|
||||||
|
scheduleDraw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drag.value.moved = true
|
||||||
|
|
||||||
pointerEvents.forEach((pointerEvent) => {
|
pointerEvents.forEach((pointerEvent) => {
|
||||||
const now = pointerEvent.timeStamp || performance.now()
|
const now = pointerEvent.timeStamp || performance.now()
|
||||||
const deltaX = pointerEvent.clientX - drag.value!.lastX
|
const deltaX = pointerEvent.clientX - drag.value!.lastX
|
||||||
const deltaTime = Math.max(1, now - drag.value!.time)
|
const deltaTime = Math.max(1, now - drag.value!.time)
|
||||||
velocity.value = deltaX / deltaTime
|
velocity.value = deltaX / deltaTime
|
||||||
pan(deltaX)
|
pan(deltaX)
|
||||||
drag.value = { lastX: pointerEvent.clientX, time: now }
|
drag.value = { ...drag.value!, lastX: pointerEvent.clientX, time: now, moved: true }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
const handlePointerUp = () => {
|
||||||
|
const shouldInertia = Boolean(drag.value?.moved)
|
||||||
drag.value = null
|
drag.value = null
|
||||||
cancelInertia()
|
cancelInertia()
|
||||||
inertiaRafId.value = window.requestAnimationFrame(applyInertia)
|
if (shouldInertia) inertiaRafId.value = window.requestAnimationFrame(applyInertia)
|
||||||
|
else scheduleDraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerCancel = () => {
|
const handlePointerCancel = () => {
|
||||||
|
|||||||
@@ -80,10 +80,10 @@
|
|||||||
|
|
||||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 2xl:grid-cols-3">
|
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 2xl:grid-cols-3">
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg 2xl:col-span-2"
|
class="theme-2026-card theme-2026-card-hover rounded-[24px] p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-5 2xl:col-span-2"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between sm:mb-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||||
{{ t('admin.dashboard.fileHealth') }}
|
{{ t('admin.dashboard.fileHealth') }}
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||||
<MetricProgress
|
<MetricProgress
|
||||||
:label="t('admin.dashboard.activeFileRatio')"
|
:label="t('admin.dashboard.activeFileRatio')"
|
||||||
:value="dashboardData.activeRatio"
|
:value="dashboardData.activeRatio"
|
||||||
@@ -119,24 +119,24 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5">
|
<div class="mt-4 grid grid-cols-2 gap-2 sm:mt-6 sm:gap-3 xl:grid-cols-3 2xl:grid-cols-5">
|
||||||
<button
|
<button
|
||||||
v-for="action in healthActions"
|
v-for="action in healthActions"
|
||||||
:key="action.key"
|
:key="action.key"
|
||||||
type="button"
|
type="button"
|
||||||
class="theme-2026-card-hover group flex min-h-24 flex-col justify-between rounded-[24px] border p-4 text-left shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:rounded-[30px]"
|
class="theme-2026-card-hover group flex min-h-20 flex-col justify-between rounded-[20px] border p-3 text-left shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:min-h-24 sm:rounded-[30px] sm:p-4"
|
||||||
:class="getHealthActionClass(action.tone)"
|
:class="getHealthActionClass(action.tone)"
|
||||||
@click="openHealthQueue(action.health)"
|
@click="openHealthQueue(action.health)"
|
||||||
>
|
>
|
||||||
<span class="flex items-start justify-between gap-3">
|
<span class="flex items-start justify-between gap-3">
|
||||||
<span>
|
<span>
|
||||||
<span class="block text-2xl font-semibold">{{ action.count }}</span>
|
<span class="block text-xl font-semibold sm:text-2xl">{{ action.count }}</span>
|
||||||
<span class="mt-1 block text-sm font-medium">{{ action.label }}</span>
|
<span class="mt-1 block text-sm font-medium">{{ action.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
<component :is="getHealthActionIcon(action.tone)" class="h-5 w-5 shrink-0" />
|
<component :is="getHealthActionIcon(action.tone)" class="h-5 w-5 shrink-0" />
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-3 flex items-center justify-between gap-2 text-xs">
|
<span class="mt-2 flex items-center justify-between gap-2 text-xs sm:mt-3">
|
||||||
<span class="line-clamp-2">{{ action.description }}</span>
|
<span class="hidden line-clamp-2 sm:block">{{ action.description }}</span>
|
||||||
<ArrowRightIcon
|
<ArrowRightIcon
|
||||||
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
||||||
/>
|
/>
|
||||||
@@ -144,9 +144,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="mt-4 grid grid-cols-2 gap-2 sm:mt-6 sm:gap-4">
|
||||||
<div
|
<div
|
||||||
class="theme-2026-card-hover rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card-hover rounded-[20px] border p-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:rounded-[30px] sm:p-4 sm:shadow-md"
|
||||||
:class="[subtlePanelClass, cardHoverClass]"
|
:class="[subtlePanelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<p class="text-sm" :class="[mutedTextClass]">
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="theme-2026-card-hover rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card-hover rounded-[20px] border p-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:rounded-[30px] sm:p-4 sm:shadow-md"
|
||||||
:class="[subtlePanelClass, cardHoverClass]"
|
:class="[subtlePanelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<p class="text-sm" :class="[mutedTextClass]">
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover rounded-[24px] p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-5"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
<div class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover rounded-[24px] p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-5"
|
||||||
: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">
|
||||||
@@ -239,6 +239,7 @@
|
|||||||
<p class="text-sm" :class="[mutedTextClass]">
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
{{ analyticsSummaryText }}
|
{{ analyticsSummaryText }}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mt-1 text-xs sm:hidden" :class="[mutedTextClass]">轻触或滑动查看数据,使用下方按钮调整范围</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-full space-y-3 lg:w-auto">
|
<div class="w-full max-w-full space-y-3 lg:w-auto">
|
||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
@@ -309,7 +310,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover rounded-[24px] p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-5"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-5 flex items-center justify-between">
|
||||||
@@ -631,12 +632,12 @@ const MetricProgress = defineComponent({
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class:
|
class:
|
||||||
'theme-2026-card-hover rounded-[30px] border border-[#d4e0e3] bg-[#f6faf9] p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:border-[#8fb2bf] hover:shadow-lg hover:shadow-[#a9bdc2]/60 dark:border-[#40545c] dark:bg-[#17252c]/50 dark:hover:border-[#7199a8] dark:hover:bg-[#263941] dark:hover:shadow-black/25'
|
'theme-2026-card-hover rounded-[18px] border border-[#d4e0e3] bg-[#f6faf9] p-2.5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-[#8fb2bf] hover:shadow-md sm:rounded-[30px] sm:p-4 sm:shadow-md dark:border-[#40545c] dark:bg-[#17252c]/50 dark:hover:border-[#7199a8] dark:hover:bg-[#263941] dark:hover:shadow-black/25'
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
h('div', { class: 'mb-2 flex items-center justify-between text-sm' }, [
|
h('div', { class: 'mb-2 flex flex-col gap-0.5 text-xs sm:flex-row sm:items-center sm:justify-between sm:text-sm' }, [
|
||||||
h('span', { class: 'text-gray-500 dark:text-gray-400' }, props.label),
|
h('span', { class: 'line-clamp-1 text-gray-500 dark:text-gray-400' }, props.label),
|
||||||
h('span', { class: 'font-medium text-gray-900 dark:text-white' }, `${props.value}%`)
|
h('span', { class: 'font-medium tabular-nums 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-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700' }, [
|
||||||
h('div', {
|
h('div', {
|
||||||
@@ -644,7 +645,7 @@ const MetricProgress = defineComponent({
|
|||||||
style: { width: `${props.value}%` }
|
style: { width: `${props.value}%` }
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
h('p', { class: 'mt-2 text-sm text-gray-500 dark:text-gray-400' }, props.detail)
|
h('p', { class: 'mt-2 hidden text-sm text-gray-500 sm:block dark:text-gray-400' }, props.detail)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user