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
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 };
|
|
}
|
|
|