app.tsx 9.0 KB

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