From e67edd9feb8f99df5ddcc152d541aae4c11fe242 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:47:22 +0800 Subject: [PATCH] fix(glass): scope fixed shell backplate (#603) --- src/@layouts/components/VerticalNavLayout.vue | 20 +++--- src/App.vue | 3 + .../useGlassFixedShellBackplate.spec.ts | 64 ++++++++++++++++++- .../useGlassFixedShellBackplate.ts | 24 ++++++- .../__tests__/glassOverlayMaterial.spec.ts | 2 +- src/styles/themes/glass.scss | 9 +-- 6 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/@layouts/components/VerticalNavLayout.vue b/src/@layouts/components/VerticalNavLayout.vue index fbd59f81..fac6abb2 100644 --- a/src/@layouts/components/VerticalNavLayout.vue +++ b/src/@layouts/components/VerticalNavLayout.vue @@ -76,6 +76,8 @@ export default defineComponent({ }) return () => { + const hasFixedShellBackplate = fixedShellBackplate.layers.value.length > 0 + // 👉 Vertical nav const verticalNav = h( VerticalNav, @@ -88,15 +90,14 @@ export default defineComponent({ }, ) - const fixedShellBackplateNode = - fixedShellBackplate.layers.value.length > 0 - ? h(GlassFixedShellBackplate, { - isOverlayNav: mdAndDown.value, - isOverlayNavActive: isOverlayNavActive.value, - layers: fixedShellBackplate.layers.value, - transitionDurationMs: fixedShellBackplate.transitionDurationMs, - }) - : null + const fixedShellBackplateNode = hasFixedShellBackplate + ? h(GlassFixedShellBackplate, { + isOverlayNav: mdAndDown.value, + isOverlayNavActive: isOverlayNavActive.value, + layers: fixedShellBackplate.layers.value, + transitionDurationMs: fixedShellBackplate.transitionDurationMs, + }) + : null // 👉 Navbar const navbar = h( @@ -158,6 +159,7 @@ export default defineComponent({ isCollapsedLayout.value && 'layout-vertical-nav-collapsed', isHorizontalLayout.value && 'layout-horizontal-nav-active', isHorizontalLayout.value && isNavbarScrolled && 'layout-horizontal-nav-scrolled', + hasFixedShellBackplate && 'layout-fixed-shell-backplate-active', route.meta.layoutWrapperClasses, !isHorizontalLayout.value && isNavbarScrolled && 'window-scrolled', ], diff --git a/src/App.vue b/src/App.vue index c3b9e248..1bb65d34 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,6 +29,7 @@ import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composab import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle' import { useGlassWallpaperTransaction } from '@/composables/useGlassWallpaperTransaction' import { + isChromiumFixedShellBackplateBrowser, provideGlassFixedShellBackplate, shouldUseGlassFixedShellBackplate, type GlassFixedShellBackplateLayer, @@ -308,6 +309,7 @@ function getBackgroundLayerStyle(layer: LoginBackgroundLayer) { } } +const needsStableFixedBackdrop = isChromiumFixedShellBackplateBrowser() const fixedShellBackplateLayers = computed(() => { const hasWallpaper = renderedBackgroundLayers.value.some(layer => Boolean(layer.url)) if ( @@ -315,6 +317,7 @@ const fixedShellBackplateLayers = computed { const eligible = { appearance: 'frosted' as const, hasWallpaper: true, isAuthenticated: true, + needsStableFixedBackdrop: true, quality: 'css' as const, themeName: 'glass', } - it('enables the direct wallpaper backplate only for authenticated frosted CSS rendering', () => { + it('enables the direct wallpaper backplate only for affected authenticated Chromium rendering', () => { expect(shouldUseGlassFixedShellBackplate(eligible)).toBe(true) + expect(shouldUseGlassFixedShellBackplate({ ...eligible, needsStableFixedBackdrop: false })).toBe(false) expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'clear' })).toBe(false) expect(shouldUseGlassFixedShellBackplate({ ...eligible, appearance: 'tinted' })).toBe(false) expect(shouldUseGlassFixedShellBackplate({ ...eligible, quality: 'balanced' })).toBe(false) @@ -21,3 +26,58 @@ describe('shouldUseGlassFixedShellBackplate', () => { expect(shouldUseGlassFixedShellBackplate({ ...eligible, hasWallpaper: false })).toBe(false) }) }) + +describe('isChromiumFixedShellBackplateBrowser', () => { + it('uses the Chromium engine brand for Chrome and Edge', () => { + expect( + isChromiumFixedShellBackplateBrowser({ + userAgent: 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36', + userAgentData: { + brands: [{ brand: 'Chromium' }, { brand: 'Google Chrome' }], + }, + }), + ).toBe(true) + expect( + isChromiumFixedShellBackplateBrowser({ + userAgent: 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0', + userAgentData: { + brands: [{ brand: 'Chromium' }, { brand: 'Microsoft Edge' }], + }, + }), + ).toBe(true) + expect( + isChromiumFixedShellBackplateBrowser({ + userAgent: 'Mozilla/5.0 Version/26.4 Safari/605.1.15', + userAgentData: { + brands: [{ brand: 'Safari' }], + }, + }), + ).toBe(false) + }) + + it.each([ + ['Chrome', 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36'], + ['Edge', 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0'], + ['Opera', 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 OPR/121.0.0.0'], + ['Vivaldi', 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 Vivaldi/7.6.0.0'], + ['Yandex Browser', 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 YaBrowser/25.8.0.0'], + ['Samsung Internet', 'Mozilla/5.0 Chrome/140.0.0.0 Mobile Safari/537.36 SamsungBrowser/28.0'], + ['Android WebView', 'Mozilla/5.0; wv) Version/4.0 Chrome/140.0.0.0 Mobile Safari/537.36'], + ['Chromium', 'Mozilla/5.0 Chromium/140.0.0.0 Safari/537.36'], + ])('falls back to Chromium UA tokens for %s', (_browser, userAgent) => { + expect(isChromiumFixedShellBackplateBrowser({ userAgent })).toBe(true) + }) + + it('leaves Safari and iOS Chrome on the native backdrop path', () => { + expect( + isChromiumFixedShellBackplateBrowser({ + userAgent: 'Mozilla/5.0 Version/26.4 Safari/605.1.15', + }), + ).toBe(false) + expect( + isChromiumFixedShellBackplateBrowser({ + userAgent: 'Mozilla/5.0 CriOS/140.0.0.0 Mobile/15E148 Safari/604.1', + }), + ).toBe(false) + }) +}) diff --git a/src/composables/useGlassFixedShellBackplate.ts b/src/composables/useGlassFixedShellBackplate.ts index 76b34946..a2269c2c 100644 --- a/src/composables/useGlassFixedShellBackplate.ts +++ b/src/composables/useGlassFixedShellBackplate.ts @@ -21,12 +21,25 @@ interface GlassFixedShellBackplateEligibility { hasWallpaper: boolean /** 当前页面是否处于认证后的主布局。 */ isAuthenticated: boolean + /** 当前浏览器是否需要绕开 Chromium fixed document backdrop 的重合成路径。 */ + needsStableFixedBackdrop: boolean /** 当前玻璃渲染质量。 */ quality: ThemeCustomizerGlassQuality /** Vuetify 当前解析后的主题名。 */ themeName: string } +interface GlassFixedShellBrowserIdentity { + /** User-Agent Client Hints 暴露的浏览器品牌。 */ + userAgentData?: { + brands?: readonly { + brand: string + }[] + } + /** Client Hints 不可用时使用的传统浏览器标识。 */ + userAgent: string +} + const GLASS_FIXED_SHELL_BACKPLATE_KEY: InjectionKey = Symbol('glass-fixed-shell-backplate') const EMPTY_FIXED_SHELL_LAYERS = shallowRef([]) @@ -35,10 +48,19 @@ const EMPTY_FIXED_SHELL_CONTEXT: GlassFixedShellBackplateContext = { transitionDurationMs: 0, } -/** 只有磨砂标准使用直接壁纸背板;其他材质和 GPU 质量继续走既有呈现路径。 */ +/** Chromium 使用独立 fixed 背板规避 document backdrop 重合成;WebKit 保留原生采样路径。 */ +export function isChromiumFixedShellBackplateBrowser(browserIdentity: GlassFixedShellBrowserIdentity = navigator) { + const brands = browserIdentity.userAgentData?.brands + if (brands?.length) return brands.some(({ brand }) => brand === 'Chromium') + + return /\b(?:Chrome|Chromium)\/\d+/u.test(browserIdentity.userAgent) +} + +/** 只有需要稳定 fixed 背板的 Chromium 磨砂标准布局使用直接壁纸采样。 */ export function shouldUseGlassFixedShellBackplate(options: GlassFixedShellBackplateEligibility) { return ( options.isAuthenticated && + options.needsStableFixedBackdrop && options.themeName === 'glass' && options.appearance === 'frosted' && options.quality === 'css' && diff --git a/src/styles/__tests__/glassOverlayMaterial.spec.ts b/src/styles/__tests__/glassOverlayMaterial.spec.ts index 9ae7a39a..f7c81c5b 100644 --- a/src/styles/__tests__/glassOverlayMaterial.spec.ts +++ b/src/styles/__tests__/glassOverlayMaterial.spec.ts @@ -39,7 +39,7 @@ describe('glass overlay material styles', () => { expect(styles).toContain('--glass-fixed-shell-backplate-filter: blur(min(var(--glass-blur-raised), 60px))') expect(styles).toMatch( - /&\[data-glass-appearance='frosted'\]\[data-glass-quality='css'\][\s\S]*?\.layout-vertical-nav::before,[\s\S]*?\.layout-navbar,[\s\S]*?backdrop-filter:\s*none\s*!important;/, + /&\[data-glass-appearance='frosted'\]\[data-glass-quality='css'\][\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-vertical-nav::before,[\s\S]*?\.layout-wrapper\.layout-fixed-shell-backplate-active \.layout-navbar,[\s\S]*?backdrop-filter:\s*none\s*!important;/, ) expect(backplate).toContain('filter: var(--glass-fixed-shell-backplate-filter)') expect(backplate).not.toContain('backdrop-filter') diff --git a/src/styles/themes/glass.scss b/src/styles/themes/glass.scss index 713e713f..728d44aa 100644 --- a/src/styles/themes/glass.scss +++ b/src/styles/themes/glass.scss @@ -462,11 +462,12 @@ html[data-theme='glass'] { } } - // 磨砂标准直接渲染稳定壁纸背板,固定功能层不得读取随页面重绘的 document backdrop。 + // Chromium 磨砂标准直接渲染稳定壁纸背板,固定功能层不得读取随页面重绘的 document backdrop。 &[data-glass-appearance='frosted'][data-glass-quality='css'] body[data-theme='glass'] { - .layout-vertical-nav::before, - .layout-navbar, - .layout-wrapper.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed .layout-navbar { + .layout-wrapper.layout-fixed-shell-backplate-active .layout-vertical-nav::before, + .layout-wrapper.layout-fixed-shell-backplate-active .layout-navbar, + .layout-wrapper.layout-fixed-shell-backplate-active.layout-horizontal-nav-active.layout-horizontal-nav-scrolled.layout-navbar-fixed + .layout-navbar { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }