app.tsx 11 KB

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