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

View File

@@ -46,17 +46,18 @@ const getImgUrl = computed(() => {
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
class="ring-gray-500"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
'ring-1': imageLoaded,
}"
@click="goPlay"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="backdrop-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
class="app-hover-lift-card ring-gray-500"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
'ring-1': imageLoaded,
}"
@click="goPlay"
>
<template #image>
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
<template #placeholder>
@@ -86,7 +87,14 @@ const getImgUrl = computed(() => {
color="success"
/>
</div>
</VCard>
</VCard>
</div>
</template>
</VHover>
</template>
<style scoped>
.backdrop-card-hover-area {
inline-size: 100%;
}
</style>

View File

@@ -73,16 +73,16 @@ async function deleteDownload() {
<template>
<VHover>
<template #default="hover">
<VCard
v-if="cardState"
v-bind="hover.props"
:key="props.info?.hash"
class="downloading-card app-surface flex flex-col h-full overflow-hidden"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
min-height="150"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-if="cardState" v-bind="hover.props" class="downloading-card-hover-area h-full">
<VCard
:key="props.info?.hash"
class="downloading-card app-hover-lift-card app-surface flex flex-col h-full overflow-hidden"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
min-height="150"
>
<template #image>
<VImg
:src="props.info?.media.image"
@@ -130,7 +130,8 @@ async function deleteDownload() {
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
</VCardActions>
</div>
</VCard>
</VCard>
</div>
</template>
</VHover>
</template>
@@ -138,6 +139,10 @@ async function deleteDownload() {
<style lang="scss" scoped>
/* stylelint-disable selector-pseudo-class-no-unknown */
.downloading-card-hover-area {
inline-size: 100%;
}
.downloading-card-image {
block-size: 100%;
}

View File

@@ -156,15 +156,17 @@ onMounted(async () => {
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
@click="goPlay"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="library-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
class="app-hover-lift-card"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
@click="goPlay"
>
<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">
@@ -184,7 +186,14 @@ onMounted(async () => {
</template>
</VImg>
</template>
</VCard>
</VCard>
</div>
</template>
</VHover>
</template>
<style scoped>
.library-card-hover-area {
inline-size: 100%;
}
</style>

View File

@@ -498,9 +498,9 @@ onBeforeUnmount(() => {
<VCard
:height="props.height"
:width="props.width"
class="outline-none ring-gray-500 media-card"
class="app-hover-lift-card outline-none ring-gray-500 media-card"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
'app-hover-lift-card--hovering': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goMediaDetail(hover.isHovering ?? false)"

View File

@@ -75,15 +75,17 @@ function goPersonDetail() {
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="person-card-hover-area">
<VCard
:height="personProps.height"
:width="personProps.width"
class="app-hover-lift-card"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div class="person-card relative cursor-pointer ring-gray-700">
<div style="padding-block-end: 150%">
<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>
</VCard>
</VCard>
</div>
</template>
</VHover>
</template>
<style lang="scss" scoped>
.person-card-hover-area {
inline-size: 100%;
}
.person-card {
background-image: linear-gradient(
45deg,

View File

@@ -230,16 +230,17 @@ onUnmounted(() => {
<div>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:width="props.width"
:height="props.height"
@click="showPluginDetail"
class="flex flex-col h-full"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="plugin-app-card-hover-area h-full">
<VCard
:width="props.width"
:height="props.height"
@click="showPluginDetail"
class="app-hover-lift-card flex flex-col h-full"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
>
<div
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%)`"
@@ -325,13 +326,18 @@ onUnmounted(() => {
</IconBtn>
</div>
</VCardText>
</VCard>
</VCard>
</div>
</template>
</VHover>
</div>
</template>
<style scoped>
.plugin-app-card-hover-area {
inline-size: 100%;
}
.plugin-app-card__tags-section {
display: flex;
overflow: hidden;

View File

@@ -567,19 +567,19 @@ watch(
<!-- 插件卡片 -->
<VHover>
<template #default="hover">
<VCard
v-if="isVisible"
v-bind="hover.props"
:width="props.width"
:height="props.height"
@click="handleCardClick"
class="flex flex-col h-full"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
'cursor-move': props.sortable,
}"
:ripple="!props.sortable"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-if="isVisible" v-bind="hover.props" class="plugin-card-hover-area h-full">
<VCard
:width="props.width"
:height="props.height"
@click="handleCardClick"
class="app-hover-lift-card flex flex-col h-full"
:class="{
'app-hover-lift-card--hovering': hover.isHovering && !props.sortable,
'cursor-move': props.sortable,
}"
:ripple="!props.sortable"
>
<div
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%)`"
@@ -669,7 +669,8 @@ watch(
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
<VIcon icon="mdi-new-box" class="text-white" />
</div>
</VCard>
</VCard>
</div>
</template>
</VHover>
@@ -677,6 +678,10 @@ watch(
</template>
<style lang="scss" scoped>
.plugin-card-hover-area {
inline-size: 100%;
}
.card-cover-blurred::before {
position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */

View File

@@ -211,20 +211,21 @@ const dropdownItems = ref([
<!-- 文件夹卡片 -->
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:ripple="false"
:width="props.width"
:height="props.height"
min-height="8.5rem"
@click="handleCardClick"
class="plugin-folder-card h-full"
:class="{
'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--hover': hover.isHovering && !props.sortable,
'plugin-folder-card--sortable': props.sortable,
}"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="plugin-folder-card-hover-area h-full">
<VCard
:ripple="false"
:width="props.width"
:height="props.height"
min-height="8.5rem"
@click="handleCardClick"
class="plugin-folder-card app-hover-lift-card h-full"
:class="{
'plugin-folder-card--mobile': display.mobile,
'plugin-folder-card--hover': hover.isHovering && !props.sortable,
'plugin-folder-card--sortable': props.sortable,
}"
>
<template v-if="backgroundImage" #image>
<VImg :src="backgroundImage" cover position="top"> </VImg>
</template>
@@ -288,25 +289,29 @@ const dropdownItems = ref([
</VMenu>
</div>
</div>
</VCard>
</VCard>
</div>
</template>
</VHover>
</div>
</template>
<style lang="scss" scoped>
.plugin-folder-card-hover-area {
inline-size: 100%;
}
.plugin-folder-card {
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&--sortable {
cursor: move;
}
&--hover {
transform: translateY(-4px);
transform: translate3d(0, -0.25rem, 0);
}
&__bg {

View File

@@ -47,16 +47,17 @@ async function goPlay(isHovering: boolean | null = false) {
<template>
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:height="props.height"
:width="props.width"
class="outline-none ring-gray-500"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
'ring-1': isImageLoaded,
}"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="poster-card-hover-area">
<VCard
:height="props.height"
:width="props.width"
class="app-hover-lift-card outline-none ring-gray-500"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
'ring-1': isImageLoaded,
}"
>
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
@@ -93,7 +94,14 @@ async function goPlay(isHovering: boolean | null = false) {
{{ props.media?.title }}
</h1>
</VCardText>
</VCard>
</VCard>
</div>
</template>
</VHover>
</template>
<style scoped>
.poster-card-hover-area {
inline-size: 100%;
}
</style>

View File

@@ -239,25 +239,27 @@ onMounted(() => {
<template>
<div>
<VCard
class="site-card relative h-full flex flex-col overflow-hidden group transition-all duration-300"
:class="[
cardProps.site?.is_active ? '' : 'opacity-70',
{
'border-error': statColor === 'error',
'border-warning': statColor === 'warning',
'border-success': statColor === 'success',
'cursor-pointer hover:-translate-y-1': !cardProps.sortable,
'cursor-move': cardProps.sortable,
'site-card--sortable': cardProps.sortable,
},
]"
:ripple="false"
variant="flat"
elevation="0"
:hover="!cardProps.sortable"
@click="handleCardClick"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="site-card-hover-area h-full">
<VCard
class="site-card app-hover-lift-card relative h-full flex flex-col overflow-hidden group"
:class="[
cardProps.site?.is_active ? '' : 'opacity-70',
{
'border-error': statColor === 'error',
'border-warning': statColor === 'warning',
'border-success': statColor === 'success',
'cursor-pointer site-card--hoverable': !cardProps.sortable,
'cursor-move': cardProps.sortable,
'site-card--sortable': cardProps.sortable,
},
]"
:ripple="false"
variant="flat"
elevation="0"
:hover="!cardProps.sortable"
@click="handleCardClick"
>
<!-- 装饰性状态指示器 -->
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
@@ -419,11 +421,20 @@ onMounted(() => {
</VMenu>
</VBtn>
</VSheet>
</VCard>
</VCard>
</div>
</div>
</template>
<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 {
position: absolute;
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;
opacity: 0.8;
}
@@ -644,7 +655,7 @@ onMounted(() => {
visibility: hidden;
}
.site-card:hover .site-card-actions {
.site-card-hover-area:hover .site-card-actions {
opacity: 1;
transform: translateX(0);
visibility: visible;

View File

@@ -404,26 +404,27 @@ function handleCardClick() {
<div>
<VHover>
<template #default="hover">
<div
class="subscribe-card-shell w-full h-full relative"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
'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"
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="subscribe-card-hover-area w-full h-full">
<div
class="subscribe-card-shell app-hover-lift-card w-full h-full relative"
:class="{
'subscribe-card-paused': subscribeState === 'S',
'subscribe-card-pending-tint': subscribeState === 'P',
'cursor-move': props.sortable,
'app-hover-lift-card--hovering': hover.isHovering && !props.sortable,
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
}"
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
v-if="bestVersionBadge && imageLoaded"
class="best-version-badge"
@@ -568,13 +569,18 @@ function handleCardClick() {
/>
</div>
</div>
</VCard>
</VCard>
</div>
</div>
</template>
</VHover>
</div>
</template>
<style lang="scss" scoped>
.subscribe-card-hover-area {
inline-size: 100%;
}
.subscribe-card-background {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
}

View File

@@ -93,16 +93,17 @@ function doDelete() {
<div class="h-full">
<VHover>
<template #default="hover">
<div
class="w-full h-full overflow-hidden"
:class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="subscribe-share-card-hover-area w-full h-full">
<div
class="app-hover-lift-card w-full h-full overflow-hidden"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
>
<VCard
v-bind="hover.props"
:key="props.media?.id"
class="flex flex-col h-full"
class="app-hover-lift-card flex flex-col h-full"
min-height="150"
@click="showForkSubscribe"
>
@@ -155,13 +156,18 @@ function doDelete() {
{{ dateText }}
</VCardText>
</div>
</VCard>
</VCard>
</div>
</div>
</template>
</VHover>
</div>
</template>
<style lang="scss" scoped>
.subscribe-share-card-hover-area {
inline-size: 100%;
}
.subscribe-card-background {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
}

View File

@@ -100,12 +100,13 @@ watch(
</script>
<template>
<div class="h-full">
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="subtitle-card-hover-area h-full">
<VCard
:width="props.width || '100%'"
:variant="isDownloaded ? 'outlined' : 'flat'"
@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 }"
hover
>
@@ -203,11 +204,19 @@ watch(
</template>
<style scoped>
.subtitle-card-hover-area {
inline-size: 100%;
}
.subtitle-card-hover-area:hover .subtitle-card {
transform: translate3d(0, -0.25rem, 0);
}
.subtitle-card {
border: 1px solid transparent;
}
.subtitle-card:hover {
.subtitle-card-hover-area:hover .subtitle-card {
border-color: rgba(var(--v-theme-primary), 0.3);
}
</style>

View File

@@ -99,10 +99,11 @@ watch(
</script>
<template>
<div class="w-100">
<!-- Hover 命中区域保持静止避免列表项上浮后底边反复触发 mouseleave -->
<div class="subtitle-item-hover-area w-100">
<VListItem
: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 }"
@click="handleAddDownload"
>
@@ -206,11 +207,19 @@ watch(
</template>
<style scoped>
.subtitle-item-hover-area {
inline-size: 100%;
}
.subtitle-item-hover-area:hover .subtitle-item {
transform: translate3d(0, -0.25rem, 0);
}
.subtitle-item {
border: 1px solid transparent;
}
.subtitle-item:hover {
.subtitle-item-hover-area:hover .subtitle-item {
border-color: rgba(var(--v-theme-primary), 0.3);
}
</style>

View File

@@ -146,12 +146,13 @@ watch(
</script>
<template>
<div class="h-full">
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="torrent-card-hover-area h-full">
<VCard
:width="props.width || '100%'"
:variant="isDownloaded ? 'outlined' : 'flat'"
@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 }"
hover
>
@@ -316,12 +317,20 @@ watch(
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 {
border: 1px solid transparent;
}
.torrent-card:hover {
.torrent-card-hover-area:hover .torrent-card {
border-color: rgba(var(--v-theme-primary), 0.3);
}

View File

@@ -115,10 +115,11 @@ watch(
</script>
<template>
<div class="w-100">
<!-- Hover 命中区域保持静止避免列表项上浮后底边反复触发 mouseleave -->
<div class="torrent-item-hover-area w-100">
<VListItem
: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 }"
@click="handleAddDownload"
>
@@ -262,11 +263,19 @@ watch(
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 {
border: 1px solid transparent;
}
.torrent-item:hover {
.torrent-item-hover-area:hover .torrent-item {
border-color: rgba(var(--v-theme-primary), 0.3);
}

View File

@@ -127,14 +127,16 @@ onMounted(() => {
})
</script>
<template>
<VCard
:class="[
'transition-transform duration-300 hover:-translate-y-1',
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
]"
class="user-card flex flex-column h-full"
@click="editUser"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div class="user-card-hover-area h-full">
<VCard
:class="[
'app-hover-lift-card',
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
]"
class="user-card flex flex-column h-full"
@click="editUser"
>
<div class="user-card__body flex-grow flex-grow-1">
<!-- 用户头像和基本信息 -->
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
@@ -302,10 +304,19 @@ onMounted(() => {
</div>
</VCardText>
</div>
</VCard>
</VCard>
</div>
</template>
<style scoped>
.user-card-hover-area {
inline-size: 100%;
}
.user-card-hover-area:hover .user-card {
transform: translate3d(0, -0.25rem, 0);
}
.user-card {
block-size: 100%;
}

View File

@@ -95,17 +95,18 @@ function doDelete() {
<div class="h-full">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
:key="props.workflow?.id"
class="workflow-share-card flex flex-col h-full cursor-pointer overflow-hidden"
:class="{
'workflow-share-card--hovering': hover.isHovering,
}"
min-height="150"
:style="{ background: gradientStyle }"
@click="showForkWorkflow"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="workflow-share-card-hover-area h-full">
<VCard
:key="props.workflow?.id"
class="workflow-share-card app-hover-lift-card flex flex-col h-full cursor-pointer overflow-hidden"
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
min-height="150"
:style="{ background: gradientStyle }"
@click="showForkWorkflow"
>
<div class="h-full flex flex-col">
<VCardText class="flex items-center pa-3 pb-1 grow">
<div class="flex flex-col justify-center w-full">
@@ -134,20 +135,16 @@ function doDelete() {
{{ dateText }}
</VCardText>
</div>
</VCard>
</VCard>
</div>
</template>
</VHover>
</div>
</template>
<style lang="scss" scoped>
// 阴影需要落在实际卡片上,不能被额外的 overflow 容器裁掉。
.workflow-share-card {
transition: transform 0.3s ease, box-shadow 0.2s ease;
transform: translateZ(0);
.workflow-share-card-hover-area {
inline-size: 100%;
}
.workflow-share-card--hovering {
transform: translate3d(0, -0.25rem, 0);
}
</style>

View File

@@ -220,14 +220,15 @@ const resolveProgress = (item: Workflow) => {
<template>
<div class="h-full">
<VHover v-slot="hover">
<VCard
v-bind="hover.props"
class="mx-auto h-full"
@click="handleFlow(workflow)"
:ripple="false"
:loading="loading"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
>
<!-- Hover 命中区域保持静止避免卡片上浮后底边反复触发 mouseleave -->
<div v-bind="hover.props" class="workflow-task-card-hover-area h-full">
<VCard
class="app-hover-lift-card mx-auto h-full"
@click="handleFlow(workflow)"
:ripple="false"
:loading="loading"
:class="{ 'app-hover-lift-card--hovering': hover.isHovering }"
>
<VCardItem
class="px-2 py-2"
:style="{
@@ -367,7 +368,14 @@ const resolveProgress = (item: Workflow) => {
</div>
</div>
</VCardText>
</VCard>
</VCard>
</div>
</VHover>
</div>
</template>
<style scoped>
.workflow-task-card-hover-area {
inline-size: 100%;
}
</style>

View File

@@ -184,6 +184,16 @@ html[data-theme-radius='extra'] {
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 动效:短距离、轻缩放,保持快速但不生硬。
.mp-page-route {
inline-size: 100%;

View File

@@ -157,11 +157,12 @@ onMounted(() => {
<VCardText>
<p class="text-body-2 text-medium-emphasis mb-4">{{ t('setupWizard.preferences.quickPresetsDesc') }}</p>
<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
:color="selectedPreset === key ? preset.color : 'default'"
:variant="selectedPreset === key ? 'tonal' : 'outlined'"
class="cursor-pointer preset-card"
class="app-hover-lift-card cursor-pointer preset-card"
@click="selectPreset(key)"
>
<VCardText class="text-center pa-4">
@@ -218,11 +219,10 @@ onMounted(() => {
<style scoped>
.cursor-pointer {
cursor: pointer;
transition: all 0.3s ease;
}
.preset-card:hover {
transform: translateY(-4px);
.preset-card-hover-area:hover .preset-card {
transform: translate3d(0, -0.25rem, 0);
}
.preset-card:active {