diff --git a/src/components/cards/PosterCard.vue b/src/components/cards/PosterCard.vue
index e8039e26..36c1d848 100644
--- a/src/components/cards/PosterCard.vue
+++ b/src/components/cards/PosterCard.vue
@@ -17,6 +17,12 @@ const isImageLoaded = ref(false)
// 图片加载失败
const imageLoadError = ref(false)
+const cardStyle = computed(() => ({
+ aspectRatio: props.height ? undefined : '2 / 3',
+ blockSize: props.height,
+ inlineSize: props.width || '100%',
+}))
+
// 角标颜色
function getChipColor(type: string) {
if (type === '电影') return 'border-blue-500 bg-blue-600'
@@ -50,8 +56,7 @@ async function goPlay(isHovering: boolean | null = false) {
{{ props.media?.type }}
@@ -101,7 +107,34 @@ async function goPlay(isHovering: boolean | null = false) {
diff --git a/src/composables/useDashboardMotion.ts b/src/composables/useDashboardMotion.ts
new file mode 100644
index 00000000..24a0094f
--- /dev/null
+++ b/src/composables/useDashboardMotion.ts
@@ -0,0 +1,41 @@
+import { TransitionPresets, usePreferredReducedMotion, useTransition, type UseTransitionOptions } from '@vueuse/core'
+import { computed, type MaybeRefOrGetter } from 'vue'
+
+const fileSizeUnits = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] as const
+
+export function useDashboardMotionDisabled() {
+ const preferredMotion = usePreferredReducedMotion()
+
+ return computed(() => preferredMotion.value === 'reduce')
+}
+
+export function useAnimatedDashboardNumber(source: MaybeRefOrGetter, options: UseTransitionOptions = {}) {
+ const disabled = useDashboardMotionDisabled()
+
+ return useTransition(source, {
+ duration: 420,
+ transition: TransitionPresets.easeOutQuart,
+ disabled,
+ ...options,
+ })
+}
+
+export function formatDashboardCount(value: number) {
+ return Math.round(Math.max(Number(value) || 0, 0)).toLocaleString()
+}
+
+export function formatDashboardFileSize(bytes: number, decimals = 2, targetBytes = bytes) {
+ let size = Math.abs(Number(targetBytes) || Number(bytes) || 0)
+ let unitIndex = 0
+
+ while (size >= 1024 && unitIndex < fileSizeUnits.length - 1) {
+ size /= 1024
+ unitIndex++
+ }
+
+ const divisor = 1024 ** unitIndex
+ const value = (Math.abs(Number(bytes) || 0) / divisor).toFixed(decimals)
+ const prefix = bytes < 0 ? '-' : ''
+
+ return `${prefix}${value} ${fileSizeUnits[unitIndex]}`
+}
diff --git a/src/views/dashboard/AnalyticsCpu.vue b/src/views/dashboard/AnalyticsCpu.vue
index d4e78642..32aa2a23 100644
--- a/src/views/dashboard/AnalyticsCpu.vue
+++ b/src/views/dashboard/AnalyticsCpu.vue
@@ -2,6 +2,7 @@
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import api from '@/api'
+import { useAnimatedDashboardNumber } from '@/composables/useDashboardMotion'
import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
@@ -39,6 +40,10 @@ const series = ref([
// 当前值
const current = ref(0)
+const animatedCurrent = useAnimatedDashboardNumber(current, {
+ duration: 520,
+})
+const animatedCurrentText = computed(() => Math.round(animatedCurrent.value).toLocaleString())
const chartOptions = controlledComputed(
() => vuetifyTheme.name.value,
@@ -109,7 +114,7 @@ async function loadCpuData() {
if (!props.allowRefresh) return
try {
// 请求数据
- current.value = (await api.get('dashboard/cpu')) ?? 0
+ current.value = Number(await api.get('dashboard/cpu')) || 0
// 使用nextTick确保DOM更新完成后再更新图表数据
await nextTick()
// 添加到序列
@@ -141,7 +146,9 @@ useKeepAliveRefresh(refresh)
- {{ t('dashboard.current') }}:{{ current }}%
+
+ {{ t('dashboard.current') }}:{{ animatedCurrentText }}%
+
@@ -159,4 +166,8 @@ useKeepAliveRefresh(refresh)
min-block-size: 0;
}
+.dashboard-chart-value {
+ font-variant-numeric: tabular-nums;
+}
+
diff --git a/src/views/dashboard/AnalyticsMediaStatistic.vue b/src/views/dashboard/AnalyticsMediaStatistic.vue
index 8d7d3a1b..0875b43f 100644
--- a/src/views/dashboard/AnalyticsMediaStatistic.vue
+++ b/src/views/dashboard/AnalyticsMediaStatistic.vue
@@ -1,44 +1,72 @@