Merge pull request 'test/custom-admin-ui' (#1) from test/custom-admin-ui into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -70,16 +70,20 @@ const offset = ref(0)
|
|||||||
const windowSize = ref(30)
|
const windowSize = ref(30)
|
||||||
const drag = ref<DragState | null>(null)
|
const drag = ref<DragState | null>(null)
|
||||||
const hoverX = ref<number | null>(null)
|
const hoverX = ref<number | null>(null)
|
||||||
|
const hoverVisual = ref<{ x: number; downloadY: number; uploadY: number } | null>(null)
|
||||||
const velocity = ref(0)
|
const velocity = ref(0)
|
||||||
const rafId = ref<number | null>(null)
|
const rafId = ref<number | null>(null)
|
||||||
const inertiaRafId = ref<number | null>(null)
|
const inertiaRafId = ref<number | null>(null)
|
||||||
const lastInertiaTime = ref(0)
|
const lastInertiaTime = ref(0)
|
||||||
|
const lastHoverTime = ref(0)
|
||||||
const tooltip = ref<TooltipState | null>(null)
|
const tooltip = ref<TooltipState | null>(null)
|
||||||
|
|
||||||
const INERTIA_REFERENCE_FRAME_MS = 1000 / 120
|
const INERTIA_REFERENCE_FRAME_MS = 1000 / 120
|
||||||
const INERTIA_MAX_FRAME_MS = 1000 / 30
|
const INERTIA_MAX_FRAME_MS = 1000 / 30
|
||||||
const INERTIA_DECAY_PER_120HZ_FRAME = 0.96
|
const INERTIA_DECAY_PER_120HZ_FRAME = 0.96
|
||||||
const INERTIA_STOP_VELOCITY = 0.01
|
const INERTIA_STOP_VELOCITY = 0.01
|
||||||
|
const HOVER_EASE_PER_120HZ_FRAME = 0.34
|
||||||
|
const HOVER_SNAP_DISTANCE = 0.2
|
||||||
|
|
||||||
const panelClass = computed(() =>
|
const panelClass = computed(() =>
|
||||||
props.isDarkMode
|
props.isDarkMode
|
||||||
@@ -118,13 +122,13 @@ const clampState = () => {
|
|||||||
|
|
||||||
const scheduleDraw = () => {
|
const scheduleDraw = () => {
|
||||||
if (rafId.value !== null) return
|
if (rafId.value !== null) return
|
||||||
rafId.value = window.requestAnimationFrame(() => {
|
rafId.value = window.requestAnimationFrame((timestamp) => {
|
||||||
rafId.value = null
|
rafId.value = null
|
||||||
draw()
|
draw(timestamp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const draw = () => {
|
const draw = (timestamp = performance.now()) => {
|
||||||
const canvas = canvasRef.value
|
const canvas = canvasRef.value
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
@@ -266,19 +270,40 @@ const draw = () => {
|
|||||||
const uploadTraffic = Number(row.uploadTraffic || 0)
|
const uploadTraffic = Number(row.uploadTraffic || 0)
|
||||||
const downloadY = padTop + height - (downloads / max) * height
|
const downloadY = padTop + height - (downloads / max) * height
|
||||||
const uploadY = padTop + height - (uploads / max) * height
|
const uploadY = padTop + height - (uploads / max) * height
|
||||||
const anchorY = Math.min(downloadY, uploadY)
|
const elapsed = lastHoverTime.value
|
||||||
|
? Math.min(timestamp - lastHoverTime.value, INERTIA_MAX_FRAME_MS)
|
||||||
|
: INERTIA_REFERENCE_FRAME_MS
|
||||||
|
lastHoverTime.value = timestamp
|
||||||
|
|
||||||
|
if (!hoverVisual.value) {
|
||||||
|
hoverVisual.value = { x: snapX, downloadY, uploadY }
|
||||||
|
} else {
|
||||||
|
const ease = 1 - Math.pow(1 - HOVER_EASE_PER_120HZ_FRAME, elapsed / INERTIA_REFERENCE_FRAME_MS)
|
||||||
|
const nextX = hoverVisual.value.x + (snapX - hoverVisual.value.x) * ease
|
||||||
|
const nextDownloadY = hoverVisual.value.downloadY + (downloadY - hoverVisual.value.downloadY) * ease
|
||||||
|
const nextUploadY = hoverVisual.value.uploadY + (uploadY - hoverVisual.value.uploadY) * ease
|
||||||
|
hoverVisual.value = {
|
||||||
|
x: Math.abs(nextX - snapX) < HOVER_SNAP_DISTANCE ? snapX : nextX,
|
||||||
|
downloadY:
|
||||||
|
Math.abs(nextDownloadY - downloadY) < HOVER_SNAP_DISTANCE ? downloadY : nextDownloadY,
|
||||||
|
uploadY: Math.abs(nextUploadY - uploadY) < HOVER_SNAP_DISTANCE ? uploadY : nextUploadY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visual = hoverVisual.value
|
||||||
|
const anchorY = Math.min(visual.downloadY, visual.uploadY)
|
||||||
const placeBelow = anchorY < 104
|
const placeBelow = anchorY < 104
|
||||||
const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8))
|
const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8))
|
||||||
|
|
||||||
ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)'
|
ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)'
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(snapX, padTop)
|
ctx.moveTo(visual.x, padTop)
|
||||||
ctx.lineTo(snapX, padTop + height)
|
ctx.lineTo(visual.x, padTop + height)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
tooltip.value = {
|
tooltip.value = {
|
||||||
left: clamp(snapX, tooltipInset, rect.width - tooltipInset),
|
left: clamp(visual.x, tooltipInset, rect.width - tooltipInset),
|
||||||
top: placeBelow ? anchorY + 16 : anchorY - 12,
|
top: placeBelow ? anchorY + 16 : anchorY - 12,
|
||||||
date: row.date,
|
date: row.date,
|
||||||
downloads,
|
downloads,
|
||||||
@@ -287,9 +312,19 @@ const draw = () => {
|
|||||||
uploadTraffic: formatBytes(uploadTraffic),
|
uploadTraffic: formatBytes(uploadTraffic),
|
||||||
transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)'
|
transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)'
|
||||||
}
|
}
|
||||||
drawHoverPoint({ x: snapX, y: downloadY }, downloadColor)
|
drawHoverPoint({ x: visual.x, y: visual.downloadY }, downloadColor)
|
||||||
drawHoverPoint({ x: snapX, y: uploadY }, uploadColor)
|
drawHoverPoint({ x: visual.x, y: visual.uploadY }, uploadColor)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(visual.x - snapX) >= HOVER_SNAP_DISTANCE ||
|
||||||
|
Math.abs(visual.downloadY - downloadY) >= HOVER_SNAP_DISTANCE ||
|
||||||
|
Math.abs(visual.uploadY - uploadY) >= HOVER_SNAP_DISTANCE
|
||||||
|
) {
|
||||||
|
scheduleDraw()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
hoverVisual.value = null
|
||||||
|
lastHoverTime.value = 0
|
||||||
tooltip.value = null
|
tooltip.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +368,7 @@ const applyInertia = (timestamp: number) => {
|
|||||||
lastInertiaTime.value = timestamp
|
lastInertiaTime.value = timestamp
|
||||||
|
|
||||||
pan(velocity.value * elapsed, false)
|
pan(velocity.value * elapsed, false)
|
||||||
draw()
|
draw(timestamp)
|
||||||
velocity.value *= Math.pow(
|
velocity.value *= Math.pow(
|
||||||
INERTIA_DECAY_PER_120HZ_FRAME,
|
INERTIA_DECAY_PER_120HZ_FRAME,
|
||||||
elapsed / INERTIA_REFERENCE_FRAME_MS
|
elapsed / INERTIA_REFERENCE_FRAME_MS
|
||||||
@@ -385,6 +420,8 @@ const handlePointerCancel = () => {
|
|||||||
const handlePointerLeave = () => {
|
const handlePointerLeave = () => {
|
||||||
drag.value = null
|
drag.value = null
|
||||||
hoverX.value = null
|
hoverX.value = null
|
||||||
|
hoverVisual.value = null
|
||||||
|
lastHoverTime.value = 0
|
||||||
scheduleDraw()
|
scheduleDraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export interface AnalyticsFileRow {
|
|||||||
upload_count?: number
|
upload_count?: number
|
||||||
upload_traffic?: number | string
|
upload_traffic?: number | string
|
||||||
current: boolean
|
current: boolean
|
||||||
|
expired?: boolean
|
||||||
deleted?: boolean
|
deleted?: boolean
|
||||||
created_at?: string
|
created_at?: string
|
||||||
uploaded_at?: string
|
uploaded_at?: string
|
||||||
|
|||||||
@@ -9,19 +9,6 @@
|
|||||||
{{ t('fileManage.subtitle', { count: summary.totalFiles }) }}
|
{{ t('fileManage.subtitle', { count: summary.totalFiles }) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<BaseButton
|
|
||||||
v-if="hasActiveFilters"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="handleResetFilters"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<XIcon class="mr-2 h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
{{ t('fileManage.resetFilters') }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
@@ -59,6 +46,18 @@
|
|||||||
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
|
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<BaseButton
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="handleResetFilters"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<XIcon class="mr-2 h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ t('fileManage.resetFilters') }}
|
||||||
|
</BaseButton>
|
||||||
<BaseButton variant="secondary" @click="isFilterPanelOpen = !isFilterPanelOpen">
|
<BaseButton variant="secondary" @click="isFilterPanelOpen = !isFilterPanelOpen">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<FilterIcon class="mr-2 h-4 w-4" />
|
<FilterIcon class="mr-2 h-4 w-4" />
|
||||||
@@ -66,6 +65,7 @@
|
|||||||
{{ filterBodyVisible ? '收起筛选' : '展开筛选' }}
|
{{ filterBodyVisible ? '收起筛选' : '展开筛选' }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-show="filterBodyVisible" class="mt-4 grid gap-4">
|
<div v-show="filterBodyVisible" class="mt-4 grid gap-4">
|
||||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center">
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
{{ formatHistoryBytes(file.download_traffic) }}
|
{{ formatHistoryBytes(file.download_traffic) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
|
<td class="px-4 py-3 text-sm" :class="[mutedTextClass]">
|
||||||
{{ file.deleted ? '已删除' : file.current ? '现存' : '历史' }}
|
{{ getHistoryStatusLabel(file) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -1613,6 +1613,12 @@ const handleHistoryPageSizeChange = (size: number) => {
|
|||||||
const formatHistoryBytes = (value: number | string | undefined) =>
|
const formatHistoryBytes = (value: number | string | undefined) =>
|
||||||
formatFileSize(Number(value || 0), 1)
|
formatFileSize(Number(value || 0), 1)
|
||||||
const formatHistoryDate = (value?: string) => (value ? formatTimestamp(value) : '-')
|
const formatHistoryDate = (value?: string) => (value ? formatTimestamp(value) : '-')
|
||||||
|
const getHistoryStatusLabel = (file: AnalyticsFileRow) => {
|
||||||
|
if (file.deleted) return '已删除'
|
||||||
|
if (file.expired) return '已过期'
|
||||||
|
if (file.current) return '现存'
|
||||||
|
return '历史'
|
||||||
|
}
|
||||||
|
|
||||||
const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900'))
|
const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900'))
|
||||||
const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500'))
|
const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500'))
|
||||||
|
|||||||
Reference in New Issue
Block a user