Pārlūkot izejas kodu

feat: 实现远程多语言资源加载功能

- 新增 src/API/i18n/i18nActions.ts: 创建多语言资源API接口,支持从后端获取JSON格式翻译文件
- 新增 src/states/i18nSlice.ts: 实现多语言状态管理,包含加载状态、错误处理和异步thunk
- 修改 src/states/store.ts: 集成i18n reducer到Redux store配置
- 修改 src/app.tsx: 重构应用入口,支持远程多语言资源加载,添加加载状态和错误处理UI

替换本地静态多语言文件为完全远程化方案,支持动态更新翻译内容
dengdx 2 nedēļas atpakaļ
vecāks
revīzija
d4006f4ad5
4 mainītis faili ar 204 papildinājumiem un 59 dzēšanām
  1. 12 0
      src/API/i18n/i18nActions.ts
  2. 131 59
      src/app.tsx
  3. 59 0
      src/states/i18nSlice.ts
  4. 2 0
      src/states/store.ts

+ 12 - 0
src/API/i18n/i18nActions.ts

@@ -0,0 +1,12 @@
+import axiosInstance from '../interceptor';
+
+export interface I18nMessages {
+  [key: string]: string | I18nMessages;
+}
+
+export async function fetchI18nMessages(locale: string): Promise<I18nMessages> {
+  const response = await axiosInstance.get(`/pub/trans/${locale}/${locale}.js`);
+
+  // 返回的是标准JSON格式,直接使用
+  return response.data;
+}

+ 131 - 59
src/app.tsx

@@ -1,25 +1,20 @@
-import { PropsWithChildren, useState, useEffect } from 'react';
+import { useState, useEffect, ReactNode } from 'react';
 import { useLaunch } from '@tarojs/taro';
 import { IntlProvider } from 'react-intl';
 import { ConfigProvider, Button } from 'antd';
 import { Provider } from 'react-redux';
-import store from './states/store';
+import store, { useAppDispatch, useAppSelector } from './states/store';
 import { initializeProductState } from './states/productSlice';
+import { loadI18nMessages } from './states/i18nSlice';
 import './app.css';
 import { lightTheme, darkTheme } from './themes';
 import ProductSelector from './components/ProductSelector';
 import QuotaAlertModal from './pages/security/QuotaAlertModal';
-
-const locale = (window.navigator.language || 'en').toLowerCase().split('-')[0]; // Get locale from browser or default to 'en'
-import messages_en from './assets/i18n/messages/en';
-import messages_zh from './assets/i18n/messages/zh';
 import AcquisitionTracer from './pages/exam/components/acquisitionTracer';
 import { logger } from './log/logger';
 console.log = logger.log;
 console.warn = logger.warn;
 console.error = logger.error;
-
-const messages = locale === 'zh' ? messages_zh : messages_en;
 console.log(`process.env.USE_MSW: ${process.env.USE_MSW}`);
 console.log(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);
 
@@ -36,78 +31,155 @@ if (process.env.NODE_ENV === 'development' && process.env.USE_MSW === 'true') {
     });
 }
 
-function App({ children }: PropsWithChildren<React.ReactNode>) {
+function AppContent({ children }: { children: ReactNode }) {
+  const dispatch = useAppDispatch();
+  const { messages, loading, error, currentLocale } = useAppSelector(
+    (state) => state.i18n
+  );
+  const [currentTheme, setCurrentTheme] = useState(lightTheme); // 默认使用 light 主题
+  const [isI18nReady, setIsI18nReady] = useState(false);
+
   useLaunch(() => {
     console.log('App launched.');
   });
 
-  const [currentTheme, setCurrentTheme] = useState(lightTheme); // 默认使用 light 主题
   const changeTheme = (themeConfig: typeof lightTheme) => {
     setCurrentTheme(themeConfig);
   };
 
+  // 检测浏览器语言
+  const browserLocale = (window.navigator.language || 'en')
+    .toLowerCase()
+    .split('-')[0];
+
   useEffect(() => {
     store.dispatch(initializeProductState());
-  }, []);
+
+    // 应用启动时加载多语言资源
+    const localeToLoad = ['zh', 'en'].includes(browserLocale)
+      ? browserLocale
+      : 'en';
+    dispatch(loadI18nMessages(localeToLoad))
+      .unwrap()
+      .then(() => {
+        setIsI18nReady(true);
+      })
+      .catch((error) => {
+        console.error('加载多语言资源失败:', error);
+        // 设置默认状态,让应用继续运行
+        setIsI18nReady(true);
+      });
+  }, [dispatch, browserLocale]);
+
+  // 显示加载状态
+  if (loading || !isI18nReady) {
+    return (
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+          height: '100vh',
+          backgroundColor: lightTheme.token.colorBgLayout,
+          color: lightTheme.token.colorText,
+        }}
+      >
+        <div>加载多语言资源中...</div>
+      </div>
+    );
+  }
+
+  // 显示错误状态
+  if (error) {
+    return (
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+          height: '100vh',
+          flexDirection: 'column',
+          backgroundColor: lightTheme.token.colorBgLayout,
+          color: lightTheme.token.colorText,
+        }}
+      >
+        <div>多语言资源加载失败: {error}</div>
+        <button
+          onClick={() => window.location.reload()}
+          style={{ marginTop: '16px', padding: '8px 16px' }}
+        >
+          重新加载
+        </button>
+      </div>
+    );
+  }
 
   // children 是将要会渲染的页面
-  // return children
   return (
-    <Provider store={store}>
-      <ConfigProvider theme={currentTheme}>
-        <IntlProvider locale={locale} messages={messages}>
-          <style>
-            {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
-            {`:root {
+    <ConfigProvider theme={currentTheme}>
+      <IntlProvider
+        locale={currentLocale}
+        messages={messages as Record<string, string>}
+      >
+        <style>
+          {/*把theme中的colorPrimary转换成变量--color-primary,变量被tailwindcss使用*/}
+          {`:root {
             --color-primary: ${currentTheme.token.colorPrimary};
           }`}
-          </style>
+        </style>
+        <div
+          style={{
+            backgroundColor: currentTheme.token.colorBgLayout,
+            color: currentTheme.token.colorText,
+            borderRadius: '8px',
+            boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
+          }}
+        >
+          <AcquisitionTracer />
+          <QuotaAlertModal />
+          {children}
+          {process.env.NODE_ENV === 'development' && <ProductSelector />}
           <div
             style={{
-              backgroundColor: currentTheme.token.colorBgLayout,
-              color: currentTheme.token.colorText,
-              borderRadius: '8px',
-              boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
+              position: 'fixed',
+              top: '50%',
+              right: 16,
+              transform: 'translateY(-50%)',
+              display: 'flex',
+              flexDirection: 'column',
+              gap: 8, // 按钮间距
+              zIndex: 1000,
             }}
           >
-            <AcquisitionTracer />
-            <QuotaAlertModal />
-            {children}
-            {process.env.NODE_ENV === 'development' && <ProductSelector />}
-            <div
-              style={{
-                position: 'fixed',
-                top: '50%',
-                right: 16,
-                transform: 'translateY(-50%)',
-                display: 'flex',
-                flexDirection: 'column',
-                gap: 8, // 按钮间距
-                zIndex: 1000,
-              }}
+            <Button
+              type="primary"
+              shape="circle"
+              size="large"
+              onClick={() => changeTheme(darkTheme)}
+              title="Switch to Dark Theme"
             >
-              <Button
-                type="primary"
-                shape="circle"
-                size="large"
-                onClick={() => changeTheme(darkTheme)}
-                title="Switch to Dark Theme"
-              >
-                🌙
-              </Button>
-              <Button
-                type="primary"
-                shape="circle"
-                size="large"
-                onClick={() => changeTheme(lightTheme)}
-                title="Switch to Light Theme"
-              >
-                ☀️
-              </Button>
-            </div>
+              🌙
+            </Button>
+            <Button
+              type="primary"
+              shape="circle"
+              size="large"
+              onClick={() => changeTheme(lightTheme)}
+              title="Switch to Light Theme"
+            >
+              ☀️
+            </Button>
           </div>
-        </IntlProvider>
-      </ConfigProvider>
+        </div>
+      </IntlProvider>
+    </ConfigProvider>
+  );
+}
+
+function App({ children }: { children: ReactNode }) {
+  return (
+    <Provider store={store}>
+      <AppContent>{children}</AppContent>
     </Provider>
   );
 }

+ 59 - 0
src/states/i18nSlice.ts

@@ -0,0 +1,59 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { fetchI18nMessages, I18nMessages } from '../API/i18n/i18nActions';
+
+export interface I18nState {
+  messages: I18nMessages;
+  currentLocale: string;
+  supportedLocales: string[];
+  loading: boolean;
+  error: string | null;
+}
+
+const initialState: I18nState = {
+  messages: {},
+  currentLocale: 'en',
+  supportedLocales: ['en', 'zh'],
+  loading: false,
+  error: null,
+};
+
+// 异步thunk:加载多语言资源
+export const loadI18nMessages = createAsyncThunk(
+  'i18n/loadMessages',
+  async (locale: string) => {
+    const messages = await fetchI18nMessages(locale);
+    return { locale, messages };
+  }
+);
+
+const i18nSlice = createSlice({
+  name: 'i18n',
+  initialState,
+  reducers: {
+    setCurrentLocale: (state, action: PayloadAction<string>) => {
+      state.currentLocale = action.payload;
+    },
+    clearError: (state) => {
+      state.error = null;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      .addCase(loadI18nMessages.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      .addCase(loadI18nMessages.fulfilled, (state, action) => {
+        state.loading = false;
+        state.messages = action.payload.messages;
+        state.currentLocale = action.payload.locale;
+      })
+      .addCase(loadI18nMessages.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.error.message || 'Failed to load i18n messages';
+      });
+  },
+});
+
+export const { setCurrentLocale, clearError } = i18nSlice.actions;
+export default i18nSlice.reducer;

+ 2 - 0
src/states/store.ts

@@ -55,6 +55,7 @@ import templateReducer from './patient/DiagnosticReport/templateSlice';
 import imageSelectionReducer from './patient/DiagnosticReport/imageSelectionSlice';
 import diagnosticReportReducer from './patient/DiagnosticReport/slice';
 import permissionReducer from './permissionSlice';
+import i18nReducer from './i18nSlice';
 
 const store = configureStore({
   reducer: {
@@ -102,6 +103,7 @@ const store = configureStore({
     imageSelection: imageSelectionReducer,
     diagnosticReport: diagnosticReportReducer,
     permission: permissionReducer,
+    i18n: i18nReducer,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(