mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
feat: add transparent glass quality options (#507)
* feat(theme): add lightweight transparent glass mode * perf(theme): reduce realtime glass backdrop filters * fix(theme): align transparency presets
This commit is contained in:
+47
-15
@@ -12,6 +12,12 @@ import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundMan
|
||||
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
||||
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
||||
import {
|
||||
applyStoredTransparencySettings,
|
||||
TRANSPARENCY_SETTINGS_CHANGED_EVENT,
|
||||
type TransparencyGlassQuality,
|
||||
type TransparencySettings,
|
||||
} from '@/composables/useTransparencySettings'
|
||||
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { themeManager } from '@/utils/themeManager'
|
||||
@@ -60,29 +66,38 @@ const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
||||
const shouldUseTransparentBackgroundTreatment = computed(
|
||||
() => (Boolean(isLogin.value) && isTransparentTheme.value) || isLoginWallpaperRoute.value,
|
||||
)
|
||||
const shouldLoadBackgroundImages = computed(
|
||||
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isTransparentTheme.value),
|
||||
)
|
||||
const transparentBackgroundBlur = ref(16)
|
||||
const transparencyGlassQuality = ref<TransparencyGlassQuality>(
|
||||
localStorage.getItem('transparency-glass-quality') === 'realtime' ? 'realtime' : 'lightweight',
|
||||
)
|
||||
const shouldRenderGlobalBlurLayer = computed(
|
||||
() =>
|
||||
shouldUseTransparentBackgroundTreatment.value &&
|
||||
transparentBackgroundBlur.value > 0 &&
|
||||
transparencyGlassQuality.value === 'realtime',
|
||||
)
|
||||
let backgroundRetryTimer: number | null = null
|
||||
let backgroundRequestController: AbortController | null = null
|
||||
let authenticatedStateTimer: number | null = null
|
||||
|
||||
function getStoredNumber(key: string, fallback: number, min: number, max: number) {
|
||||
const parsed = Number.parseFloat(localStorage.getItem(key) || '')
|
||||
if (!Number.isFinite(parsed)) return fallback
|
||||
function applyTransparentBackgroundSettings() {
|
||||
const settings = applyStoredTransparencySettings()
|
||||
|
||||
return Math.min(max, Math.max(min, parsed))
|
||||
transparentBackgroundBlur.value = settings.backgroundBlur
|
||||
transparencyGlassQuality.value = settings.glassQuality
|
||||
}
|
||||
|
||||
function applyTransparentBackgroundSettings() {
|
||||
document.documentElement.style.setProperty(
|
||||
'--transparent-background-poster-opacity',
|
||||
(1 - getStoredNumber('transparency-background-poster-opacity', 0, 0, 1)).toString(),
|
||||
)
|
||||
document.documentElement.style.setProperty(
|
||||
'--transparent-background-blur',
|
||||
`${getStoredNumber('transparency-background-blur', 16, 0, 30)}px`,
|
||||
)
|
||||
function handleTransparencySettingsChanged(event: Event) {
|
||||
const { backgroundBlur, glassQuality } = (event as CustomEvent<TransparencySettings>).detail
|
||||
|
||||
transparentBackgroundBlur.value = backgroundBlur
|
||||
transparencyGlassQuality.value = glassQuality
|
||||
}
|
||||
|
||||
applyTransparentBackgroundSettings()
|
||||
@@ -360,6 +375,7 @@ onMounted(async () => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||
window.addEventListener('pageshow', handlePageShowThemeSync)
|
||||
window.addEventListener('focus', handlePageShowThemeSync)
|
||||
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||
|
||||
// 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
|
||||
watch(
|
||||
@@ -413,6 +429,7 @@ onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||
window.removeEventListener('pageshow', handlePageShowThemeSync)
|
||||
window.removeEventListener('focus', handlePageShowThemeSync)
|
||||
window.removeEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -422,7 +439,11 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)"
|
||||
class="background-container"
|
||||
:class="{ 'is-transparent-theme': isTransparentTheme && isLogin }"
|
||||
:class="{
|
||||
'is-transparent-theme': shouldUseTransparentBackgroundTreatment,
|
||||
'is-transparent-glass-lightweight':
|
||||
shouldUseTransparentBackgroundTreatment && transparencyGlassQuality === 'lightweight',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
@@ -432,7 +453,7 @@ onUnmounted(() => {
|
||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||
/>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp :class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }">
|
||||
@@ -494,6 +515,17 @@ onUnmounted(() => {
|
||||
opacity: var(--transparent-background-poster-opacity, 1);
|
||||
}
|
||||
|
||||
.background-container.is-transparent-glass-lightweight .background-image.active {
|
||||
filter: blur(var(--transparent-background-blur, 16px));
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.background-container.is-transparent-glass-lightweight .background-image.active::after {
|
||||
background:
|
||||
linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%),
|
||||
rgba(128, 128, 128, 30%);
|
||||
}
|
||||
|
||||
/* 全局磨砂层 */
|
||||
.global-blur-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -38,9 +38,11 @@ const {
|
||||
onBackgroundBlurChange,
|
||||
onBackgroundPosterOpacityChange,
|
||||
onBlurChange,
|
||||
onGlassQualityChange,
|
||||
onOpacityChange,
|
||||
resetTransparencySettings,
|
||||
transparencyBlur,
|
||||
transparencyGlassQuality,
|
||||
transparencyOpacity,
|
||||
} = useTransparencySettings()
|
||||
</script>
|
||||
@@ -118,6 +120,34 @@ const {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-body-2 d-block mb-2">{{ t('theme.transparencyGlassQuality') }}</span>
|
||||
<VBtnToggle
|
||||
v-model="transparencyGlassQuality"
|
||||
mandatory
|
||||
divided
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
class="w-full"
|
||||
@update:model-value="onGlassQualityChange"
|
||||
>
|
||||
<VBtn value="lightweight" class="flex-1">
|
||||
{{ t('theme.transparencyGlassQualityLightweight') }}
|
||||
</VBtn>
|
||||
<VBtn value="realtime" class="flex-1">
|
||||
{{ t('theme.transparencyGlassQualityRealtime') }}
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
<p class="text-caption text-medium-emphasis mt-2 mb-0">
|
||||
{{
|
||||
transparencyGlassQuality === 'realtime'
|
||||
? t('theme.transparencyGlassQualityRealtimeHint')
|
||||
: t('theme.transparencyGlassQualityLightweightHint')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-body-2 d-block mb-2">{{ t('common.preset') }}</span>
|
||||
<VBtnGroup density="compact" variant="outlined" class="w-full">
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export type TransparencyGlassQuality = 'lightweight' | 'realtime'
|
||||
|
||||
export interface TransparencySettings {
|
||||
backgroundBlur: number
|
||||
backgroundPosterOpacity: number
|
||||
blur: number
|
||||
glassQuality: TransparencyGlassQuality
|
||||
level: string
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export const transparencyPresets = {
|
||||
low: { opacity: 0.1, blur: 5 },
|
||||
low: { opacity: 0.6, blur: 5 },
|
||||
medium: { opacity: 0.3, blur: 10 },
|
||||
high: { opacity: 0.6, blur: 15 },
|
||||
high: { opacity: 0.1, blur: 15 },
|
||||
}
|
||||
|
||||
export const TRANSPARENCY_SETTINGS_CHANGED_EVENT = 'transparency-settings-changed'
|
||||
|
||||
/** 将数值限制在指定范围内。 */
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
/** 读取玻璃质量档位,未知值回落到默认的轻量玻璃路径。 */
|
||||
function readGlassQuality(): TransparencyGlassQuality {
|
||||
const storedQuality = localStorage.getItem('transparency-glass-quality')
|
||||
|
||||
return storedQuality === 'realtime' ? 'realtime' : 'lightweight'
|
||||
}
|
||||
|
||||
/** 从本地存储读取透明主题设置。 */
|
||||
export function readTransparencySettings(): TransparencySettings {
|
||||
return {
|
||||
@@ -26,6 +38,7 @@ export function readTransparencySettings(): TransparencySettings {
|
||||
blur: parseFloat(localStorage.getItem('transparency-blur') || '10'),
|
||||
backgroundPosterOpacity: parseFloat(localStorage.getItem('transparency-background-poster-opacity') || '0'),
|
||||
backgroundBlur: parseFloat(localStorage.getItem('transparency-background-blur') || '16'),
|
||||
glassQuality: readGlassQuality(),
|
||||
level: localStorage.getItem('transparency-level') || 'medium',
|
||||
}
|
||||
}
|
||||
@@ -39,6 +52,7 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
||||
? clamp(settings.backgroundPosterOpacity, 0, 1)
|
||||
: 0,
|
||||
backgroundBlur: Number.isFinite(settings.backgroundBlur) ? clamp(settings.backgroundBlur, 0, 30) : 16,
|
||||
glassQuality: settings.glassQuality === 'realtime' ? 'realtime' : 'lightweight',
|
||||
level: settings.level,
|
||||
}
|
||||
|
||||
@@ -51,13 +65,20 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
||||
root.style.setProperty('--transparent-blur-heavy', `${normalized.blur * 1.6}px`)
|
||||
root.style.setProperty('--transparent-background-poster-opacity', (1 - normalized.backgroundPosterOpacity).toString())
|
||||
root.style.setProperty('--transparent-background-blur', `${normalized.backgroundBlur}px`)
|
||||
root.classList.toggle('transparent-blur-disabled', normalized.blur <= 0)
|
||||
root.classList.toggle('transparent-background-blur-disabled', normalized.backgroundBlur <= 0)
|
||||
root.classList.toggle('transparent-glass-lightweight', normalized.glassQuality === 'lightweight')
|
||||
root.classList.toggle('transparent-glass-realtime', normalized.glassQuality === 'realtime')
|
||||
|
||||
localStorage.setItem('transparency-opacity', normalized.opacity.toString())
|
||||
localStorage.setItem('transparency-blur', normalized.blur.toString())
|
||||
localStorage.setItem('transparency-background-poster-opacity', normalized.backgroundPosterOpacity.toString())
|
||||
localStorage.setItem('transparency-background-blur', normalized.backgroundBlur.toString())
|
||||
localStorage.setItem('transparency-glass-quality', normalized.glassQuality)
|
||||
localStorage.setItem('transparency-level', normalized.level)
|
||||
|
||||
window.dispatchEvent(new CustomEvent<TransparencySettings>(TRANSPARENCY_SETTINGS_CHANGED_EVENT, { detail: normalized }))
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -73,6 +94,7 @@ export function useTransparencySettings() {
|
||||
const transparencyBlur = ref(storedSettings.blur)
|
||||
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
||||
const backgroundBlur = ref(storedSettings.backgroundBlur)
|
||||
const transparencyGlassQuality = ref<TransparencyGlassQuality>(storedSettings.glassQuality)
|
||||
const transparencyLevel = ref(storedSettings.level)
|
||||
|
||||
const currentPresetLevel = computed(() => {
|
||||
@@ -95,6 +117,7 @@ export function useTransparencySettings() {
|
||||
blur: transparencyBlur.value,
|
||||
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
||||
backgroundBlur: backgroundBlur.value,
|
||||
glassQuality: transparencyGlassQuality.value,
|
||||
level: transparencyLevel.value,
|
||||
})
|
||||
|
||||
@@ -102,6 +125,7 @@ export function useTransparencySettings() {
|
||||
transparencyBlur.value = normalized.blur
|
||||
backgroundPosterOpacity.value = normalized.backgroundPosterOpacity
|
||||
backgroundBlur.value = normalized.backgroundBlur
|
||||
transparencyGlassQuality.value = normalized.glassQuality
|
||||
transparencyLevel.value = normalized.level
|
||||
}
|
||||
|
||||
@@ -149,12 +173,18 @@ export function useTransparencySettings() {
|
||||
syncTransparencySettings()
|
||||
}
|
||||
|
||||
/** 处理玻璃质量档位变化。 */
|
||||
function onGlassQualityChange() {
|
||||
syncTransparencySettings()
|
||||
}
|
||||
|
||||
/** 重置透明主题设置为默认值。 */
|
||||
function resetTransparencySettings() {
|
||||
transparencyOpacity.value = transparencyPresets.medium.opacity
|
||||
transparencyBlur.value = transparencyPresets.medium.blur
|
||||
backgroundPosterOpacity.value = 0
|
||||
backgroundBlur.value = 16
|
||||
transparencyGlassQuality.value = 'lightweight'
|
||||
transparencyLevel.value = 'medium'
|
||||
syncTransparencySettings()
|
||||
}
|
||||
@@ -167,10 +197,12 @@ export function useTransparencySettings() {
|
||||
onBackgroundBlurChange,
|
||||
onBackgroundPosterOpacityChange,
|
||||
onBlurChange,
|
||||
onGlassQualityChange,
|
||||
onOpacityChange,
|
||||
resetTransparencySettings,
|
||||
syncTransparencySettings,
|
||||
transparencyBlur,
|
||||
transparencyGlassQuality,
|
||||
transparencyOpacity,
|
||||
transparencyLevel,
|
||||
}
|
||||
|
||||
@@ -154,6 +154,13 @@ export default {
|
||||
transparencyBlur: 'Blur',
|
||||
backgroundPosterOpacity: 'Background Opacity',
|
||||
backgroundBlur: 'Background Frosted Blur',
|
||||
transparencyGlassQuality: 'Glass Effect Quality',
|
||||
transparencyGlassQualityLightweight: 'Lightweight',
|
||||
transparencyGlassQualityRealtime: 'Realtime',
|
||||
transparencyGlassQualityLightweightHint:
|
||||
'Keeps a similar translucent glass look while reducing live background sampling on dense pages.',
|
||||
transparencyGlassQualityRealtimeHint:
|
||||
'Uses the full live glass rendering path for higher visual quality, but may increase GPU usage.',
|
||||
transparencyReset: 'Reset',
|
||||
transparencyLow: 'Low Transparency',
|
||||
transparencyMedium: 'Medium Transparency',
|
||||
|
||||
@@ -152,6 +152,11 @@ export default {
|
||||
transparencyBlur: '模糊度',
|
||||
backgroundPosterOpacity: '背景透明度',
|
||||
backgroundBlur: '背景磨砂效果',
|
||||
transparencyGlassQuality: '玻璃效果质量',
|
||||
transparencyGlassQualityLightweight: '轻量',
|
||||
transparencyGlassQualityRealtime: '实时',
|
||||
transparencyGlassQualityLightweightHint: '保持接近实时毛玻璃的透明观感,减少密集页面的背景实时采样。',
|
||||
transparencyGlassQualityRealtimeHint: '使用完整实时毛玻璃路径,视觉质量更高,但可能增加 GPU 占用。',
|
||||
transparencyReset: '重置',
|
||||
transparencyLow: '低透明度',
|
||||
transparencyMedium: '中等透明度',
|
||||
|
||||
@@ -152,6 +152,11 @@ export default {
|
||||
transparencyBlur: '模糊度',
|
||||
backgroundPosterOpacity: '背景透明度',
|
||||
backgroundBlur: '背景磨砂效果',
|
||||
transparencyGlassQuality: '玻璃效果品質',
|
||||
transparencyGlassQualityLightweight: '輕量',
|
||||
transparencyGlassQualityRealtime: '即時',
|
||||
transparencyGlassQualityLightweightHint: '保持接近即時毛玻璃的透明觀感,減少密集頁面的背景即時取樣。',
|
||||
transparencyGlassQualityRealtimeHint: '使用完整即時毛玻璃路徑,視覺品質更高,但可能增加 GPU 佔用。',
|
||||
transparencyReset: '重置',
|
||||
transparencyLow: '低透明度',
|
||||
transparencyMedium: '中等透明度',
|
||||
|
||||
@@ -348,6 +348,11 @@ onActivated(async () => {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.content-group :deep(.slider-content-wrapper) {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-block-size: 16rem;
|
||||
}
|
||||
|
||||
.empty-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -707,6 +707,13 @@ html[data-theme="transparent"] .app-card-colorful,
|
||||
--app-surface-border: 0 solid transparent;
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-glass-lightweight .app-card-colorful,
|
||||
html[data-theme="transparent"].transparent-glass-lightweight .v-theme--transparent .app-card-colorful,
|
||||
html[data-theme="transparent"].transparent-glass-realtime .app-card-colorful,
|
||||
html[data-theme="transparent"].transparent-glass-realtime .v-theme--transparent .app-card-colorful {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
// 保证卡片右上角的浮动操作区始终高于可点击的卡片内容层,避免误触发详情打开。
|
||||
.app-card-top-action {
|
||||
z-index: 2;
|
||||
|
||||
@@ -165,3 +165,122 @@ html[data-theme="transparent"] {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-background-blur-disabled {
|
||||
.global-blur-layer,
|
||||
.background-image.active {
|
||||
backdrop-filter: none;
|
||||
filter: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-glass-lightweight {
|
||||
.global-blur-layer {
|
||||
display: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.layout-vertical-nav,
|
||||
.v-toolbar,
|
||||
.v-footer,
|
||||
.theme-customizer-panel-host,
|
||||
.theme-customizer-panel,
|
||||
.agent-assistant-panel,
|
||||
.agent-assistant-fab__bubble,
|
||||
.nav-button,
|
||||
.layout-navbar {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
}
|
||||
|
||||
.layout-vertical-nav,
|
||||
.theme-customizer-panel-host,
|
||||
.agent-assistant-panel {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
}
|
||||
|
||||
.v-list,
|
||||
.v-card:not(.no-blur, .bg-primary),
|
||||
.v-sheet,
|
||||
.v-expansion-panel {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.v-card:not(.no-blur, .bg-primary),
|
||||
.v-sheet,
|
||||
.v-expansion-panel,
|
||||
.v-list {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
.v-overlay__content,
|
||||
.v-overlay__content .v-card:not(.bg-primary),
|
||||
.v-overlay__content .v-list {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
}
|
||||
|
||||
.v-overlay__content .v-card:not(.bg-primary),
|
||||
.v-overlay__content .v-list {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||
}
|
||||
|
||||
.app-card-colorful {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-glass-realtime {
|
||||
.background-image.active {
|
||||
filter: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.v-list,
|
||||
.v-card:not(.no-blur, .bg-primary),
|
||||
.v-sheet,
|
||||
.v-expansion-panel {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.layout-vertical-nav,
|
||||
.v-toolbar,
|
||||
.v-footer,
|
||||
.theme-customizer-panel-host,
|
||||
.theme-customizer-panel,
|
||||
.agent-assistant-panel,
|
||||
.agent-assistant-fab__bubble,
|
||||
.nav-button,
|
||||
.layout-navbar,
|
||||
.v-overlay__content,
|
||||
.v-overlay__content .v-card:not(.bg-primary),
|
||||
.v-overlay__content .v-list {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
}
|
||||
|
||||
.layout-vertical-nav,
|
||||
.theme-customizer-panel-host,
|
||||
.agent-assistant-panel {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="transparent"].transparent-blur-disabled {
|
||||
.layout-vertical-nav,
|
||||
.v-list,
|
||||
.v-card:not(.no-blur),
|
||||
.v-toolbar,
|
||||
.v-footer,
|
||||
.v-sheet,
|
||||
.v-expansion-panel,
|
||||
.v-overlay__content,
|
||||
.v-overlay__content .v-card:not(.bg-primary),
|
||||
.v-overlay__content .v-list,
|
||||
.theme-customizer-panel-host,
|
||||
.theme-customizer-panel,
|
||||
.agent-assistant-panel,
|
||||
.agent-assistant-fab__bubble,
|
||||
.nav-button,
|
||||
.layout-navbar {
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ provide('rankingPropsKey', reactive({ ...props }))
|
||||
const componentLoaded = ref(false)
|
||||
// 是否已尝试加载
|
||||
const hasTriedLoading = ref(false)
|
||||
const loadingStarted = ref(false)
|
||||
|
||||
// 使用 shallowRef 避免横向卡片区的大数组深层代理
|
||||
const dataList = shallowRef<MediaInfo[]>([])
|
||||
@@ -36,6 +37,8 @@ const containerRef = ref<HTMLElement | null>(null)
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
if (loadingStarted.value) return
|
||||
loadingStarted.value = true
|
||||
if (!props.apipath) return
|
||||
dataList.value = await api.get(props.apipath)
|
||||
if (dataList.value.length > 0) {
|
||||
@@ -47,10 +50,29 @@ async function fetchData() {
|
||||
console.error(error)
|
||||
componentLoaded.value = true
|
||||
} finally {
|
||||
loadingStarted.value = false
|
||||
hasTriedLoading.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function isNearViewport(element: HTMLElement, rootMargin = 300) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight
|
||||
|
||||
return rect.bottom >= -rootMargin && rect.top <= viewportHeight + rootMargin
|
||||
}
|
||||
|
||||
/** IntersectionObserver 未及时回调时,首屏附近内容仍需主动发起数据请求。 */
|
||||
function loadIfNearViewport() {
|
||||
const element = containerRef.value
|
||||
if (!element || loadingStarted.value || hasTriedLoading.value) return
|
||||
|
||||
if (isNearViewport(element)) {
|
||||
fetchData()
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 IntersectionObserver 实现懒加载
|
||||
const { stop } = useIntersectionObserver(
|
||||
containerRef,
|
||||
@@ -65,7 +87,13 @@ const { stop } = useIntersectionObserver(
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(loadIfNearViewport)
|
||||
window.setTimeout(loadIfNearViewport, 600)
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
loadIfNearViewport()
|
||||
if (dataList.value.length == 0 && hasTriedLoading.value) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user