app.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 }) => {
  23. server.start({
  24. onUnhandledRequest: 'error', // 未处理的请求触发网络错误
  25. });
  26. console.log(`启动了MSW`);
  27. })
  28. .catch((err) => {
  29. console.warn('Mock server module not found:', err);
  30. });
  31. }
  32. function AppContent({ children }: { children: ReactNode }) {
  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(() => {
  40. console.log('App launched.');
  41. });
  42. const changeTheme = (themeConfig: typeof lightTheme) => {
  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: lightTheme.token.colorBgLayout,
  76. color: lightTheme.token.colorText,
  77. }}
  78. >
  79. <div>加载多语言资源中...</div>
  80. <div style={{ display: 'none' }}>
  81. {children}
  82. </div>
  83. </div>
  84. );
  85. }
  86. // 显示错误状态
  87. if (error) {
  88. return (
  89. <div
  90. style={{
  91. display: 'flex',
  92. justifyContent: 'center',
  93. alignItems: 'center',
  94. height: '100vh',
  95. flexDirection: 'column',
  96. backgroundColor: lightTheme.token.colorBgLayout,
  97. color: lightTheme.token.colorText,
  98. }}
  99. >
  100. <div>多语言资源加载失败: {error}</div>
  101. <button
  102. onClick={() => window.location.reload()}
  103. style={{ marginTop: '16px', padding: '8px 16px' }}
  104. >
  105. 重新加载
  106. </button>
  107. <div style={{ display: 'none' }}>
  108. {children}
  109. </div>
  110. </div>
  111. );
  112. }
  113. // children 是将要会渲染的页面
  114. return (
  115. <ConfigProvider theme={currentTheme}>
  116. <IntlProvider
  117. locale={currentLocale}
  118. messages={messages as Record<string, string>}
  119. >
  120. <style>
  121. {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
  122. {`:root {
  123. --color-primary: ${currentTheme.token.colorPrimary};
  124. }`}
  125. </style>
  126. <div
  127. style={{
  128. backgroundColor: currentTheme.token.colorBgLayout,
  129. color: currentTheme.token.colorText,
  130. borderRadius: '8px',
  131. boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
  132. }}
  133. >
  134. <AcquisitionTracer />
  135. <QuotaAlertModal />
  136. {children}
  137. {process.env.NODE_ENV === 'development' && <ProductSelector />}
  138. <div
  139. style={{
  140. position: 'fixed',
  141. top: '50%',
  142. right: 16,
  143. transform: 'translateY(-50%)',
  144. display: 'flex',
  145. flexDirection: 'column',
  146. gap: 8, // 按钮间距
  147. zIndex: 1000,
  148. }}
  149. >
  150. <Button
  151. type="primary"
  152. shape="circle"
  153. size="large"
  154. onClick={() => changeTheme(darkTheme)}
  155. title="Switch to Dark Theme"
  156. >
  157. 🌙
  158. </Button>
  159. <Button
  160. type="primary"
  161. shape="circle"
  162. size="large"
  163. onClick={() => changeTheme(lightTheme)}
  164. title="Switch to Light Theme"
  165. >
  166. ☀️
  167. </Button>
  168. </div>
  169. </div>
  170. </IntlProvider>
  171. </ConfigProvider>
  172. );
  173. }
  174. function App({ children }: { children: ReactNode }) {
  175. // 只在 Cypress 测试环境下暴露 store 到 window 对象
  176. if (typeof window !== 'undefined' && (window as any).Cypress) {
  177. (window as any).store = store;
  178. }
  179. return (
  180. <Provider store={store}>
  181. <AppContent>{children}</AppContent>
  182. </Provider>
  183. );
  184. }
  185. export default App;