全新popiart网站 react项目重构
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

226 lines
8.4 KiB

import { useEffect, useRef, useState, type RefObject } from "react";
// ---------------------------------------------------------------------------
// 配置:1920 设计稿等比缩放
// ---------------------------------------------------------------------------
/** Layout 设计稿基准宽度(px) */
export const LAYOUT_DESIGN_WIDTH = 1920;
/** Layout 内容区设计稿基准宽度(1920 - 90px 侧栏) */
export const LAYOUT_CONTENT_DESIGN_WIDTH = 1830;
/** 缩放比例下限 */
export const LAYOUT_SCALE_MIN = 0.65;
/** 窗口宽度低于此值时不缩放(走页面自身响应式) */
export const LAYOUT_SCALE_DISABLE_BELOW = 900;
const SCALED_HOST_CLASSES = ["layoutShellViewport_scaled", "layoutShellViewport_scaled--zoom", "layoutShellViewport_scaled--transform"] as const;
/** Layout 缩放 host(--layout-scale 写入处) */
export const LAYOUT_SCALE_HOST_SELECTOR = ".layoutShellViewport";
// ---------------------------------------------------------------------------
// 纯函数:计算比例 & 判断是否启用
// ---------------------------------------------------------------------------
export function computeLayoutViewportScale(viewportWidth: number, designWidth = LAYOUT_DESIGN_WIDTH): number {
if (viewportWidth < LAYOUT_SCALE_DISABLE_BELOW) {
return 1;
}
const raw = viewportWidth / designWidth;
return Math.max(LAYOUT_SCALE_MIN, raw);
}
function isLayoutScalingActive(windowWidth: number, scale: number): boolean {
return windowWidth >= LAYOUT_SCALE_DISABLE_BELOW && Math.abs(scale - 1) > 0.001;
}
export function supportsLayoutCssZoom(): boolean {
if (typeof CSS === "undefined" || !CSS.supports) return false;
return CSS.supports("zoom", "1");
}
/** 从 Layout host 读取 --layout-scale(portal 弹窗等 body 外挂载内容同步缩放用) */
export function readLayoutScaleFromHost(): number {
if (typeof document === "undefined") return 1;
const host = document.querySelector(LAYOUT_SCALE_HOST_SELECTOR);
if (!host) return 1;
const raw = getComputedStyle(host).getPropertyValue("--layout-scale").trim();
const parsed = Number.parseFloat(raw);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
}
/** 订阅 Layout host 上的 --layout-scale 变化 */
export function resolveLayoutScale(): number {
const windowWidth = typeof document !== "undefined" ? document.documentElement.clientWidth : LAYOUT_DESIGN_WIDTH;
const computedScale = computeLayoutViewportScale(windowWidth);
const hostScale = readLayoutScaleFromHost();
// host 未及时写入时(如首帧),回退到与 Layout 相同的计算值
if (Math.abs(hostScale - 1) < 0.001 && Math.abs(computedScale - 1) > 0.001) {
return computedScale;
}
return hostScale;
}
/** 订阅 Layout host 上的 --layout-scale 变化 */
export function useLayoutScale(): number {
const [scale, setScale] = useState(1);
useEffect(() => {
const sync = () => setScale(resolveLayoutScale());
sync();
window.addEventListener("resize", sync);
const host = document.querySelector(LAYOUT_SCALE_HOST_SELECTOR);
if (!host) {
return () => window.removeEventListener("resize", sync);
}
const observer = new MutationObserver(sync);
observer.observe(host, { attributes: true, attributeFilter: ["style", "class"] });
return () => {
window.removeEventListener("resize", sync);
observer.disconnect();
};
}, []);
return scale;
}
// ---------------------------------------------------------------------------
// DOM:同步 host 状态 / 内层尺寸 / 清理
// ---------------------------------------------------------------------------
function syncHostScaleState(host: HTMLElement, scale: number, scaling: boolean, useZoom: boolean): void {
host.style.setProperty("--layout-scale", String(scale));
host.classList.toggle("layoutShellViewport_scaled", scaling);
host.classList.toggle("layoutShellViewport_scaled--zoom", scaling && useZoom);
host.classList.toggle("layoutShellViewport_scaled--transform", scaling && !useZoom);
}
function clearScaledElementStyles(inner: HTMLDivElement, spacer: HTMLDivElement): void {
spacer.style.width = "";
spacer.style.height = "";
spacer.style.minHeight = "";
inner.style.width = "";
inner.style.height = "";
inner.style.minHeight = "";
inner.style.zoom = "";
}
function applyScaledElementStyles(inner: HTMLDivElement, spacer: HTMLDivElement, scale: number, viewportWidth: number, viewportHeight: number, designWidth: number, useZoom: boolean): void {
const scaledInnerWidth = scale > 0 ? viewportWidth / scale : designWidth;
const scaledInnerHeight = viewportHeight > 0 && scale > 0 ? viewportHeight / scale : 0;
inner.style.width = `${String(scaledInnerWidth)}px`;
inner.style.height = scaledInnerHeight > 0 ? `${String(scaledInnerHeight)}px` : "";
inner.style.minHeight = inner.style.height;
if (useZoom) {
inner.style.zoom = String(scale);
const rect = inner.getBoundingClientRect();
spacer.style.width = `${String(rect.width)}px`;
spacer.style.height = `${String(Math.max(rect.height, viewportHeight))}px`;
} else {
inner.style.zoom = "";
spacer.style.width = `${String(scaledInnerWidth * scale)}px`;
spacer.style.height = `${String(inner.offsetHeight * scale)}px`;
}
spacer.style.minHeight = `${String(viewportHeight)}px`;
}
function teardownHostScaleState(host: HTMLElement | null | undefined): void {
if (!host) return;
host.style.removeProperty("--layout-scale");
host.classList.remove(...SCALED_HOST_CLASSES);
}
// ---------------------------------------------------------------------------
// Hook:缩放指定内容区域,不缩放整页 html
// ---------------------------------------------------------------------------
type UseLayoutViewportScaleResult = {
scaleInnerRef: RefObject<HTMLDivElement>;
scaleSpacerRef: RefObject<HTMLDivElement>;
};
type UseLayoutViewportScaleOptions = {
designWidth?: number;
measureRef?: RefObject<HTMLElement | null>;
};
/**
* DOM 结构默认兼容旧 Layout 缩放,也可通过 measureRef / designWidth 改成内容区缩放。
* 通过补偿 viewport / scale 避免缩放后底部留白(见 Layout.scss)。
*/
export function useLayoutViewportScale(viewportRef: RefObject<HTMLElement | null>, options?: UseLayoutViewportScaleOptions): UseLayoutViewportScaleResult {
const scaleInnerRef = useRef<HTMLDivElement>(null!);
const scaleSpacerRef = useRef<HTMLDivElement>(null!);
const designWidth = options?.designWidth ?? LAYOUT_DESIGN_WIDTH;
const measureRef = options?.measureRef;
useEffect(() => {
const useZoom = supportsLayoutCssZoom();
const viewport = viewportRef.current;
const measureElement = measureRef?.current;
const inner = scaleInnerRef.current;
const spacer = scaleSpacerRef.current;
if (!inner || !spacer) return;
let disposed = false;
const apply = () => {
if (disposed) return;
const viewportWidth = measureElement?.clientWidth ?? document.documentElement.clientWidth;
const viewportHeight = measureElement?.clientHeight ?? window.innerHeight;
const windowWidth = document.documentElement.clientWidth;
const scaleBasisWidth = windowWidth >= LAYOUT_SCALE_DISABLE_BELOW ? Math.max(viewportWidth, LAYOUT_SCALE_DISABLE_BELOW) : viewportWidth;
const scale = computeLayoutViewportScale(scaleBasisWidth, designWidth);
const scaling = isLayoutScalingActive(windowWidth, scale);
const host = viewport ?? document.documentElement;
syncHostScaleState(host, scale, scaling, useZoom);
if (!scaling) {
clearScaledElementStyles(inner, spacer);
return;
}
applyScaledElementStyles(inner, spacer, scale, viewportWidth, viewportHeight, designWidth, useZoom);
};
const resizeObserver = new ResizeObserver(() => apply());
resizeObserver.observe(inner);
if (measureElement != null && measureElement !== inner) {
resizeObserver.observe(measureElement);
}
const mutationObserver = new MutationObserver(() => apply());
mutationObserver.observe(inner, { childList: true, subtree: true, attributes: true });
apply();
window.addEventListener("resize", apply);
return () => {
disposed = true;
window.removeEventListener("resize", apply);
resizeObserver.disconnect();
mutationObserver.disconnect();
teardownHostScaleState(viewport ?? viewportRef.current);
clearScaledElementStyles(inner, spacer);
};
}, [designWidth, measureRef, viewportRef]);
return { scaleInnerRef, scaleSpacerRef };
}