feat(analytics): 添加数字滚动动画并优化数字显示样式
- 新增 CountTo 组件实现平滑数字滚动效果,提升用户体验 - 在统计数字区域应用等宽字体和字体回退方案,防止滚动时宽度抖动 - 替换静态数字显示为动态滚动动画,增强数据展示的视觉吸引力
This commit is contained in:
68
cwd-admin/src/components/CountTo.vue
Normal file
68
cwd-admin/src/components/CountTo.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<span>{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
startVal: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
endVal: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
}
|
||||
});
|
||||
|
||||
const displayValue = ref(props.startVal);
|
||||
const localStartVal = ref(props.startVal);
|
||||
const startTime = ref<number | null>(null);
|
||||
const rAF = ref<number | null>(null);
|
||||
|
||||
const easeOutExpo = (t: number, b: number, c: number, d: number) => {
|
||||
return t === d ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
|
||||
};
|
||||
|
||||
const count = (timestamp: number) => {
|
||||
if (!startTime.value) startTime.value = timestamp;
|
||||
const progress = timestamp - startTime.value;
|
||||
|
||||
if (props.duration > 0 && progress < props.duration) {
|
||||
const val = easeOutExpo(progress, localStartVal.value, props.endVal - localStartVal.value, props.duration);
|
||||
displayValue.value = Math.floor(val);
|
||||
rAF.value = requestAnimationFrame(count);
|
||||
} else {
|
||||
displayValue.value = props.endVal;
|
||||
rAF.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
startTime.value = null;
|
||||
localStartVal.value = displayValue.value;
|
||||
if (rAF.value) cancelAnimationFrame(rAF.value);
|
||||
rAF.value = requestAnimationFrame(count);
|
||||
};
|
||||
|
||||
watch(() => props.endVal, () => {
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.endVal !== props.startVal) {
|
||||
startAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (rAF.value) {
|
||||
cancelAnimationFrame(rAF.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -46,6 +46,10 @@
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
/* 使用等宽数字,防止数字滚动时宽度抖动 */
|
||||
font-variant-numeric: tabular-nums;
|
||||
/* 确保数字字体回退到系统等宽字体,增强兼容性 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.trend {
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
<div class="stats-grid">
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">全站总访问量</div>
|
||||
<div class="stats-value">{{ overview.totalPv }}</div>
|
||||
<div class="stats-value"><CountTo :end-val="overview.totalPv" /></div>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">今日访问量</div>
|
||||
<div class="stats-value">
|
||||
{{ overview.todayPv }}
|
||||
<CountTo :end-val="overview.todayPv" />
|
||||
<span
|
||||
v-if="overview.yesterdayPv !== undefined"
|
||||
class="trend"
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">本周访问量</div>
|
||||
<div class="stats-value">
|
||||
{{ overview.weekPv }}
|
||||
<CountTo :end-val="overview.weekPv" />
|
||||
<span
|
||||
v-if="overview.lastWeekPv !== undefined"
|
||||
class="trend"
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">本月访问量</div>
|
||||
<div class="stats-value">
|
||||
{{ overview.monthPv }}
|
||||
<CountTo :end-val="overview.monthPv" />
|
||||
<span
|
||||
v-if="overview.lastMonthPv !== undefined"
|
||||
class="trend"
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">有访问记录的页面数</div>
|
||||
<div class="stats-value">{{ overview.totalPages }}</div>
|
||||
<div class="stats-value"><CountTo :end-val="overview.totalPages" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,6 +215,7 @@
|
||||
import { onMounted, onBeforeUnmount, ref, nextTick, watch, inject, computed } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import CountTo from "../../components/CountTo.vue";
|
||||
import {
|
||||
fetchVisitOverview,
|
||||
fetchVisitPages,
|
||||
|
||||
Reference in New Issue
Block a user