| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- import { useState, useEffect, ReactNode } from 'react';
- import { useLaunch } from '@tarojs/taro';
- import { IntlProvider } from 'react-intl';
- import { ConfigProvider } from 'antd';
- import { Provider } from 'react-redux';
- import store, { useAppDispatch, useAppSelector } from './states/store';
- import { initializeProductState } from './states/productSlice';
- import { loadI18nMessages } from './states/i18nSlice';
- import { checkServerConnection } from './features/serverConfig';
- import { initializeAnnotationManager, cleanupAnnotationManager } from './features/imageAnnotation';
- import { platform } from './utils/platform';
- import './app.css';
- import ProductSelector from './components/ProductSelector';
- import ThemeSwitcher from './components/ThemeSwitcher';
- import QuotaAlertModal from './pages/security/QuotaAlertModal';
- import AcquisitionTracer from './pages/exam/components/acquisitionTracer';
- import FeatureNotAvailableFeedback from './components/FeatureNotAvailableFeedback';
- import { setFeatureNotAvailableOpen } from './states/featureNotAvailableSlice';
- import { setBusinessFlow } from './states/BusinessFlowSlice';
- import { logger } from './log/logger';
- import { theme } from 'antd';
- import ServerConfigModal from './features/serverConfig/components/ServerConfigModal';
- console.log = logger.log;
- console.warn = logger.warn;
- console.error = logger.error;
- console.debug = logger.debug;
- console.log(`process.env.USE_MSW: ${process.env.USE_MSW}`);
- console.log(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);
- console.debug('debug level')
- if (process.env.NODE_ENV === 'development' && process.env.USE_MSW === 'true') {
- import('../mocks/server')
- .then(({ server }): void => {
- server.start({
- onUnhandledRequest: 'error', // 未处理的请求触发网络错误
- });
- console.log(`启动了MSW`);
- })
- .catch((err): void => {
- console.warn('Mock server module not found:', err);
- });
- }
- function AppContent({ children }: { children: ReactNode }): JSX.Element {
- const dispatch = useAppDispatch();
- const { messages, loading, error, currentLocale } = useAppSelector(
- (state) => state.i18n
- );
- const isFeatureNotAvailableOpen = useAppSelector(
- (state) => state.featureNotAvailable.isOpen
- );
- const { currentTheme, themeType } = useAppSelector((state) => state.theme);
- const [isI18nReady, setIsI18nReady] = useState(false);
- const [showConfigModal, setShowConfigModal] = useState(false);
- const [connectionChecked, setConnectionChecked] = useState(false);
- const themeWithAlgorithm = {
- ...currentTheme,
- algorithm:
- themeType === 'light' ? theme.defaultAlgorithm : theme.darkAlgorithm,
- };
- useLaunch((): void => {
- console.log('App launched.');
- });
- useEffect(() => {
- // 浏览器环境不需要服务器连接检查,直接初始化应用
- if (platform.isBrowser) {
- console.log('浏览器环境,跳过服务器连接检查,直接初始化应用');
- initializeApp();
- return;
- }
- // Electron/Cordova 环境:检查服务器连接
- dispatch(checkServerConnection())
- .unwrap()
- .then((result) => {
- setConnectionChecked(true);
- if (result.needsConfig) {
- // 需要配置,显示对话框
- console.log('检测到需要服务器配置,显示配置对话框');
- setShowConfigModal(true);
- } else {
- // 连接正常,继续正常初始化
- console.log('服务器连接正常,开始应用初始化');
- return initializeApp();
- }
- })
- .catch((error) => {
- console.error('连接检查失败:', error);
- setConnectionChecked(true);
- setShowConfigModal(true);
- });
- // 应用退出时清理注释管理器
- return () => {
- cleanupAnnotationManager().catch(console.error);
- };
- }, [dispatch]);
- // 应用正常初始化函数
- const initializeApp = async () => {
- try {
- const productState = await dispatch(initializeProductState()).unwrap();
- console.log(`初始化,拉取到产品信息:${JSON.stringify(productState)}`);
- const languageCode = productState.language;
- await dispatch(loadI18nMessages(languageCode)).unwrap();
- // ✅ 初始化注释管理器(在产品状态和国际化之后)
- await initializeAnnotationManager();
- setIsI18nReady(true);
- } catch (error) {
- console.error('应用初始化失败:', error);
- // 显示配置对话框,让用户重新配置
- setShowConfigModal(true);
- }
- };
- // 配置保存后的处理
- const handleConfigSaved = () => {
- setShowConfigModal(false);
- // 重新检查连接并初始化应用
- dispatch(checkServerConnection())
- .unwrap()
- .then((result) => {
- if (!result.needsConfig) {
- initializeApp();
- }
- })
- .catch((error) => {
- console.error('重新检查连接失败:', error);
- });
- };
- console.log('当前语言:', currentLocale);
- console.log('messages', messages);
- // children 是将要会渲染的页面
- // IntlProvider 始终存在,使用默认值避免 useIntl 报错
- return (
- <ConfigProvider theme={themeWithAlgorithm}>
- <IntlProvider
- locale={currentLocale ? currentLocale.split('_')[0] : 'en'} // en_US -> en,提供默认值
- messages={(messages as Record<string, string>) || {}} // 提供空对象作为默认值
- >
- <style>
- {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
- {`:root {
- --color-primary: ${currentTheme.token.colorPrimary};
- --color-bg-layout: ${currentTheme.token.colorBgLayout};
- --color-text: ${currentTheme.token.colorText};
- --button-bg-hover: ${currentTheme.token.buttonBgHover};
- }`}
- </style>
- {/* 加载状态覆盖层 */}
- {(loading || (!isI18nReady && !connectionChecked)) && (
- <div
- style={{
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- zIndex: 9999,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: currentTheme.token.colorBgLayout,
- color: currentTheme.token.colorText,
- }}
- >
- <div>加载多语言资源中...</div>
- </div>
- )}
- {/* 服务器配置对话框 */}
- <ServerConfigModal
- open={showConfigModal}
- onSave={handleConfigSaved}
- onCancel={() => setShowConfigModal(false)}
- />
- {/* 错误状态覆盖层 */}
- {error && (
- <div
- style={{
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- zIndex: 9999,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- flexDirection: 'column',
- backgroundColor: currentTheme.token.colorBgLayout,
- color: currentTheme.token.colorText,
- }}
- >
- <div>多语言资源加载失败: {error}</div>
- <button
- onClick={() => window.location.reload()}
- style={{ marginTop: '16px', padding: '8px 16px' }}
- >
- 重新加载
- </button>
- </div>
- )}
- {/* children 始终被渲染,满足 Taro 框架要求 */}
- <div
- style={{
- backgroundColor: currentTheme.token.colorBgLayout,
- color: currentTheme.token.colorText,
- borderRadius: '8px',
- boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
- }}
- >
- <AcquisitionTracer />
- <QuotaAlertModal />
- <FeatureNotAvailableFeedback
- open={isFeatureNotAvailableOpen}
- onClose={() => dispatch(setFeatureNotAvailableOpen(false))}
- onContinue={() => {
- dispatch(setFeatureNotAvailableOpen(false));
- dispatch(setBusinessFlow('continueAfterFeatureNotAvailable'));
- }}
- />
- {children}
- {process.env.NODE_ENV === 'development' && <ProductSelector />}
- <ThemeSwitcher />
- </div>
- </IntlProvider>
- </ConfigProvider>
- );
- }
- function App({ children }: { children: ReactNode }): JSX.Element {
- // 只在 Cypress 测试环境下暴露 store 到 window 对象
- if (
- typeof window !== 'undefined' &&
- (window as unknown as { Cypress: unknown }).Cypress
- ) {
- (window as unknown as { store: typeof store }).store = store;
- }
- return (
- <Provider store={store}>
- <AppContent>{children}</AppContent>
- </Provider>
- );
- }
- export default App;
|