app.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { useState, useEffect, ReactNode } from 'react';
  2. import { useLaunch } from '@tarojs/taro';
  3. import { IntlProvider } from 'react-intl';
  4. import { ConfigProvider } 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 { checkServerConnection } from './features/serverConfig';
  10. import { initializeAnnotationManager, cleanupAnnotationManager } from './features/imageAnnotation';
  11. import { platform } from './utils/platform';
  12. import './app.css';
  13. import ProductSelector from './components/ProductSelector';
  14. import ThemeSwitcher from './components/ThemeSwitcher';
  15. import QuotaAlertModal from './pages/security/QuotaAlertModal';
  16. import AcquisitionTracer from './pages/exam/components/acquisitionTracer';
  17. import FeatureNotAvailableFeedback from './components/FeatureNotAvailableFeedback';
  18. import { setFeatureNotAvailableOpen } from './states/featureNotAvailableSlice';
  19. import { setBusinessFlow } from './states/BusinessFlowSlice';
  20. import { logger } from './log/logger';
  21. import { theme } from 'antd';
  22. import ServerConfigModal from './features/serverConfig/components/ServerConfigModal';
  23. console.log = logger.log;
  24. console.warn = logger.warn;
  25. console.error = logger.error;
  26. console.debug = logger.debug;
  27. console.log(`process.env.USE_MSW: ${process.env.USE_MSW}`);
  28. console.log(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);
  29. console.debug('debug level')
  30. if (process.env.NODE_ENV === 'development' && process.env.USE_MSW === 'true') {
  31. import('../mocks/server')
  32. .then(({ server }): void => {
  33. server.start({
  34. onUnhandledRequest: 'error', // 未处理的请求触发网络错误
  35. });
  36. console.log(`启动了MSW`);
  37. })
  38. .catch((err): void => {
  39. console.warn('Mock server module not found:', err);
  40. });
  41. }
  42. function AppContent({ children }: { children: ReactNode }): JSX.Element {
  43. const dispatch = useAppDispatch();
  44. const { messages, loading, error, currentLocale } = useAppSelector(
  45. (state) => state.i18n
  46. );
  47. const isFeatureNotAvailableOpen = useAppSelector(
  48. (state) => state.featureNotAvailable.isOpen
  49. );
  50. const { currentTheme, themeType } = useAppSelector((state) => state.theme);
  51. const [isI18nReady, setIsI18nReady] = useState(false);
  52. const [showConfigModal, setShowConfigModal] = useState(false);
  53. const [connectionChecked, setConnectionChecked] = useState(false);
  54. const themeWithAlgorithm = {
  55. ...currentTheme,
  56. algorithm:
  57. themeType === 'light' ? theme.defaultAlgorithm : theme.darkAlgorithm,
  58. };
  59. useLaunch((): void => {
  60. console.log('App launched.');
  61. });
  62. useEffect(() => {
  63. // 浏览器环境不需要服务器连接检查,直接初始化应用
  64. if (platform.isBrowser) {
  65. console.log('浏览器环境,跳过服务器连接检查,直接初始化应用');
  66. initializeApp();
  67. return;
  68. }
  69. // Electron/Cordova 环境:检查服务器连接
  70. dispatch(checkServerConnection())
  71. .unwrap()
  72. .then((result) => {
  73. setConnectionChecked(true);
  74. if (result.needsConfig) {
  75. // 需要配置,显示对话框
  76. console.log('检测到需要服务器配置,显示配置对话框');
  77. setShowConfigModal(true);
  78. } else {
  79. // 连接正常,继续正常初始化
  80. console.log('服务器连接正常,开始应用初始化');
  81. return initializeApp();
  82. }
  83. })
  84. .catch((error) => {
  85. console.error('连接检查失败:', error);
  86. setConnectionChecked(true);
  87. setShowConfigModal(true);
  88. });
  89. // 应用退出时清理注释管理器
  90. return () => {
  91. cleanupAnnotationManager().catch(console.error);
  92. };
  93. }, [dispatch]);
  94. // 应用正常初始化函数
  95. const initializeApp = async () => {
  96. try {
  97. const productState = await dispatch(initializeProductState()).unwrap();
  98. console.log(`初始化,拉取到产品信息:${JSON.stringify(productState)}`);
  99. const languageCode = productState.language;
  100. await dispatch(loadI18nMessages(languageCode)).unwrap();
  101. // ✅ 初始化注释管理器(在产品状态和国际化之后)
  102. await initializeAnnotationManager();
  103. setIsI18nReady(true);
  104. } catch (error) {
  105. console.error('应用初始化失败:', error);
  106. // 显示配置对话框,让用户重新配置
  107. setShowConfigModal(true);
  108. }
  109. };
  110. // 配置保存后的处理
  111. const handleConfigSaved = () => {
  112. setShowConfigModal(false);
  113. // 重新检查连接并初始化应用
  114. dispatch(checkServerConnection())
  115. .unwrap()
  116. .then((result) => {
  117. if (!result.needsConfig) {
  118. initializeApp();
  119. }
  120. })
  121. .catch((error) => {
  122. console.error('重新检查连接失败:', error);
  123. });
  124. };
  125. console.log('当前语言:', currentLocale);
  126. console.log('messages', messages);
  127. // children 是将要会渲染的页面
  128. // IntlProvider 始终存在,使用默认值避免 useIntl 报错
  129. return (
  130. <ConfigProvider theme={themeWithAlgorithm}>
  131. <IntlProvider
  132. locale={currentLocale ? currentLocale.split('_')[0] : 'en'} // en_US -> en,提供默认值
  133. messages={(messages as Record<string, string>) || {}} // 提供空对象作为默认值
  134. >
  135. <style>
  136. {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
  137. {`:root {
  138. --color-primary: ${currentTheme.token.colorPrimary};
  139. --color-bg-layout: ${currentTheme.token.colorBgLayout};
  140. --color-text: ${currentTheme.token.colorText};
  141. --button-bg-hover: ${currentTheme.token.buttonBgHover};
  142. }`}
  143. </style>
  144. {/* 加载状态覆盖层 */}
  145. {(loading || (!isI18nReady && !connectionChecked)) && (
  146. <div
  147. style={{
  148. position: 'fixed',
  149. top: 0,
  150. left: 0,
  151. right: 0,
  152. bottom: 0,
  153. zIndex: 9999,
  154. display: 'flex',
  155. justifyContent: 'center',
  156. alignItems: 'center',
  157. backgroundColor: currentTheme.token.colorBgLayout,
  158. color: currentTheme.token.colorText,
  159. }}
  160. >
  161. <div>加载多语言资源中...</div>
  162. </div>
  163. )}
  164. {/* 服务器配置对话框 */}
  165. <ServerConfigModal
  166. open={showConfigModal}
  167. onSave={handleConfigSaved}
  168. onCancel={() => setShowConfigModal(false)}
  169. />
  170. {/* 错误状态覆盖层 */}
  171. {error && (
  172. <div
  173. style={{
  174. position: 'fixed',
  175. top: 0,
  176. left: 0,
  177. right: 0,
  178. bottom: 0,
  179. zIndex: 9999,
  180. display: 'flex',
  181. justifyContent: 'center',
  182. alignItems: 'center',
  183. flexDirection: 'column',
  184. backgroundColor: currentTheme.token.colorBgLayout,
  185. color: currentTheme.token.colorText,
  186. }}
  187. >
  188. <div>多语言资源加载失败: {error}</div>
  189. <button
  190. onClick={() => window.location.reload()}
  191. style={{ marginTop: '16px', padding: '8px 16px' }}
  192. >
  193. 重新加载
  194. </button>
  195. </div>
  196. )}
  197. {/* children 始终被渲染,满足 Taro 框架要求 */}
  198. <div
  199. style={{
  200. backgroundColor: currentTheme.token.colorBgLayout,
  201. color: currentTheme.token.colorText,
  202. borderRadius: '8px',
  203. boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
  204. }}
  205. >
  206. <AcquisitionTracer />
  207. <QuotaAlertModal />
  208. <FeatureNotAvailableFeedback
  209. open={isFeatureNotAvailableOpen}
  210. onClose={() => dispatch(setFeatureNotAvailableOpen(false))}
  211. onContinue={() => {
  212. dispatch(setFeatureNotAvailableOpen(false));
  213. dispatch(setBusinessFlow('continueAfterFeatureNotAvailable'));
  214. }}
  215. />
  216. {children}
  217. {process.env.NODE_ENV === 'development' && <ProductSelector />}
  218. <ThemeSwitcher />
  219. </div>
  220. </IntlProvider>
  221. </ConfigProvider>
  222. );
  223. }
  224. function App({ children }: { children: ReactNode }): JSX.Element {
  225. // 只在 Cypress 测试环境下暴露 store 到 window 对象
  226. if (
  227. typeof window !== 'undefined' &&
  228. (window as unknown as { Cypress: unknown }).Cypress
  229. ) {
  230. (window as unknown as { store: typeof store }).store = store;
  231. }
  232. return (
  233. <Provider store={store}>
  234. <AppContent>{children}</AppContent>
  235. </Provider>
  236. );
  237. }
  238. export default App;