fix: 修复卡片 hover 上浮抖动

This commit is contained in:
jxxghp
2026-06-24 21:34:38 +08:00
parent 48ed396a19
commit 530fe9d35b
21 changed files with 339 additions and 201 deletions
+20 -12
View File
@@ -46,17 +46,18 @@ const getImgUrl = computed(() => {
<template> <template>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="backdrop-card-hover-area">
:height="props.height" <VCard
:width="props.width" :height="props.height"
class="ring-gray-500" :width="props.width"
:class="{ class="app-hover-lift-card ring-gray-500"
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, :class="{
'ring-1': imageLoaded, 'app-hover-lift-card--hovering': hover.isHovering,
}" 'ring-1': imageLoaded,
@click="goPlay" }"
> @click="goPlay"
>
<template #image> <template #image>
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler"> <VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<template #placeholder> <template #placeholder>
@@ -86,7 +87,14 @@ const getImgUrl = computed(() => {
color="success" color="success"
/> />
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</template> </template>
<style scoped>
.backdrop-card-hover-area {
inline-size: 100%;
}
</style>
+16 -11
View File
@@ -73,16 +73,16 @@ async function deleteDownload() {
<template> <template>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-if="cardState" <div v-if="cardState" v-bind="hover.props" class="downloading-card-hover-area h-full">
v-bind="hover.props" <VCard
:key="props.info?.hash" :key="props.info?.hash"
class="downloading-card app-surface flex flex-col h-full overflow-hidden" class="downloading-card app-hover-lift-card app-surface flex flex-col h-full overflow-hidden"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, 'app-hover-lift-card--hovering': hover.isHovering,
}" }"
min-height="150" min-height="150"
> >
<template #image> <template #image>
<VImg <VImg
:src="props.info?.media.image" :src="props.info?.media.image"
@@ -130,7 +130,8 @@ async function deleteDownload() {
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" /> <VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions> </VCardActions>
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</template> </template>
@@ -138,6 +139,10 @@ async function deleteDownload() {
<style lang="scss" scoped> <style lang="scss" scoped>
/* stylelint-disable selector-pseudo-class-no-unknown */ /* stylelint-disable selector-pseudo-class-no-unknown */
.downloading-card-hover-area {
inline-size: 100%;
}
.downloading-card-image { .downloading-card-image {
block-size: 100%; block-size: 100%;
} }
+19 -10
View File
@@ -156,15 +156,17 @@ onMounted(async () => {
<template> <template>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="library-card-hover-area">
:height="props.height" <VCard
:width="props.width" :height="props.height"
:class="{ :width="props.width"
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, class="app-hover-lift-card"
}" :class="{
@click="goPlay" 'app-hover-lift-card--hovering': hover.isHovering,
> }"
@click="goPlay"
>
<template #image> <template #image>
<canvas ref="canvasRef" width="640" height="360" class="w-full h-full hidden" /> <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" cover @load="imageLoadHandler" @error="imageErrorHandler">
@@ -184,7 +186,14 @@ onMounted(async () => {
</template> </template>
</VImg> </VImg>
</template> </template>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</template> </template>
<style scoped>
.library-card-hover-area {
inline-size: 100%;
}
</style>
+2 -2
View File
@@ -498,9 +498,9 @@ onBeforeUnmount(() => {
<VCard <VCard
:height="props.height" :height="props.height"
:width="props.width" :width="props.width"
class="outline-none ring-gray-500 media-card" class="app-hover-lift-card outline-none ring-gray-500 media-card"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, 'app-hover-lift-card--hovering': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }"
@click.stop="goMediaDetail(hover.isHovering ?? false)" @click.stop="goMediaDetail(hover.isHovering ?? false)"
+17 -10
View File
@@ -75,15 +75,17 @@ function goPersonDetail() {
<template> <template>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="person-card-hover-area">
:height="personProps.height" <VCard
:width="personProps.width" :height="personProps.height"
:class="{ :width="personProps.width"
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, class="app-hover-lift-card"
}" :class="{
@click.stop="goPersonDetail" 'app-hover-lift-card--hovering': hover.isHovering,
> }"
@click.stop="goPersonDetail"
>
<div class="person-card relative cursor-pointer ring-gray-700"> <div class="person-card relative cursor-pointer ring-gray-700">
<div style="padding-block-end: 150%"> <div style="padding-block-end: 150%">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2"> <div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
@@ -107,12 +109,17 @@ function goPersonDetail() {
</div> </div>
</div> </div>
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.person-card-hover-area {
inline-size: 100%;
}
.person-card { .person-card {
background-image: linear-gradient( background-image: linear-gradient(
45deg, 45deg,
+17 -11
View File
@@ -230,16 +230,17 @@ onUnmounted(() => {
<div> <div>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="plugin-app-card-hover-area h-full">
:width="props.width" <VCard
:height="props.height" :width="props.width"
@click="showPluginDetail" :height="props.height"
class="flex flex-col h-full" @click="showPluginDetail"
:class="{ class="app-hover-lift-card flex flex-col h-full"
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, :class="{
}" 'app-hover-lift-card--hovering': hover.isHovering,
> }"
>
<div <div
class="flex-grow" class="flex-grow"
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`" :style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
@@ -325,13 +326,18 @@ onUnmounted(() => {
</IconBtn> </IconBtn>
</div> </div>
</VCardText> </VCardText>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.plugin-app-card-hover-area {
inline-size: 100%;
}
.plugin-app-card__tags-section { .plugin-app-card__tags-section {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
+19 -14
View File
@@ -567,19 +567,19 @@ watch(
<!-- 插件卡片 --> <!-- 插件卡片 -->
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-if="isVisible" <div v-if="isVisible" v-bind="hover.props" class="plugin-card-hover-area h-full">
v-bind="hover.props" <VCard
:width="props.width" :width="props.width"
:height="props.height" :height="props.height"
@click="handleCardClick" @click="handleCardClick"
class="flex flex-col h-full" class="app-hover-lift-card flex flex-col h-full"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable, 'app-hover-lift-card--hovering': hover.isHovering && !props.sortable,
'cursor-move': props.sortable, 'cursor-move': props.sortable,
}" }"
:ripple="!props.sortable" :ripple="!props.sortable"
> >
<div <div
class="flex-grow" class="flex-grow"
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`" :style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
@@ -669,7 +669,8 @@ watch(
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5"> <div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
<VIcon icon="mdi-new-box" class="text-white" /> <VIcon icon="mdi-new-box" class="text-white" />
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
@@ -677,6 +678,10 @@ watch(
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.plugin-card-hover-area {
inline-size: 100%;
}
.card-cover-blurred::before { .card-cover-blurred::before {
position: absolute; position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */ /* stylelint-disable-next-line property-no-vendor-prefix */
+22 -17
View File
@@ -211,20 +211,21 @@ const dropdownItems = ref([
<!-- 文件夹卡片 --> <!-- 文件夹卡片 -->
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="plugin-folder-card-hover-area h-full">
:ripple="false" <VCard
:width="props.width" :ripple="false"
:height="props.height" :width="props.width"
min-height="8.5rem" :height="props.height"
@click="handleCardClick" min-height="8.5rem"
class="plugin-folder-card h-full" @click="handleCardClick"
:class="{ class="plugin-folder-card app-hover-lift-card h-full"
'plugin-folder-card--mobile': display.mobile, :class="{
'plugin-folder-card--hover': hover.isHovering && !props.sortable, 'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--sortable': props.sortable, 'plugin-folder-card--hover': hover.isHovering && !props.sortable,
}" 'plugin-folder-card--sortable': props.sortable,
> }"
>
<template v-if="backgroundImage" #image> <template v-if="backgroundImage" #image>
<VImg :src="backgroundImage" cover position="top"> </VImg> <VImg :src="backgroundImage" cover position="top"> </VImg>
</template> </template>
@@ -288,25 +289,29 @@ const dropdownItems = ref([
</VMenu> </VMenu>
</div> </div>
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.plugin-folder-card-hover-area {
inline-size: 100%;
}
.plugin-folder-card { .plugin-folder-card {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--sortable { &--sortable {
cursor: move; cursor: move;
} }
&--hover { &--hover {
transform: translateY(-4px); transform: translate3d(0, -0.25rem, 0);
} }
&__bg { &__bg {
+19 -11
View File
@@ -47,16 +47,17 @@ async function goPlay(isHovering: boolean | null = false) {
<template> <template>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="poster-card-hover-area">
:height="props.height" <VCard
:width="props.width" :height="props.height"
class="outline-none ring-gray-500" :width="props.width"
:class="{ class="app-hover-lift-card outline-none ring-gray-500"
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, :class="{
'ring-1': isImageLoaded, 'app-hover-lift-card--hovering': hover.isHovering,
}" 'ring-1': isImageLoaded,
> }"
>
<VImg <VImg
aspect-ratio="2/3" aspect-ratio="2/3"
:src="getImgUrl" :src="getImgUrl"
@@ -93,7 +94,14 @@ async function goPlay(isHovering: boolean | null = false) {
{{ props.media?.title }} {{ props.media?.title }}
</h1> </h1>
</VCardText> </VCardText>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</template> </template>
<style scoped>
.poster-card-hover-area {
inline-size: 100%;
}
</style>
+33 -22
View File
@@ -239,25 +239,27 @@ onMounted(() => {
<template> <template>
<div> <div>
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
class="site-card relative h-full flex flex-col overflow-hidden group transition-all duration-300" <div class="site-card-hover-area h-full">
:class="[ <VCard
cardProps.site?.is_active ? '' : 'opacity-70', class="site-card app-hover-lift-card relative h-full flex flex-col overflow-hidden group"
{ :class="[
'border-error': statColor === 'error', cardProps.site?.is_active ? '' : 'opacity-70',
'border-warning': statColor === 'warning', {
'border-success': statColor === 'success', 'border-error': statColor === 'error',
'cursor-pointer hover:-translate-y-1': !cardProps.sortable, 'border-warning': statColor === 'warning',
'cursor-move': cardProps.sortable, 'border-success': statColor === 'success',
'site-card--sortable': cardProps.sortable, 'cursor-pointer site-card--hoverable': !cardProps.sortable,
}, 'cursor-move': cardProps.sortable,
]" 'site-card--sortable': cardProps.sortable,
:ripple="false" },
variant="flat" ]"
elevation="0" :ripple="false"
:hover="!cardProps.sortable" variant="flat"
@click="handleCardClick" elevation="0"
> :hover="!cardProps.sortable"
@click="handleCardClick"
>
<!-- 装饰性状态指示器 --> <!-- 装饰性状态指示器 -->
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div> <div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
@@ -419,11 +421,20 @@ onMounted(() => {
</VMenu> </VMenu>
</VBtn> </VBtn>
</VSheet> </VSheet>
</VCard> </VCard>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.site-card-hover-area {
inline-size: 100%;
}
.site-card-hover-area:hover .site-card--hoverable {
transform: translate3d(0, -0.25rem, 0);
}
.site-status-indicator { .site-status-indicator {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
@@ -455,7 +466,7 @@ onMounted(() => {
} }
/* 站点卡片悬停时状态指示器变化 */ /* 站点卡片悬停时状态指示器变化 */
.site-card:not(.site-card--sortable):hover .site-status-indicator { .site-card-hover-area:hover .site-card:not(.site-card--sortable) .site-status-indicator {
block-size: 2px; block-size: 2px;
opacity: 0.8; opacity: 0.8;
} }
@@ -644,7 +655,7 @@ onMounted(() => {
visibility: hidden; visibility: hidden;
} }
.site-card:hover .site-card-actions { .site-card-hover-area:hover .site-card-actions {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
visibility: visible; visibility: visible;
+24 -18
View File
@@ -404,26 +404,27 @@ function handleCardClick() {
<div> <div>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<div <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
class="subscribe-card-shell w-full h-full relative" <div v-bind="hover.props" class="subscribe-card-hover-area w-full h-full">
:class="{ <div
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable, class="subscribe-card-shell app-hover-lift-card w-full h-full relative"
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
}"
>
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full overflow-hidden"
:class="{ :class="{
'subscribe-card-paused': subscribeState === 'S', 'app-hover-lift-card--hovering': hover.isHovering && !props.sortable,
'subscribe-card-pending-tint': subscribeState === 'P', 'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
'cursor-move': props.sortable,
}" }"
min-height="150"
@click="handleCardClick"
:ripple="!props.batchMode && !props.sortable"
> >
<VCard
:key="props.media?.id"
class="flex flex-col h-full overflow-hidden"
:class="{
'subscribe-card-paused': subscribeState === 'S',
'subscribe-card-pending-tint': subscribeState === 'P',
'cursor-move': props.sortable,
}"
min-height="150"
@click="handleCardClick"
:ripple="!props.batchMode && !props.sortable"
>
<div <div
v-if="bestVersionBadge && imageLoaded" v-if="bestVersionBadge && imageLoaded"
class="best-version-badge" class="best-version-badge"
@@ -568,13 +569,18 @@ function handleCardClick() {
/> />
</div> </div>
</div> </div>
</VCard> </VCard>
</div>
</div> </div>
</template> </template>
</VHover> </VHover>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.subscribe-card-hover-area {
inline-size: 100%;
}
.subscribe-card-background { .subscribe-card-background {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%); background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
} }
+15 -9
View File
@@ -93,16 +93,17 @@ function doDelete() {
<div class="h-full"> <div class="h-full">
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<div <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
class="w-full h-full overflow-hidden" <div v-bind="hover.props" class="subscribe-share-card-hover-area w-full h-full">
:class="{ <div
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, class="app-hover-lift-card w-full h-full overflow-hidden"
}" :class="{
> 'app-hover-lift-card--hovering': hover.isHovering,
}"
>
<VCard <VCard
v-bind="hover.props"
:key="props.media?.id" :key="props.media?.id"
class="flex flex-col h-full" class="app-hover-lift-card flex flex-col h-full"
min-height="150" min-height="150"
@click="showForkSubscribe" @click="showForkSubscribe"
> >
@@ -155,13 +156,18 @@ function doDelete() {
{{ dateText }} {{ dateText }}
</VCardText> </VCardText>
</div> </div>
</VCard> </VCard>
</div>
</div> </div>
</template> </template>
</VHover> </VHover>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.subscribe-share-card-hover-area {
inline-size: 100%;
}
.subscribe-card-background { .subscribe-card-background {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%); background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
} }
+12 -3
View File
@@ -100,12 +100,13 @@ watch(
</script> </script>
<template> <template>
<div class="h-full"> <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="subtitle-card-hover-area h-full">
<VCard <VCard
:width="props.width || '100%'" :width="props.width || '100%'"
:variant="isDownloaded ? 'outlined' : 'flat'" :variant="isDownloaded ? 'outlined' : 'flat'"
@click="handleAddDownload" @click="handleAddDownload"
class="h-full cursor-pointer transition-transform hover:-translate-y-1 duration-300 d-flex flex-column overflow-hidden subtitle-card" class="app-hover-lift-card h-full cursor-pointer d-flex flex-column overflow-hidden subtitle-card"
:class="{ 'border-success border-2 opacity-85': isDownloaded }" :class="{ 'border-success border-2 opacity-85': isDownloaded }"
hover hover
> >
@@ -203,11 +204,19 @@ watch(
</template> </template>
<style scoped> <style scoped>
.subtitle-card-hover-area {
inline-size: 100%;
}
.subtitle-card-hover-area:hover .subtitle-card {
transform: translate3d(0, -0.25rem, 0);
}
.subtitle-card { .subtitle-card {
border: 1px solid transparent; border: 1px solid transparent;
} }
.subtitle-card:hover { .subtitle-card-hover-area:hover .subtitle-card {
border-color: rgba(var(--v-theme-primary), 0.3); border-color: rgba(var(--v-theme-primary), 0.3);
} }
</style> </style>
+12 -3
View File
@@ -99,10 +99,11 @@ watch(
</script> </script>
<template> <template>
<div class="w-100"> <!-- Hover 命中区域保持静止避免列表项上浮后底边反复触发 mouseleave -->
<div class="subtitle-item-hover-area w-100">
<VListItem <VListItem
:value="subtitle?.enclosure" :value="subtitle?.enclosure"
class="pa-3 mb-2 rounded subtitle-item transition-all duration-300 hover:-translate-y-1 overflow-hidden" class="app-hover-lift-card pa-3 mb-2 rounded subtitle-item overflow-hidden"
:class="{ 'border-start border-success border-3 opacity-85': isDownloaded }" :class="{ 'border-start border-success border-3 opacity-85': isDownloaded }"
@click="handleAddDownload" @click="handleAddDownload"
> >
@@ -206,11 +207,19 @@ watch(
</template> </template>
<style scoped> <style scoped>
.subtitle-item-hover-area {
inline-size: 100%;
}
.subtitle-item-hover-area:hover .subtitle-item {
transform: translate3d(0, -0.25rem, 0);
}
.subtitle-item { .subtitle-item {
border: 1px solid transparent; border: 1px solid transparent;
} }
.subtitle-item:hover { .subtitle-item-hover-area:hover .subtitle-item {
border-color: rgba(var(--v-theme-primary), 0.3); border-color: rgba(var(--v-theme-primary), 0.3);
} }
</style> </style>
+12 -3
View File
@@ -146,12 +146,13 @@ watch(
</script> </script>
<template> <template>
<div class="h-full"> <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="torrent-card-hover-area h-full">
<VCard <VCard
:width="props.width || '100%'" :width="props.width || '100%'"
:variant="isDownloaded ? 'outlined' : 'flat'" :variant="isDownloaded ? 'outlined' : 'flat'"
@click="handleAddDownload(props.torrent)" @click="handleAddDownload(props.torrent)"
class="h-full cursor-pointer transition-transform hover:-translate-y-1 duration-300 d-flex flex-column overflow-hidden torrent-card" class="app-hover-lift-card h-full cursor-pointer d-flex flex-column overflow-hidden torrent-card"
:class="{ 'border-success border-2 opacity-85': isDownloaded }" :class="{ 'border-success border-2 opacity-85': isDownloaded }"
hover hover
> >
@@ -316,12 +317,20 @@ watch(
inset-inline-end: 0; inset-inline-end: 0;
} }
.torrent-card-hover-area {
inline-size: 100%;
}
.torrent-card-hover-area:hover .torrent-card {
transform: translate3d(0, -0.25rem, 0);
}
/* 卡片悬停效果 */ /* 卡片悬停效果 */
.torrent-card { .torrent-card {
border: 1px solid transparent; border: 1px solid transparent;
} }
.torrent-card:hover { .torrent-card-hover-area:hover .torrent-card {
border-color: rgba(var(--v-theme-primary), 0.3); border-color: rgba(var(--v-theme-primary), 0.3);
} }
+12 -3
View File
@@ -115,10 +115,11 @@ watch(
</script> </script>
<template> <template>
<div class="w-100"> <!-- Hover 命中区域保持静止避免列表项上浮后底边反复触发 mouseleave -->
<div class="torrent-item-hover-area w-100">
<VListItem <VListItem
:value="props.torrent?.torrent_info?.enclosure" :value="props.torrent?.torrent_info?.enclosure"
class="pa-3 mb-2 rounded torrent-item transition-all duration-300 hover:-translate-y-1 overflow-hidden" class="app-hover-lift-card pa-3 mb-2 rounded torrent-item overflow-hidden"
:class="{ 'border-start border-success border-3 opacity-85': isDownloaded }" :class="{ 'border-start border-success border-3 opacity-85': isDownloaded }"
@click="handleAddDownload" @click="handleAddDownload"
> >
@@ -262,11 +263,19 @@ watch(
inset-inline-end: 0; inset-inline-end: 0;
} }
.torrent-item-hover-area {
inline-size: 100%;
}
.torrent-item-hover-area:hover .torrent-item {
transform: translate3d(0, -0.25rem, 0);
}
.torrent-item { .torrent-item {
border: 1px solid transparent; border: 1px solid transparent;
} }
.torrent-item:hover { .torrent-item-hover-area:hover .torrent-item {
border-color: rgba(var(--v-theme-primary), 0.3); border-color: rgba(var(--v-theme-primary), 0.3);
} }
+20 -9
View File
@@ -127,14 +127,16 @@ onMounted(() => {
}) })
</script> </script>
<template> <template>
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
:class="[ <div class="user-card-hover-area h-full">
'transition-transform duration-300 hover:-translate-y-1', <VCard
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '', :class="[
]" 'app-hover-lift-card',
class="user-card flex flex-column h-full" !props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
@click="editUser" ]"
> class="user-card flex flex-column h-full"
@click="editUser"
>
<div class="user-card__body flex-grow flex-grow-1"> <div class="user-card__body flex-grow flex-grow-1">
<!-- 用户头像和基本信息 --> <!-- 用户头像和基本信息 -->
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']"> <VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
@@ -302,10 +304,19 @@ onMounted(() => {
</div> </div>
</VCardText> </VCardText>
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
<style scoped> <style scoped>
.user-card-hover-area {
inline-size: 100%;
}
.user-card-hover-area:hover .user-card {
transform: translate3d(0, -0.25rem, 0);
}
.user-card { .user-card {
block-size: 100%; block-size: 100%;
} }
+16 -19
View File
@@ -95,17 +95,18 @@ function doDelete() {
<div class="h-full"> <div class="h-full">
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="workflow-share-card-hover-area h-full">
:key="props.workflow?.id" <VCard
class="workflow-share-card flex flex-col h-full cursor-pointer overflow-hidden" :key="props.workflow?.id"
:class="{ class="workflow-share-card app-hover-lift-card flex flex-col h-full cursor-pointer overflow-hidden"
'workflow-share-card--hovering': hover.isHovering, :class="{
}" 'app-hover-lift-card--hovering': hover.isHovering,
min-height="150" }"
:style="{ background: gradientStyle }" min-height="150"
@click="showForkWorkflow" :style="{ background: gradientStyle }"
> @click="showForkWorkflow"
>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<VCardText class="flex items-center pa-3 pb-1 grow"> <VCardText class="flex items-center pa-3 pb-1 grow">
<div class="flex flex-col justify-center w-full"> <div class="flex flex-col justify-center w-full">
@@ -134,20 +135,16 @@ function doDelete() {
{{ dateText }} {{ dateText }}
</VCardText> </VCardText>
</div> </div>
</VCard> </VCard>
</div>
</template> </template>
</VHover> </VHover>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// 阴影需要落在实际卡片上,不能被额外的 overflow 容器裁掉。 .workflow-share-card-hover-area {
.workflow-share-card { inline-size: 100%;
transition: transform 0.3s ease, box-shadow 0.2s ease;
transform: translateZ(0);
} }
.workflow-share-card--hovering {
transform: translate3d(0, -0.25rem, 0);
}
</style> </style>
+17 -9
View File
@@ -220,14 +220,15 @@ const resolveProgress = (item: Workflow) => {
<template> <template>
<div class="h-full"> <div class="h-full">
<VHover v-slot="hover"> <VHover v-slot="hover">
<VCard <!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
v-bind="hover.props" <div v-bind="hover.props" class="workflow-task-card-hover-area h-full">
class="mx-auto h-full" <VCard
@click="handleFlow(workflow)" class="app-hover-lift-card mx-auto h-full"
:ripple="false" @click="handleFlow(workflow)"
:loading="loading" :ripple="false"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }" :loading="loading"
> :class="{ 'app-hover-lift-card--hovering': hover.isHovering }"
>
<VCardItem <VCardItem
class="px-2 py-2" class="px-2 py-2"
:style="{ :style="{
@@ -367,7 +368,14 @@ const resolveProgress = (item: Workflow) => {
</div> </div>
</div> </div>
</VCardText> </VCardText>
</VCard> </VCard>
</div>
</VHover> </VHover>
</div> </div>
</template> </template>
<style scoped>
.workflow-task-card-hover-area {
inline-size: 100%;
}
</style>
+10
View File
@@ -184,6 +184,16 @@ html[data-theme-radius='extra'] {
box-shadow: var(--app-surface-shadow) !important; box-shadow: var(--app-surface-shadow) !important;
} }
// 统一卡片上浮反馈;hover 命中区域应放在静止外层,避免上浮后底边反复触发 mouseleave。
.app-hover-lift-card {
transition: transform 0.3s ease, box-shadow 0.2s ease;
transform: translateZ(0);
}
.app-hover-lift-card--hovering {
transform: translate3d(0, -0.25rem, 0);
}
// 全局页面与 overlay 动效:短距离、轻缩放,保持快速但不生硬。 // 全局页面与 overlay 动效:短距离、轻缩放,保持快速但不生硬。
.mp-page-route { .mp-page-route {
inline-size: 100%; inline-size: 100%;
+5 -5
View File
@@ -157,11 +157,12 @@ onMounted(() => {
<VCardText> <VCardText>
<p class="text-body-2 text-medium-emphasis mb-4">{{ t('setupWizard.preferences.quickPresetsDesc') }}</p> <p class="text-body-2 text-medium-emphasis mb-4">{{ t('setupWizard.preferences.quickPresetsDesc') }}</p>
<VRow> <VRow>
<VCol v-for="(preset, key) in presetConfigs" :key="key" cols="12" sm="6" md="3"> <!-- Hover 命中区域保持静止避免预设卡片上浮后底边反复触发 mouseleave -->
<VCol v-for="(preset, key) in presetConfigs" :key="key" class="preset-card-hover-area" cols="12" sm="6" md="3">
<VCard <VCard
:color="selectedPreset === key ? preset.color : 'default'" :color="selectedPreset === key ? preset.color : 'default'"
:variant="selectedPreset === key ? 'tonal' : 'outlined'" :variant="selectedPreset === key ? 'tonal' : 'outlined'"
class="cursor-pointer preset-card" class="app-hover-lift-card cursor-pointer preset-card"
@click="selectPreset(key)" @click="selectPreset(key)"
> >
<VCardText class="text-center pa-4"> <VCardText class="text-center pa-4">
@@ -218,11 +219,10 @@ onMounted(() => {
<style scoped> <style scoped>
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
} }
.preset-card:hover { .preset-card-hover-area:hover .preset-card {
transform: translateY(-4px); transform: translate3d(0, -0.25rem, 0);
} }
.preset-card:active { .preset-card:active {