mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
fix: refactor media card interaction for improved touch handling and state management
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const activeTouchMediaCardId = ref<number | null>(null)
|
||||||
|
let mediaCardIdSeed = 0
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import noImage from '@images/no-image.jpeg'
|
import noImage from '@images/no-image.jpeg'
|
||||||
import { getDisplayImageUrl, getLogoUrl } from '@/utils/imageUtils'
|
import { getDisplayImageUrl, getLogoUrl } from '@/utils/imageUtils'
|
||||||
@@ -93,8 +100,7 @@ const selectedSites = ref<number[]>([])
|
|||||||
// 搜索菜单显示状态
|
// 搜索菜单显示状态
|
||||||
const searchMenuShow = ref(false)
|
const searchMenuShow = ref(false)
|
||||||
|
|
||||||
// 触摸设备没有稳定 hover,单独保存详情层的展开状态。
|
const mediaCardId = ++mediaCardIdSeed
|
||||||
const touchDetailVisible = ref(false)
|
|
||||||
|
|
||||||
// 粗指针设备使用点击展开详情,避免 iOS 返回后沿用 VHover 的触摸态。
|
// 粗指针设备使用点击展开详情,避免 iOS 返回后沿用 VHover 的触摸态。
|
||||||
const isTouchLikePointer = ref(
|
const isTouchLikePointer = ref(
|
||||||
@@ -196,7 +202,12 @@ async function handleCheckSubscribe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function querySubscribedSeasons() {
|
async function querySubscribedSeasons() {
|
||||||
if (props.media?.type !== '电视剧' || !isSubscribed.value || subscribedSeasonsLoaded.value || subscribedSeasonsLoading.value) {
|
if (
|
||||||
|
props.media?.type !== '电视剧' ||
|
||||||
|
!isSubscribed.value ||
|
||||||
|
subscribedSeasonsLoaded.value ||
|
||||||
|
subscribedSeasonsLoading.value
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +296,7 @@ function goMediaDetail(isHovering = false) {
|
|||||||
|
|
||||||
// 当前卡片是否进入可操作详情态,桌面使用 hover,触摸端使用显式点击态。
|
// 当前卡片是否进入可操作详情态,桌面使用 hover,触摸端使用显式点击态。
|
||||||
function isMediaCardActive(isHovering: boolean | null | undefined) {
|
function isMediaCardActive(isHovering: boolean | null | undefined) {
|
||||||
return touchDetailVisible.value || (!isTouchLikePointer.value && Boolean(isHovering))
|
return activeTouchMediaCardId.value === mediaCardId || (!isTouchLikePointer.value && Boolean(isHovering))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前卡片详情层是否显示,保留图片错误和站点选择时的强制显示。
|
// 当前卡片详情层是否显示,保留图片错误和站点选择时的强制显示。
|
||||||
@@ -295,13 +306,15 @@ function isMediaCardDetailVisible(isHovering: boolean | null | undefined) {
|
|||||||
|
|
||||||
// 清理移动端详情层展开态,避免路由返回后继续显示上一次的详情层。
|
// 清理移动端详情层展开态,避免路由返回后继续显示上一次的详情层。
|
||||||
function resetMediaCardDetailState() {
|
function resetMediaCardDetailState() {
|
||||||
touchDetailVisible.value = false
|
if (activeTouchMediaCardId.value === mediaCardId) {
|
||||||
|
activeTouchMediaCardId.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理媒体卡片点击:触摸端第一次点击展开,展开后再次点击进入详情。
|
// 处理媒体卡片点击:触摸端第一次点击展开,展开后再次点击进入详情。
|
||||||
function handleMediaCardClick(isHovering: boolean | null | undefined) {
|
function handleMediaCardClick(isHovering: boolean | null | undefined) {
|
||||||
if (isTouchLikePointer.value && !isMediaCardDetailVisible(isHovering)) {
|
if (isTouchLikePointer.value && !isMediaCardDetailVisible(isHovering)) {
|
||||||
touchDetailVisible.value = true
|
activeTouchMediaCardId.value = mediaCardId
|
||||||
querySubscribedSeasons()
|
querySubscribedSeasons()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -309,6 +322,14 @@ function handleMediaCardClick(isHovering: boolean | null | undefined) {
|
|||||||
goMediaDetail(isMediaCardActive(isHovering) || isMediaCardDetailVisible(isHovering))
|
goMediaDetail(isMediaCardActive(isHovering) || isMediaCardDetailVisible(isHovering))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDocumentPointerDown(event: PointerEvent) {
|
||||||
|
if (!isTouchLikePointer.value || activeTouchMediaCardId.value !== mediaCardId) return
|
||||||
|
if (!(event.target instanceof Node)) return
|
||||||
|
if (mediaCardRef.value?.contains(event.target)) return
|
||||||
|
|
||||||
|
resetMediaCardDetailState()
|
||||||
|
}
|
||||||
|
|
||||||
// 点击搜索
|
// 点击搜索
|
||||||
async function clickSearch() {
|
async function clickSearch() {
|
||||||
if (allSites.value?.length == 0) {
|
if (allSites.value?.length == 0) {
|
||||||
@@ -418,6 +439,9 @@ watch(
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setupIntersectionObserver()
|
setupIntersectionObserver()
|
||||||
|
if (isTouchLikePointer.value) {
|
||||||
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(resetMediaCardDetailState)
|
onActivated(resetMediaCardDetailState)
|
||||||
@@ -425,6 +449,7 @@ onDeactivated(resetMediaCardDetailState)
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
resetMediaCardDetailState()
|
resetMediaCardDetailState()
|
||||||
|
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
||||||
observer.value?.disconnect()
|
observer.value?.disconnect()
|
||||||
observer.value = null
|
observer.value = null
|
||||||
})
|
})
|
||||||
@@ -434,12 +459,7 @@ onBeforeUnmount(() => {
|
|||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<!-- Hover 命中区域保持静止,避免卡片上浮后底边反复触发 mouseleave。 -->
|
<!-- Hover 命中区域保持静止,避免卡片上浮后底边反复触发 mouseleave。 -->
|
||||||
<div
|
<div ref="mediaCardRef" v-bind="hover.props" class="media-card-hover-area" @mouseenter="querySubscribedSeasons">
|
||||||
ref="mediaCardRef"
|
|
||||||
v-bind="hover.props"
|
|
||||||
class="media-card-hover-area"
|
|
||||||
@mouseenter="querySubscribedSeasons"
|
|
||||||
>
|
|
||||||
<VCard
|
<VCard
|
||||||
:height="props.height"
|
:height="props.height"
|
||||||
:width="props.width"
|
:width="props.width"
|
||||||
@@ -486,13 +506,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||||
<div v-else class="flex align-center justify-between">
|
<div v-else class="flex align-center justify-between">
|
||||||
<IconBtn
|
<IconBtn v-if="canSearch" icon="mdi-magnify" color="white" size="small" @click.stop="clickSearch" />
|
||||||
v-if="canSearch"
|
|
||||||
icon="mdi-magnify"
|
|
||||||
color="white"
|
|
||||||
size="small"
|
|
||||||
@click.stop="clickSearch"
|
|
||||||
/>
|
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<IconBtn
|
<IconBtn
|
||||||
v-if="canSubscribe"
|
v-if="canSubscribe"
|
||||||
@@ -543,7 +557,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.media-card-hover-area {
|
.media-card-hover-area {
|
||||||
width: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-card-title {
|
.media-card-title {
|
||||||
@@ -559,11 +573,11 @@ onBeforeUnmount(() => {
|
|||||||
.media-card-subscribe-summary {
|
.media-card-subscribe-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
|
||||||
min-block-size: 1.25rem;
|
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
gap: 0.25rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
|
min-block-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-card-subscribe-summary span {
|
.media-card-subscribe-summary span {
|
||||||
|
|||||||
Reference in New Issue
Block a user