📈 修复趋势坐标轴与移动日期选择
This commit is contained in:
@@ -212,6 +212,32 @@ const updatePart = (part: DatePart, value: string) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.native-date-strip {
|
||||||
|
min-height: 2.35rem;
|
||||||
|
padding-inline: 0.35ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-date-part {
|
||||||
|
height: 2.25rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-date-year {
|
||||||
|
width: 3.7ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-date-month,
|
||||||
|
.native-date-day {
|
||||||
|
width: 1.8ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-date-separator {
|
||||||
|
margin-inline: 0.15ch;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark .native-date-strip {
|
.dark .native-date-strip {
|
||||||
background: rgba(38, 57, 65, 0.62);
|
background: rgba(38, 57, 65, 0.62);
|
||||||
color: #d8e3e5;
|
color: #d8e3e5;
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
type DragState = {
|
type DragState = {
|
||||||
startX: number
|
startOffset: number
|
||||||
lastX: number
|
lastX: number
|
||||||
time: number
|
time: number
|
||||||
pointerType: string
|
pointerType: string
|
||||||
|
isTimeline: boolean
|
||||||
moved: boolean
|
moved: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +163,11 @@ const draw = (timestamp = performance.now()) => {
|
|||||||
const padBottom = mobile ? 42 : 36
|
const padBottom = mobile ? 42 : 36
|
||||||
const width = Math.max(1, rect.width - padLeft - padRight)
|
const width = Math.max(1, rect.width - padLeft - padRight)
|
||||||
const height = Math.max(1, rect.height - padTop - padBottom)
|
const height = Math.max(1, rect.height - padTop - padBottom)
|
||||||
const max = Math.max(
|
const rawMax = Math.max(
|
||||||
1,
|
0,
|
||||||
...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)])
|
...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)])
|
||||||
)
|
)
|
||||||
|
const max = rawMax <= 2 ? 3 : Math.ceil(rawMax / 3) * 3
|
||||||
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
||||||
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
||||||
const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3'
|
const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3'
|
||||||
@@ -185,17 +187,23 @@ const draw = (timestamp = performance.now()) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const denominator = Math.max(1, windowSize.value - 1)
|
const denominator = Math.max(1, windowSize.value - 1)
|
||||||
|
const plotLeft = padLeft
|
||||||
|
const plotRight = padLeft + width
|
||||||
const buildPoints = (field: 'downloads' | 'uploads') =>
|
const buildPoints = (field: 'downloads' | 'uploads') =>
|
||||||
visible
|
visible
|
||||||
.map((row, index) => {
|
.map((row, index) => {
|
||||||
const sourceIndex = start + index
|
const sourceIndex = start + index
|
||||||
return {
|
return {
|
||||||
x: padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
x: clamp(
|
||||||
|
padLeft + ((sourceIndex - offset.value) / denominator) * width,
|
||||||
|
plotLeft,
|
||||||
|
plotRight
|
||||||
|
),
|
||||||
y: padTop + height - (Number(row[field] || 0) / max) * height,
|
y: padTop + height - (Number(row[field] || 0) / max) * height,
|
||||||
row
|
row
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((point) => point.x >= padLeft - 50 && point.x <= padLeft + width + 50)
|
.filter((point) => point.x >= plotLeft && point.x <= plotRight)
|
||||||
|
|
||||||
const drawSeries = (points: ChartPoint[], color: string) => {
|
const drawSeries = (points: ChartPoint[], color: string) => {
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = color
|
||||||
@@ -392,17 +400,21 @@ const setHoverFromClientX = (clientX: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
const rect = canvasRef.value?.getBoundingClientRect()
|
||||||
canvasRef.value?.setPointerCapture(event.pointerId)
|
canvasRef.value?.setPointerCapture(event.pointerId)
|
||||||
cancelInertia()
|
cancelInertia()
|
||||||
velocity.value = 0
|
velocity.value = 0
|
||||||
setHoverFromClientX(event.clientX)
|
setHoverFromClientX(event.clientX)
|
||||||
|
const isTimeline = Boolean(rect && event.clientY - rect.top >= rect.height - 52)
|
||||||
drag.value = {
|
drag.value = {
|
||||||
startX: event.clientX,
|
startOffset: offset.value,
|
||||||
lastX: event.clientX,
|
lastX: event.clientX,
|
||||||
time: event.timeStamp || performance.now(),
|
time: event.timeStamp || performance.now(),
|
||||||
pointerType: event.pointerType,
|
pointerType: event.pointerType,
|
||||||
|
isTimeline,
|
||||||
moved: false
|
moved: false
|
||||||
}
|
}
|
||||||
|
if (isTimeline) canvasRef.value!.style.touchAction = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
@@ -416,12 +428,12 @@ const handlePointerMove = (event: PointerEvent) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drag.value.pointerType === 'touch') {
|
if (drag.value.pointerType === 'touch' && !drag.value.isTimeline) {
|
||||||
scheduleDraw()
|
scheduleDraw()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDistance = Math.abs(latestEvent.clientX - drag.value.startX)
|
const totalDistance = Math.abs(latestEvent.clientX - drag.value.lastX)
|
||||||
const threshold = 3
|
const threshold = 3
|
||||||
if (!drag.value.moved && totalDistance < threshold) {
|
if (!drag.value.moved && totalDistance < threshold) {
|
||||||
scheduleDraw()
|
scheduleDraw()
|
||||||
@@ -440,15 +452,18 @@ const handlePointerMove = (event: PointerEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
const handlePointerUp = () => {
|
||||||
|
const timelineDrag = drag.value?.isTimeline
|
||||||
const shouldInertia = Boolean(drag.value?.moved)
|
const shouldInertia = Boolean(drag.value?.moved)
|
||||||
drag.value = null
|
drag.value = null
|
||||||
|
if (canvasRef.value) canvasRef.value.style.touchAction = ''
|
||||||
cancelInertia()
|
cancelInertia()
|
||||||
if (shouldInertia) inertiaRafId.value = window.requestAnimationFrame(applyInertia)
|
if (shouldInertia && !timelineDrag) inertiaRafId.value = window.requestAnimationFrame(applyInertia)
|
||||||
else scheduleDraw()
|
else scheduleDraw()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerCancel = () => {
|
const handlePointerCancel = () => {
|
||||||
drag.value = null
|
drag.value = null
|
||||||
|
if (canvasRef.value) canvasRef.value.style.touchAction = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerLeave = () => {
|
const handlePointerLeave = () => {
|
||||||
|
|||||||
@@ -239,10 +239,9 @@
|
|||||||
<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>
|
<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="flex w-full min-w-0 items-center justify-center gap-1.5 sm:w-auto sm:flex-row sm:gap-2">
|
||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
||||||
<NativeDateSelect
|
<NativeDateSelect
|
||||||
v-model="analyticsStart"
|
v-model="analyticsStart"
|
||||||
:field-class="fieldClass"
|
:field-class="fieldClass"
|
||||||
@@ -250,7 +249,7 @@
|
|||||||
@change="fetchAnalyticsData"
|
@change="fetchAnalyticsData"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="self-center text-sm font-semibold"
|
class="shrink-0 text-sm font-semibold"
|
||||||
:class="[isDarkMode ? 'text-[#aabdc2]' : 'text-[#789096]']"
|
:class="[isDarkMode ? 'text-[#aabdc2]' : 'text-[#789096]']"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
@@ -262,7 +261,7 @@
|
|||||||
@change="fetchAnalyticsData"
|
@change="fetchAnalyticsData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2 sm:flex sm:justify-end">
|
<div class="hidden grid-cols-3 gap-2 sm:flex sm:justify-end">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -288,7 +287,6 @@
|
|||||||
重置
|
重置
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="analyticsError"
|
v-if="analyticsError"
|
||||||
|
|||||||
Reference in New Issue
Block a user