front/src/views/site/lib/site-liquid-glass.ts
zzh 0910538e6b
All checks were successful
Build and Deploy Front / deploy (push) Successful in 8m13s
feat: 官网首页 liquid-glass 与布局优化,并改进批量邮件发信体验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 13:42:29 +08:00

232 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { inject, provide, ref, type InjectionKey, type Ref } from 'vue';
import {
attachSelectionLens,
createGlassScene,
type GlassSceneController,
type SurfaceOptions,
} from '@glass-sdk/liquid-glass/dom';
import '@glass-sdk/liquid-glass/styles.css';
export type SiteGlassSurfaceOpts = SurfaceOptions;
type QueuedJob =
| { kind: 'surface'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void }
| { kind: 'indicator'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void };
export type SiteLiquidGlassApi = {
ready: Ref<boolean>;
fallback: Ref<boolean>;
init: (root: HTMLElement, content: HTMLElement) => void;
registerSurface: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void;
registerTabIndicator: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void;
dispose: () => void;
};
const SITE_LIQUID_GLASS_KEY: InjectionKey<SiteLiquidGlassApi> = Symbol('site-liquid-glass');
/** 履约 / 权益门卡:低折射 + 关闭 fluid,减轻矮卡左右竖边光学校弯 */
export const SITE_GLASS_PANEL_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 12,
refraction: 20,
fluid: false,
interactive: false,
};
export const SITE_GLASS_TABS_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 'capsule',
refraction: 56,
interactive: true,
};
export const SITE_GLASS_TAB_INDICATOR_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 'capsule',
refraction: 40,
interactive: false,
};
/** Welcome input — frosted glass, no tint */
export const SITE_GLASS_COMPOSER_OPTS: SiteGlassSurfaceOpts = {
material: 'regular',
appearance: 'light',
radius: 12,
refraction: 32,
fluid: true,
interactive: false,
};
export function provideSiteLiquidGlass(): SiteLiquidGlassApi {
const ready = ref(false);
const fallback = ref(false);
let scene: GlassSceneController | null = null;
let removers: Array<() => void> = [];
let queue: QueuedJob[] = [];
let readyObserver: MutationObserver | null = null;
let fallbackTimer: number | null = null;
const trackedSurfaces = new Set<HTMLElement>();
function clearFallbackTimer() {
if (fallbackTimer != null) {
window.clearTimeout(fallbackTimer);
fallbackTimer = null;
}
}
function isClipReady(el: HTMLElement) {
return !!el.getAttribute('style')?.includes('clip-path');
}
function syncReady() {
if ([...trackedSurfaces].some(isClipReady)) {
ready.value = true;
fallback.value = false;
clearFallbackTimer();
return true;
}
return false;
}
function observeSurface(el: HTMLElement) {
trackedSurfaces.add(el);
if (!readyObserver) {
readyObserver = new MutationObserver(() => {
syncReady();
});
}
readyObserver.observe(el, { attributes: true, attributeFilter: ['style'] });
}
function bindSurface(el: HTMLElement, opts?: SiteGlassSurfaceOpts) {
if (!scene) return () => undefined;
const remove = scene.addSurface(el, opts);
observeSurface(el);
syncReady();
return () => {
trackedSurfaces.delete(el);
remove();
};
}
function bindIndicator(el: HTMLElement, opts?: SiteGlassSurfaceOpts) {
if (!scene) return () => undefined;
const removeSurface = bindSurface(el, opts ?? SITE_GLASS_TAB_INDICATOR_OPTS);
const removeAnimator = scene.addAnimator(attachSelectionLens(el, () => 'full'));
return () => {
removeAnimator();
removeSurface();
};
}
function enqueue(
kind: 'surface' | 'indicator',
el: HTMLElement,
opts?: SiteGlassSurfaceOpts,
) {
let removeFn = () => undefined;
let flushed = false;
const job: QueuedJob = {
kind,
el,
opts,
setRemove: (fn) => {
flushed = true;
removeFn = fn;
},
};
queue.push(job);
return () => {
if (!flushed) {
queue = queue.filter((item) => item !== job);
return;
}
removeFn();
};
}
function registerSurface(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) {
if (!el) return () => undefined;
if (scene) {
const remove = bindSurface(el, opts);
removers.push(remove);
return remove;
}
return enqueue('surface', el, opts);
}
function registerTabIndicator(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) {
if (!el) return () => undefined;
if (scene) {
const remove = bindIndicator(el, opts);
removers.push(remove);
return remove;
}
return enqueue('indicator', el, opts);
}
function init(root: HTMLElement, content: HTMLElement) {
if (scene) return;
fallbackTimer = window.setTimeout(() => {
if (!ready.value) fallback.value = true;
}, 2200);
scene = createGlassScene(root, {
maxSurfaces: 16,
onDiagnostic: (diagnostic) => {
if (diagnostic.error) {
fallback.value = true;
ready.value = false;
} else if (diagnostic.maps > 0) {
syncReady();
}
},
});
scene.setContent(content);
for (const job of queue) {
const remove =
job.kind === 'indicator' ? bindIndicator(job.el, job.opts) : bindSurface(job.el, job.opts);
job.setRemove(remove);
removers.push(remove);
}
queue = [];
syncReady();
}
function dispose() {
clearFallbackTimer();
readyObserver?.disconnect();
readyObserver = null;
removers.forEach((remove) => remove());
removers = [];
queue = [];
trackedSurfaces.clear();
scene?.dispose();
scene = null;
ready.value = false;
fallback.value = false;
}
const api: SiteLiquidGlassApi = {
ready,
fallback,
init,
registerSurface,
registerTabIndicator,
dispose,
};
provide(SITE_LIQUID_GLASS_KEY, api);
return api;
}
export function useSiteLiquidGlass(): SiteLiquidGlassApi | null {
return inject(SITE_LIQUID_GLASS_KEY, null);
}