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;