index.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // contexts/Style/index.js
  2. import React, { useReducer, useEffect, useMemo, createContext } from 'react';
  3. import { useLocation } from 'react-router-dom';
  4. import { isMobile as getIsMobile } from '../../helpers';
  5. // Action Types
  6. const ACTION_TYPES = {
  7. TOGGLE_SIDER: 'TOGGLE_SIDER',
  8. SET_SIDER: 'SET_SIDER',
  9. SET_MOBILE: 'SET_MOBILE',
  10. SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
  11. BATCH_UPDATE: 'BATCH_UPDATE',
  12. };
  13. // Constants
  14. const STORAGE_KEYS = {
  15. SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
  16. };
  17. const ROUTE_PATTERNS = {
  18. CONSOLE: '/console',
  19. };
  20. /**
  21. * 判断路径是否为控制台路由
  22. * @param {string} pathname - 路由路径
  23. * @returns {boolean} 是否为控制台路由
  24. */
  25. const isConsoleRoute = (pathname) => {
  26. return pathname === ROUTE_PATTERNS.CONSOLE ||
  27. pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
  28. };
  29. /**
  30. * 获取初始状态
  31. * @param {string} pathname - 当前路由路径
  32. * @returns {Object} 初始状态对象
  33. */
  34. const getInitialState = (pathname) => {
  35. const isMobile = getIsMobile();
  36. const isConsole = isConsoleRoute(pathname);
  37. const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
  38. return {
  39. isMobile,
  40. showSider: isConsole && !isMobile,
  41. siderCollapsed: isCollapsed,
  42. isManualSiderControl: false,
  43. };
  44. };
  45. /**
  46. * Style reducer
  47. * @param {Object} state - 当前状态
  48. * @param {Object} action - action 对象
  49. * @returns {Object} 新状态
  50. */
  51. const styleReducer = (state, action) => {
  52. switch (action.type) {
  53. case ACTION_TYPES.TOGGLE_SIDER:
  54. return {
  55. ...state,
  56. showSider: !state.showSider,
  57. isManualSiderControl: true,
  58. };
  59. case ACTION_TYPES.SET_SIDER:
  60. return {
  61. ...state,
  62. showSider: action.payload,
  63. isManualSiderControl: action.isManualControl ?? false,
  64. };
  65. case ACTION_TYPES.SET_MOBILE:
  66. return {
  67. ...state,
  68. isMobile: action.payload,
  69. };
  70. case ACTION_TYPES.SET_SIDER_COLLAPSED:
  71. // 自动保存到 localStorage
  72. localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
  73. return {
  74. ...state,
  75. siderCollapsed: action.payload,
  76. };
  77. case ACTION_TYPES.BATCH_UPDATE:
  78. return {
  79. ...state,
  80. ...action.payload,
  81. };
  82. default:
  83. return state;
  84. }
  85. };
  86. // Context (内部使用,不导出)
  87. const StyleContext = createContext(null);
  88. /**
  89. * 自定义 Hook - 处理窗口大小变化
  90. * @param {Function} dispatch - dispatch 函数
  91. * @param {Object} state - 当前状态
  92. * @param {string} pathname - 当前路径
  93. */
  94. const useWindowResize = (dispatch, state, pathname) => {
  95. useEffect(() => {
  96. const handleResize = () => {
  97. const isMobile = getIsMobile();
  98. dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
  99. // 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
  100. if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
  101. dispatch({
  102. type: ACTION_TYPES.SET_SIDER,
  103. payload: !isMobile,
  104. isManualControl: false
  105. });
  106. }
  107. };
  108. let timeoutId;
  109. const debouncedResize = () => {
  110. clearTimeout(timeoutId);
  111. timeoutId = setTimeout(handleResize, 150);
  112. };
  113. window.addEventListener('resize', debouncedResize);
  114. return () => {
  115. window.removeEventListener('resize', debouncedResize);
  116. clearTimeout(timeoutId);
  117. };
  118. }, [dispatch, state.isManualSiderControl, pathname]);
  119. };
  120. /**
  121. * 自定义 Hook - 处理路由变化
  122. * @param {Function} dispatch - dispatch 函数
  123. * @param {string} pathname - 当前路径
  124. */
  125. const useRouteChange = (dispatch, pathname) => {
  126. useEffect(() => {
  127. const isMobile = getIsMobile();
  128. const isConsole = isConsoleRoute(pathname);
  129. dispatch({
  130. type: ACTION_TYPES.BATCH_UPDATE,
  131. payload: {
  132. showSider: isConsole && !isMobile,
  133. isManualSiderControl: false,
  134. },
  135. });
  136. }, [pathname, dispatch]);
  137. };
  138. /**
  139. * 自定义 Hook - 处理移动设备侧边栏自动收起
  140. * @param {Object} state - 当前状态
  141. * @param {Function} dispatch - dispatch 函数
  142. */
  143. const useMobileSiderAutoHide = (state, dispatch) => {
  144. useEffect(() => {
  145. // 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
  146. if (state.isMobile && state.showSider && !state.isManualSiderControl) {
  147. dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
  148. }
  149. }, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
  150. };
  151. /**
  152. * Style Provider 组件
  153. */
  154. export const StyleProvider = ({ children }) => {
  155. const location = useLocation();
  156. const pathname = location.pathname;
  157. const [state, dispatch] = useReducer(
  158. styleReducer,
  159. pathname,
  160. getInitialState
  161. );
  162. useWindowResize(dispatch, state, pathname);
  163. useRouteChange(dispatch, pathname);
  164. useMobileSiderAutoHide(state, dispatch);
  165. const contextValue = useMemo(
  166. () => ({ state, dispatch }),
  167. [state]
  168. );
  169. return (
  170. <StyleContext.Provider value={contextValue}>
  171. {children}
  172. </StyleContext.Provider>
  173. );
  174. };
  175. /**
  176. * 自定义 Hook - 使用 StyleContext
  177. * @returns {{state: Object, dispatch: Function}} context value
  178. */
  179. export const useStyle = () => {
  180. const context = React.useContext(StyleContext);
  181. if (!context) {
  182. throw new Error('useStyle must be used within StyleProvider');
  183. }
  184. return context;
  185. };
  186. // 导出 action creators 以便外部使用
  187. export const styleActions = {
  188. toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
  189. setSider: (show, isManualControl = false) => ({
  190. type: ACTION_TYPES.SET_SIDER,
  191. payload: show,
  192. isManualControl
  193. }),
  194. setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
  195. setSiderCollapsed: (collapsed) => ({
  196. type: ACTION_TYPES.SET_SIDER_COLLAPSED,
  197. payload: collapsed
  198. }),
  199. };