app.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 ProductSelector from './components/ProductSelector';
  11. import { setTheme } from './states/themeSlice';
  12. import QuotaAlertModal from './pages/security/QuotaAlertModal';
  13. import AcquisitionTracer from './pages/exam/components/acquisitionTracer';
  14. import FeatureNotAvailableFeedback from './components/FeatureNotAvailableFeedback';
  15. import { setFeatureNotAvailableOpen } from './states/featureNotAvailableSlice';
  16. import { setBusinessFlow } from './states/BusinessFlowSlice';
  17. import { logger } from './log/logger';
  18. console.log = logger.log;
  19. console.warn = logger.warn;
  20. console.error = logger.error;
  21. console.log(`process.env.USE_MSW: ${process.env.USE_MSW}`);
  22. console.log(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);
  23. if (process.env.NODE_ENV === 'development' && process.env.USE_MSW === 'true') {
  24. import('../mocks/server')
  25. .then(({ server }): void => {
  26. server.start({
  27. onUnhandledRequest: 'error', // 未处理的请求触发网络错误
  28. });
  29. console.log(`启动了MSW`);
  30. })
  31. .catch((err): void => {
  32. console.warn('Mock server module not found:', err);
  33. });
  34. }
  35. function AppContent({ children }: { children: ReactNode }): JSX.Element {
  36. const dispatch = useAppDispatch();
  37. const { messages, loading, error, currentLocale } = useAppSelector(
  38. (state) => state.i18n
  39. );
  40. const isFeatureNotAvailableOpen = useAppSelector(
  41. (state) => state.featureNotAvailable.isOpen
  42. );
  43. const { currentTheme, themeType } = useAppSelector((state) => state.theme);
  44. const [isI18nReady, setIsI18nReady] = useState(false);
  45. useLaunch((): void => {
  46. console.log('App launched.');
  47. });
  48. useEffect(() => {
  49. // 先加载软件信息获取语言设置
  50. store
  51. .dispatch(initializeProductState())
  52. .unwrap()
  53. .then((productState) => {
  54. console.log(`初始化,拉取到产品信息:${JSON.stringify(productState)}`);
  55. // 从 current_locale 提取语言代码
  56. const languageCode = productState.language;
  57. return dispatch(loadI18nMessages(languageCode)).unwrap();
  58. })
  59. .then(() => {
  60. setIsI18nReady(true);
  61. })
  62. .catch((error) => {
  63. console.error('初始化失败:', error);
  64. // 阻止加载后面的页面
  65. setIsI18nReady(false);
  66. });
  67. }, [dispatch]);
  68. console.log('当前语言:', currentLocale);
  69. console.log('messages', messages);
  70. // children 是将要会渲染的页面
  71. // IntlProvider 始终存在,使用默认值避免 useIntl 报错
  72. return (
  73. <ConfigProvider theme={currentTheme}>
  74. <IntlProvider
  75. locale={currentLocale ? currentLocale.split('_')[0] : 'en'} // en_US -> en,提供默认值
  76. messages={(messages as Record<string, string>) || {}} // 提供空对象作为默认值
  77. >
  78. <style>
  79. {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
  80. {`:root {
  81. --color-primary: ${currentTheme.token.colorPrimary};
  82. --color-bg-layout: ${currentTheme.token.colorBgLayout};
  83. --color-text: ${currentTheme.token.colorText};
  84. --button-bg-hover: ${currentTheme.token.buttonBgHover};
  85. }`}
  86. </style>
  87. {/* 加载状态覆盖层 */}
  88. {(loading || !isI18nReady) && (
  89. <div
  90. style={{
  91. position: 'fixed',
  92. top: 0,
  93. left: 0,
  94. right: 0,
  95. bottom: 0,
  96. zIndex: 9999,
  97. display: 'flex',
  98. justifyContent: 'center',
  99. alignItems: 'center',
  100. backgroundColor: currentTheme.token.colorBgLayout,
  101. color: currentTheme.token.colorText,
  102. }}
  103. >
  104. <div>加载多语言资源中...</div>
  105. </div>
  106. )}
  107. {/* 错误状态覆盖层 */}
  108. {error && (
  109. <div
  110. style={{
  111. position: 'fixed',
  112. top: 0,
  113. left: 0,
  114. right: 0,
  115. bottom: 0,
  116. zIndex: 9999,
  117. display: 'flex',
  118. justifyContent: 'center',
  119. alignItems: 'center',
  120. flexDirection: 'column',
  121. backgroundColor: currentTheme.token.colorBgLayout,
  122. color: currentTheme.token.colorText,
  123. }}
  124. >
  125. <div>多语言资源加载失败: {error}</div>
  126. <button
  127. onClick={() => window.location.reload()}
  128. style={{ marginTop: '16px', padding: '8px 16px' }}
  129. >
  130. 重新加载
  131. </button>
  132. </div>
  133. )}
  134. {/* children 始终被渲染,满足 Taro 框架要求 */}
  135. <div
  136. style={{
  137. backgroundColor: currentTheme.token.colorBgLayout,
  138. color: currentTheme.token.colorText,
  139. borderRadius: '8px',
  140. boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
  141. }}
  142. >
  143. <AcquisitionTracer />
  144. <QuotaAlertModal />
  145. <FeatureNotAvailableFeedback
  146. open={isFeatureNotAvailableOpen}
  147. onClose={() => dispatch(setFeatureNotAvailableOpen(false))}
  148. onContinue={() => {
  149. dispatch(setFeatureNotAvailableOpen(false));
  150. dispatch(setBusinessFlow('continueAfterFeatureNotAvailable'));
  151. }}
  152. />
  153. {children}
  154. {process.env.NODE_ENV === 'development' && <ProductSelector />}
  155. <div
  156. style={{
  157. position: 'fixed',
  158. top: '50%',
  159. right: 16,
  160. transform: 'translateY(-50%)',
  161. display: 'flex',
  162. flexDirection: 'column',
  163. gap: 8, // 按钮间距
  164. zIndex: 1000,
  165. }}
  166. >
  167. <Button
  168. type="primary"
  169. shape="circle"
  170. size="large"
  171. onClick={() => dispatch(setTheme('dark'))}
  172. title="Switch to Dark Theme"
  173. disabled={themeType === 'dark'}
  174. >
  175. 🌙
  176. </Button>
  177. <Button
  178. type="primary"
  179. shape="circle"
  180. size="large"
  181. onClick={() => dispatch(setTheme('light'))}
  182. title="Switch to Light Theme"
  183. disabled={themeType === 'light'}
  184. >
  185. ☀️
  186. </Button>
  187. </div>
  188. </div>
  189. </IntlProvider>
  190. </ConfigProvider>
  191. );
  192. }
  193. function App({ children }: { children: ReactNode }): JSX.Element {
  194. // 只在 Cypress 测试环境下暴露 store 到 window 对象
  195. if (
  196. typeof window !== 'undefined' &&
  197. (window as unknown as { Cypress: unknown }).Cypress
  198. ) {
  199. (window as unknown as { store: typeof store }).store = store;
  200. }
  201. return (
  202. <Provider store={store}>
  203. <AppContent>{children}</AppContent>
  204. </Provider>
  205. );
  206. }
  207. export default App;