From 032bda61f689223cd39ac151a60e05d3f392484a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 23 Jul 2026 08:12:39 +0800 Subject: [PATCH] Fix mobile clear glass surfaces --- .../__tests__/useGlassOpticalRenderer.spec.ts | 50 +++++++++++++++++++ src/composables/useGlassOpticalRenderer.ts | 13 +++-- src/styles/themes/glass.scss | 18 ++----- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts index 4f0b32b0..781b87b4 100644 --- a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts +++ b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts @@ -1,4 +1,5 @@ import { + collectGlassOpticalRects, containsGlassOpticalSurface, setGlassRendererState, useGlassOpticalRenderer, @@ -35,11 +36,33 @@ vi.mock('three', async importOriginal => { } }) +/** 提供 renderer 单元测试所需的最小 ResizeObserver 实现。 */ class ResizeObserverMock { + /** 断开观察时无需执行额外逻辑。 */ disconnect() {} + + /** 测试不需要追踪具体被观察元素。 */ observe() {} } +/** 创建带稳定视口边界的光学表面元素。 */ +function appendOpticalSurface(className: string, bounds: Pick) { + const surface = document.createElement('section') + surface.className = className + surface.getBoundingClientRect = () => + ({ + ...bounds, + bottom: bounds.y + bounds.height, + left: bounds.x, + right: bounds.x + bounds.width, + top: bounds.y, + toJSON: () => ({}), + }) as DOMRect + document.body.append(surface) + + return surface +} + beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverMock) vi.stubGlobal('WebGLRenderingContext', class {}) @@ -49,6 +72,7 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks() vi.unstubAllGlobals() + document.body.replaceChildren() delete document.documentElement.dataset.glassRendererState }) @@ -89,6 +113,32 @@ describe('glass optical surface discovery', () => { expect(document.documentElement.dataset.glassRendererState).toBe('fallback') }) + it('excludes the navbar from mobile clear optical surfaces', () => { + appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 }) + appendOpticalSurface('dashboard-grid-item-content', { height: 200, width: 350, x: 20, y: 120 }) + + const rects = collectGlassOpticalRects(390, 800, 'clear') + + expect(rects).toHaveLength(1) + expect(rects[0]).toMatchObject({ height: 200, width: 350, x: 20, y: 120 }) + }) + + it('keeps the navbar in mobile frosted optical surfaces', () => { + appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 }) + + expect(collectGlassOpticalRects(390, 800, 'frosted')).toEqual([ + expect.objectContaining({ height: 64, width: 390, x: 0, y: 0 }), + ]) + }) + + it('keeps the navbar in desktop clear optical surfaces', () => { + appendOpticalSurface('layout-navbar', { height: 64, width: 1000, x: 0, y: 0 }) + + expect(collectGlassOpticalRects(1000, 800, 'clear')).toEqual([ + expect.objectContaining({ height: 64, width: 1000, x: 0, y: 0 }), + ]) + }) + it('recovers after consecutive WebGL context loss cycles', async () => { const canvas = document.createElement('canvas') const scope = effectScope() diff --git a/src/composables/useGlassOpticalRenderer.ts b/src/composables/useGlassOpticalRenderer.ts index bc1059d1..c37ee210 100644 --- a/src/composables/useGlassOpticalRenderer.ts +++ b/src/composables/useGlassOpticalRenderer.ts @@ -231,11 +231,18 @@ function isVisibleSurface(element: HTMLElement, bounds: DOMRect) { } /** 将活动界面中的高价值材质面集中转换为 renderer 矩形预算。 */ -export function collectGlassOpticalRects(viewportWidth: number, viewportHeight: number) { +export function collectGlassOpticalRects( + viewportWidth: number, + viewportHeight: number, + appearance: ThemeCustomizerGlassAppearance, +) { const candidates: GlassOpticalRect[] = [] const seen = new Set() for (const { rank, selector } of SURFACE_SELECTORS) { + // 移动端透明顶栏只使用稳定的 CSS 表面,避免滚动重扫时再次叠加壁纸折射。 + if (viewportWidth <= 600 && appearance === 'clear' && selector === '.layout-navbar') continue + for (const element of document.querySelectorAll(selector)) { if (seen.has(element)) continue seen.add(element) @@ -315,7 +322,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight - const rects = collectGlassOpticalRects(viewportWidth, viewportHeight) + const rects = collectGlassOpticalRects(viewportWidth, viewportHeight, toValue(options.appearance)) currentRects = rects const normalized = rects.map(rect => normalizeGlassOpticalRect(rect, viewportWidth, viewportHeight)) const uniformRects = resources.uniforms.uRects.value @@ -742,7 +749,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) if (!resources) return resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance) - scheduleFrame() + scheduleSurfaceUpdate() }, ) diff --git a/src/styles/themes/glass.scss b/src/styles/themes/glass.scss index eb2cd792..eb87f68c 100644 --- a/src/styles/themes/glass.scss +++ b/src/styles/themes/glass.scss @@ -41,8 +41,6 @@ html[data-theme='glass'] { --glass-control-backdrop-filter: none; --glass-control-prominent-backdrop-filter: none; --glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter); - --glass-clear-overlay-surface: rgba(11, 19, 34, 72%); - --glass-clear-mobile-nav-surface: rgba(11, 19, 34, 68%); --glass-control-icon-color: rgba(242, 245, 250, 70%); --glass-control-placeholder-color: rgba(242, 245, 250, 54%); --glass-control-shortcut-background: rgba(255, 255, 255, 8%); @@ -490,7 +488,8 @@ html[data-theme='glass'] { .v-overlay__content { border-radius: var(--app-overlay-radius); - > :where(.v-card, .v-sheet, .v-list) { + > :where(.v-card, .v-sheet, .v-list), + > form > :where(.v-card, .v-sheet) { -webkit-backdrop-filter: var(--glass-raised-backdrop-filter) !important; backdrop-filter: var(--glass-raised-backdrop-filter) !important; background-color: var(--glass-surface-raised) !important; @@ -499,7 +498,7 @@ html[data-theme='glass'] { } } - // 透明弹层沿用普通玻璃的色相,只提高 alpha,不引入独立底色。 + // 透明弹层与顶栏共用同一静态表面,不额外增加模糊或深色覆盖。 &[data-glass-appearance='clear'] { .v-overlay__content > :where(.v-card, .v-sheet, .v-list), .v-overlay__content > form > :where(.v-card, .v-sheet), @@ -507,7 +506,7 @@ html[data-theme='glass'] { .agent-assistant-panel { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; - background-color: var(--glass-clear-overlay-surface) !important; + background-color: var(--glass-surface-raised) !important; } } @@ -810,15 +809,6 @@ html[data-theme='glass'] { } @media (width <= 960px) { - &[data-glass-appearance='clear'] { - // 底部主导航和旁侧动态按钮共用 footer-nav-card,提高 alpha 后仍保持透明材质色相。 - .footer-nav-card.v-card { - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; - background-color: var(--glass-clear-mobile-nav-surface) !important; - } - } - // 全屏快捷弹窗释放外层玻璃背景,让定时服务卡片直接采样全局壁纸。 &[data-glass-appearance='frosted'] .scheduler-shortcut-dialog-card { -webkit-backdrop-filter: none !important;