Spaces:
Paused
Paused
File size: 4,791 Bytes
494c9e4 28147a1 494c9e4 28147a1 494c9e4 28147a1 494c9e4 966d483 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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | /**
* 响应式设计工具模块
* 使用 CSS 变量和 matchMedia API 实现单一数据源
*/
import { lsGet, lsRemove, lsSet } from '../storage/localStorageHelpers';
/**
* 从 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 =>
lsGet(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) lsSet(FORCE_NARROW_STORAGE_KEY, '1');
else lsRemove(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();
/**
* 桌面端正文滚动根。
* analysis 页把滚动下放到 `#results`(浮条留在 `.right_panel` 外);其它页仍滚 `.right_panel`。
*/
export const getDesktopTextScrollRoot = (): HTMLElement | null => {
const results = document.getElementById('results');
if (results) {
const oy = getComputedStyle(results).overflowY;
if (oy === 'auto' || oy === 'scroll') return results;
}
return document.querySelector('.right_panel') as HTMLElement | null;
};
/**
* 检测是否为移动端设备(基于设备能力)
* 移动端:有触屏支持,且没有鼠标或没有悬浮支持
*/
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; |