File size: 4,277 Bytes
494c9e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | /**
* 响应式设计工具模块
* 使用 CSS 变量和 matchMedia API 实现单一数据源
*/
/**
* 从 CSS 变量获取断点值
*/
const getBreakpointValue = (variableName: string): number => {
const root = document.documentElement;
const value = getComputedStyle(root).getPropertyValue(variableName).trim();
if (!value) {
throw new Error(`CSS variable ${variableName} is not defined`);
}
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throw new Error(`CSS variable ${variableName} is not a valid number: ${value}`);
}
return parsed;
};
// 延迟初始化,确保 CSS 已加载
let mobileBreakpoint: number | null = null;
let mobileMediaQuery: MediaQueryList | null = null;
/**
* 获取移动端断点查询对象
*/
const getMobileMediaQuery = (): MediaQueryList => {
if (mobileMediaQuery === null) {
if (mobileBreakpoint === null) {
mobileBreakpoint = getBreakpointValue('--breakpoint-mobile');
}
mobileMediaQuery = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`);
}
return mobileMediaQuery;
};
/** localStorage + 根节点 `data-force-narrow`,配套 _responsive.scss(宽屏用 :has(.main_frame)) */
export const FORCE_NARROW_STORAGE_KEY = 'info_radar_force_narrow';
/** 本标签设置或跨标签 storage 变化时都会派发(`window` 上) */
export const FORCE_NARROW_CHANGE_EVENT = 'force-narrow-change';
export const getForceNarrowScreen = (): boolean =>
localStorage.getItem(FORCE_NARROW_STORAGE_KEY) === '1';
export const syncForceNarrowAttribute = (): void => {
const root = document.documentElement;
if (getForceNarrowScreen()) root.setAttribute('data-force-narrow', '');
else root.removeAttribute('data-force-narrow');
};
let forceNarrowStorageListenerAttached = false;
/** 在入口最早调用;同步根节点并监听跨标签 storage */
export const initForceNarrowFromStorage = (): void => {
syncForceNarrowAttribute();
if (forceNarrowStorageListenerAttached) return;
forceNarrowStorageListenerAttached = true;
window.addEventListener('storage', (e: StorageEvent) => {
if (e.key !== FORCE_NARROW_STORAGE_KEY) return;
syncForceNarrowAttribute();
window.dispatchEvent(new Event(FORCE_NARROW_CHANGE_EVENT));
window.dispatchEvent(new Event('resize'));
});
};
export const setForceNarrowScreen = (enabled: boolean): void => {
if (enabled) localStorage.setItem(FORCE_NARROW_STORAGE_KEY, '1');
else localStorage.removeItem(FORCE_NARROW_STORAGE_KEY);
syncForceNarrowAttribute();
window.dispatchEvent(new Event(FORCE_NARROW_CHANGE_EVENT));
window.dispatchEvent(new Event('resize'));
};
/** 视口处在窄屏断点,或开启强制窄屏(未持久化时仅视口生效,与改动前一致) */
export const isNarrowScreen = (): boolean =>
getMobileMediaQuery().matches || getForceNarrowScreen();
/**
* 检测是否为移动端设备(基于设备能力)
* 移动端:有触屏支持,且没有鼠标或没有悬浮支持
*/
export const isMobileDevice = (): boolean => {
// 检查是否有触摸支持
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// 检查是否有鼠标(精确指针)
const hasMouse = window.matchMedia('(pointer: fine)').matches;
// 检查是否支持悬浮(hover)
const hasHover = window.matchMedia('(hover: hover)').matches;
// 移动端:有触摸支持,且没有鼠标或没有悬浮支持
return hasTouch && (!hasMouse || !hasHover);
};
/**
* 获取当前垂直滚动条占用的布局宽度(单位:px)
* - 传统滚动条模式下:返回大于 0 的数值
* - overlay 滚动条或无滚动条时:返回 0
*/
export const getVerticalScrollbarWidth = (): number => {
// window.innerWidth: 包含垂直滚动条宽度
// document.documentElement.clientWidth: 不包含垂直滚动条宽度
const width = window.innerWidth - document.documentElement.clientWidth;
return width > 0 ? width : 0;
};
/**
* 判断当前是否使用“占用布局宽度”的传统滚动条
* - true: 滚动条占用布局宽度(非 overlay)
* - false: 滚动条为 overlay 或当前无滚动条
*/
export const isTraditionalScrollbar = (): boolean => getVerticalScrollbarWidth() > 0; |