Animate dashboard metrics and standardize card sizing

This commit is contained in:
jxxghp
2026-06-25 11:28:36 +08:00
parent 530fe9d35b
commit 175f610524
13 changed files with 392 additions and 100 deletions

View File

@@ -13,6 +13,12 @@ const props = defineProps({
const imageLoaded = ref(false)
const imageLoadError = ref(false)
const cardStyle = computed(() => ({
aspectRatio: props.height ? undefined : '3 / 2',
blockSize: props.height,
inlineSize: props.width || '100%',
}))
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
@@ -49,8 +55,7 @@ const getImgUrl = computed(() => {
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="backdrop-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
:style="cardStyle"
class="app-hover-lift-card ring-gray-500"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
@@ -59,10 +64,18 @@ const getImgUrl = computed(() => {
@click="goPlay"
>
<template #image>
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<VImg
:src="getImgUrl"
aspect-ratio="2/3"
class="backdrop-card-image"
:class="{ 'backdrop-card-image--loaded': imageLoaded }"
cover
@load="imageLoadHandler"
@error="imageErrorHandler"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
<div class="backdrop-card-placeholder">
<VSkeletonLoader class="backdrop-card-skeleton" />
</div>
</template>
<template #default>
@@ -94,7 +107,36 @@ const getImgUrl = computed(() => {
</template>
<style scoped>
/* stylelint-disable selector-pseudo-class-no-unknown */
.backdrop-card-hover-area {
block-size: 100%;
inline-size: 100%;
}
.backdrop-card-image {
block-size: 100%;
inline-size: 100%;
}
.backdrop-card-placeholder,
.backdrop-card-skeleton {
block-size: 100%;
inline-size: 100%;
}
.backdrop-card-image :deep(.v-img__img) {
opacity: 0;
transition: opacity 0.2s ease;
}
.backdrop-card-image--loaded :deep(.v-img__img) {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.backdrop-card-image :deep(.v-img__img) {
transition: none;
}
}
</style>

View File

@@ -25,6 +25,12 @@ const imageLoaded = ref(false)
// 图片是否加载错误
const imageError = ref(false)
const cardStyle = computed(() => ({
aspectRatio: props.height ? undefined : '3 / 2',
blockSize: props.height,
inlineSize: props.width || '100%',
}))
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
@@ -159,8 +165,7 @@ onMounted(async () => {
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="library-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
:style="cardStyle"
class="app-hover-lift-card"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
@@ -169,10 +174,18 @@ onMounted(async () => {
>
<template #image>
<canvas ref="canvasRef" width="640" height="360" class="w-full h-full hidden" />
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<VImg
:src="imgUrl"
aspect-ratio="2/3"
class="library-card-image"
:class="{ 'library-card-image--loaded': imageLoaded }"
cover
@load="imageLoadHandler"
@error="imageErrorHandler"
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
<div class="library-card-placeholder">
<VSkeletonLoader class="library-card-skeleton" />
</div>
</template>
<template #default>
@@ -193,7 +206,36 @@ onMounted(async () => {
</template>
<style scoped>
/* stylelint-disable selector-pseudo-class-no-unknown */
.library-card-hover-area {
block-size: 100%;
inline-size: 100%;
}
.library-card-image {
block-size: 100%;
inline-size: 100%;
}
.library-card-placeholder,
.library-card-skeleton {
block-size: 100%;
inline-size: 100%;
}
.library-card-image :deep(.v-img__img) {
opacity: 0;
transition: opacity 0.2s ease;
}
.library-card-image--loaded :deep(.v-img__img) {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.library-card-image :deep(.v-img__img) {
transition: none;
}
}
</style>

View File

@@ -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) {
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="poster-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
:style="cardStyle"
class="app-hover-lift-card outline-none ring-gray-500"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
@@ -61,7 +66,8 @@ async function goPlay(isHovering: boolean | null = false) {
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
class="poster-card-image object-cover aspect-w-2 aspect-h-3"
:class="{ 'poster-card-image--loaded': isImageLoaded }"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
@@ -78,7 +84,7 @@ async function goPlay(isHovering: boolean | null = false) {
variant="elevated"
size="small"
:class="getChipColor(props.media?.type || '')"
class="absolute left-2 top-2 bg-opacity-80 text-white font-bold"
class="poster-card-chip absolute left-2 top-2 bg-opacity-80 text-white font-bold"
>
{{ props.media?.type }}
</VChip>
@@ -101,7 +107,34 @@ async function goPlay(isHovering: boolean | null = false) {
</template>
<style scoped>
/* stylelint-disable selector-pseudo-class-no-unknown */
.poster-card-hover-area {
block-size: 100%;
inline-size: 100%;
}
.poster-card-image {
block-size: 100%;
inline-size: 100%;
}
.poster-card-image :deep(.v-img__img) {
opacity: 0;
transition: opacity 0.2s ease;
}
.poster-card-image--loaded :deep(.v-img__img) {
opacity: 1;
}
.poster-card-image :deep(.v-responsive__sizer) {
padding-bottom: 150%;
}
@media (prefers-reduced-motion: reduce) {
.poster-card-image :deep(.v-img__img) {
transition: none;
}
}
</style>