From 817bc1019dbdabaf23543eba0a03b66f9aabd96e Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 5 Jul 2026 20:39:06 +0800 Subject: [PATCH] Improve episode group carousel navigation --- .../dialog/SubscribeSeasonDialog.vue | 170 +++++++++++++++--- src/views/discover/MediaDetailView.vue | 32 ++-- 2 files changed, 172 insertions(+), 30 deletions(-) diff --git a/src/components/dialog/SubscribeSeasonDialog.vue b/src/components/dialog/SubscribeSeasonDialog.vue index 63c8ee26..e546453b 100644 --- a/src/components/dialog/SubscribeSeasonDialog.vue +++ b/src/components/dialog/SubscribeSeasonDialog.vue @@ -67,6 +67,13 @@ const episodeGroups = ref<{ [key: string]: any }[]>([]) // 当前选择剧集组 const episodeGroup = ref(props.initialEpisodeGroup ?? '') +// 剧集组横向轨道 +const episodeGroupRail = ref(null) + +// 剧集组轨道左右滚动状态 +const canScrollEpisodeGroupsBackward = ref(false) +const canScrollEpisodeGroupsForward = ref(false) + const subscribeModeOptions = computed(() => [ { title: t('dialog.subscribeMode.normal'), @@ -173,6 +180,8 @@ async function getEpisodeGroups() { episodeGroups.value = await api.get(`media/groups/${props.media?.tmdb_id}`) } catch (error) { console.error(error) + } finally { + nextTick(updateEpisodeGroupScrollState) } } @@ -275,6 +284,32 @@ function setEpisodeGroup(value: string) { seasonsNotExisted.value = {} seasonInfos.value = [] episodeGroup.value = value + nextTick(updateEpisodeGroupScrollState) +} + +// 刷新剧集组横向轨道的左右滚动按钮状态。 +function updateEpisodeGroupScrollState() { + const rail = episodeGroupRail.value + if (!rail) { + canScrollEpisodeGroupsBackward.value = false + canScrollEpisodeGroupsForward.value = false + return + } + + const maxScrollLeft = Math.max(rail.scrollWidth - rail.clientWidth, 0) + canScrollEpisodeGroupsBackward.value = rail.scrollLeft > 4 + canScrollEpisodeGroupsForward.value = rail.scrollLeft < maxScrollLeft - 4 +} + +// 按一屏内可辨识的距离横向滚动剧集组轨道。 +function scrollEpisodeGroups(direction: 'backward' | 'forward') { + const rail = episodeGroupRail.value + if (!rail) return + + rail.scrollBy({ + behavior: 'smooth', + left: direction === 'backward' ? -Math.max(rail.clientWidth * 0.72, 240) : Math.max(rail.clientWidth * 0.72, 240), + }) } // 提交当前剧集组下选中的季及其订阅模式。 @@ -395,12 +430,19 @@ watch(() => props.subscribedSeasons, syncSelectedSeason) watch(() => props.subscribedSeasonModes, syncSelectedSeason) +watch(episodeGroupOptions, () => nextTick(updateEpisodeGroupScrollState), { flush: 'post' }) + onMounted(async () => { + window.addEventListener('resize', updateEpisodeGroupScrollState) // 自定义剧集组由 watchEffect 首次加载,避免默认季数据异步覆盖它。 if (!episodeGroup.value) getMediaSeasons() getEpisodeGroups() checkSeasonsNotExists() }) + +onBeforeUnmount(() => { + window.removeEventListener('resize', updateEpisodeGroupScrollState) +}) {{ getSubscribeText }} - + @@ -1561,9 +1561,10 @@ a.crew-name { display: inline-flex; flex: 0 0 12rem; align-items: center; + backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px))); border: 1px solid rgba(var(--v-theme-on-surface), 0.12); border-radius: var(--app-control-radius); - background: rgba(var(--v-theme-surface), 0.72); + background: rgba(var(--v-theme-surface), 0.82); color: rgb(var(--v-theme-on-surface)); gap: 0.625rem; min-inline-size: 0; @@ -1579,15 +1580,20 @@ a.crew-name { .episode-group-option:hover { border-color: rgba(var(--v-theme-primary), 0.5); - background: rgba(var(--v-theme-primary), 0.08); + background: rgba(var(--v-theme-surface), 0.9); + box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.08); } .episode-group-option--active { border-color: rgb(var(--v-theme-primary)); - background: rgba(var(--v-theme-primary), 0.14); + background: rgba(var(--v-theme-primary), 0.18); color: rgb(var(--v-theme-primary)); } +.episode-group-option--active:hover { + background: rgba(var(--v-theme-primary), 0.24); +} + .episode-group-option:focus-visible, .episode-group-nav:focus-visible { outline: 2px solid rgba(var(--v-theme-primary), 0.45); @@ -1632,9 +1638,10 @@ a.crew-name { display: inline-flex; align-items: center; justify-content: center; + backdrop-filter: var(--app-grouped-list-backdrop-filter, blur(var(--transparent-blur-light, 6px))); border: 1px solid rgba(var(--v-theme-on-surface), 0.12); border-radius: 9999px; - background: rgba(var(--v-theme-surface), 0.92); + background: rgba(var(--v-theme-surface), 0.96); block-size: 2.5rem; color: rgb(var(--v-theme-on-surface)); inline-size: 2.5rem; @@ -1648,7 +1655,8 @@ a.crew-name { .episode-group-nav:hover { border-color: rgba(var(--v-theme-primary), 0.45); - background: rgba(var(--v-theme-primary), 0.12); + background: rgba(var(--v-theme-surface), 0.96); + box-shadow: 0 6px 18px rgba(var(--v-theme-on-surface), 0.12); color: rgb(var(--v-theme-primary)); } @@ -1667,16 +1675,20 @@ a.crew-name { } .media-detail-transparent .episode-group-option { - backdrop-filter: blur(var(--transparent-blur-light, 6px)); - background: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2)); + backdrop-filter: blur(var(--transparent-blur, 10px)); + background: rgba(var(--v-theme-surface), var(--transparent-opacity, 0.3)); } .media-detail-transparent .episode-group-option:hover { - background: rgba(var(--v-theme-primary), 0.1); + background: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5)); } .media-detail-transparent .episode-group-option--active { - background: rgba(var(--v-theme-primary), 0.16); + background: rgba(var(--v-theme-primary), 0.22); +} + +.media-detail-transparent .episode-group-option--active:hover { + background: rgba(var(--v-theme-primary), 0.28); } .media-detail-transparent .episode-group-nav {