app.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { useState, useEffect, ReactNode } from 'react';
  2. import { useLaunch } from '@tarojs/taro';
  3. import { IntlProvider } from 'react-intl';
  4. import { ConfigProvider, Button } from 'antd';
  5. import { Provider } from 'react-redux';
  6. import store, { useAppDispatch, useAppSelector } from './states/store';
  7. import { initializeProductState } from './states/productSlice';
  8. import { loadI18nMessages } from './states/i18nSlice';
  9. import './app.css';
  10. import { lightTheme, darkTheme } from './themes';
  11. import ProductSelector from './components/ProductSelector';
  12. import QuotaAlertModal from './pages/security/QuotaAlertModal';
  13. import AcquisitionTracer from './pages/exam/components/acquisitionTracer';
  14. import { logger } from './log/logger';
  15. console.log = logger.log;
  16. console.warn = logger.warn;
  17. console.error = logger.error;
  18. console.log(`process.env.USE_MSW: ${process.env.USE_MSW}`);
  19. console.log(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);
  20. if (process.env.NODE_ENV === 'development' && process.env.USE_MSW === 'true') {
  21. import('../mocks/server')
  22. .then(({ server }): void => {
  23. server.start({
  24. onUnhandledRequest: 'error', // 未处理的请求触发网络错误
  25. });
  26. console.log(`启动了MSW`);
  27. })
  28. .catch((err): void => {
  29. console.warn('Mock server module not found:', err);
  30. });
  31. }
  32. function AppContent({ children }: { children: ReactNode }): JSX.Element {
  33. const dispatch = useAppDispatch();
  34. const { messages, loading, error, currentLocale } = useAppSelector(
  35. (state) => state.i18n
  36. );
  37. const [currentTheme, setCurrentTheme] = useState(lightTheme); // 默认使用 light 主题
  38. const [isI18nReady, setIsI18nReady] = useState(false);
  39. useLaunch((): void => {
  40. console.log('App launched.');
  41. });
  42. const changeTheme = (themeConfig: typeof lightTheme): void => {
  43. setCurrentTheme(themeConfig);
  44. };
  45. // 检测浏览器语言
  46. const browserLocale = (window.navigator.language || 'en')
  47. .toLowerCase()
  48. .split('-')[0];
  49. useEffect(() => {
  50. store.dispatch(initializeProductState());
  51. // 应用启动时加载多语言资源
  52. const localeToLoad = ['zh', 'en'].includes(browserLocale)
  53. ? browserLocale
  54. : 'en';
  55. dispatch(loadI18nMessages(localeToLoad))
  56. .unwrap()
  57. .then(() => {
  58. setIsI18nReady(true);
  59. })
  60. .catch((error) => {
  61. console.error('加载多语言资源失败:', error);
  62. // 阻止加载后面的页面
  63. setIsI18nReady(false);
  64. });
  65. }, [dispatch, browserLocale]);
  66. // 显示加载状态
  67. if (loading || !isI18nReady) {
  68. return (
  69. <div
  70. style={{
  71. display: 'flex',
  72. justifyContent: 'center',
  73. alignItems: 'center',
  74. height: '100vh',
  75. backgroundColor: currentTheme.token.colorBgLayout,
  76. color: currentTheme.token.colorText,
  77. }}
  78. >
  79. <div>加载多语言资源中...</div>
  80. <div style={{ display: 'none' }}>{children}</div>
  81. </div>
  82. );
  83. }
  84. // 显示错误状态
  85. if (error) {
  86. return (
  87. <div
  88. style={{
  89. display: 'flex',
  90. justifyContent: 'center',
  91. alignItems: 'center',
  92. height: '100vh',
  93. flexDirection: 'column',
  94. backgroundColor: currentTheme.token.colorBgLayout,
  95. color: currentTheme.token.colorText,
  96. }}
  97. >
  98. <div>多语言资源加载失败: {error}</div>
  99. <button
  100. onClick={() => window.location.reload()}
  101. style={{ marginTop: '16px', padding: '8px 16px' }}
  102. >
  103. 重新加载
  104. </button>
  105. <div style={{ display: 'none' }}>{children}</div>
  106. </div>
  107. );
  108. }
  109. // children 是将要会渲染的页面
  110. return (
  111. <ConfigProvider theme={currentTheme}>
  112. <IntlProvider
  113. locale={currentLocale}
  114. messages={messages as Record<string, string>}
  115. >
  116. <style>
  117. {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
  118. {`:root {
  119. --color-primary: ${currentTheme.token.colorPrimary};
  120. --color-bg-layout: ${currentTheme.token.colorBgLayout};
  121. --color-text: ${currentTheme.token.colorText};
  122. }`}
  123. </style>
  124. <div
  125. style={{
  126. backgroundColor: currentTheme.token.colorBgLayout,
  127. color: currentTheme.token.colorText,
  128. borderRadius: '8px',
  129. boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
  130. }}
  131. >
  132. <AcquisitionTracer />
  133. <QuotaAlertModal />
  134. {children}
  135. {process.env.NODE_ENV === 'development' && <ProductSelector />}
  136. <div
  137. style={{
  138. position: 'fixed',
  139. top: '50%',
  140. right: 16,
  141. transform: 'translateY(-50%)',
  142. display: 'flex',
  143. flexDirection: 'column',
  144. gap: 8, // 按钮间距
  145. zIndex: 1000,
  146. }}
  147. >
  148. <Button
  149. type="primary"
  150. shape="circle"
  151. size="large"
  152. onClick={() => changeTheme(darkTheme)}
  153. title="Switch to Dark Theme"
  154. >
  155. 🌙
  156. </Button>
  157. <Button
  158. type="primary"
  159. shape="circle"
  160. size="large"
  161. onClick={() => changeTheme(lightTheme)}
  162. title="Switch to Light Theme"
  163. >
  164. ☀️
  165. </Button>
  166. </div>
  167. </div>
  168. </IntlProvider>
  169. </ConfigProvider>
  170. );
  171. }
  172. function App({ children }: { children: ReactNode }): JSX.Element {
  173. // 只在 Cypress 测试环境下暴露 store 到 window 对象
  174. if (
  175. typeof window !== 'undefined' &&
  176. (window as unknown as { Cypress: unknown }).Cypress
  177. ) {
  178. (window as unknown as { store: typeof store }).store = store;
  179. }
  180. return (
  181. <Provider store={store}>
  182. <AppContent>{children}</AppContent>
  183. </Provider>
  184. );
  185. }
  186. export default App;