Kaynağa Gözat

feat: 实现登录页面多语言化并添加Cypress测试

- 多语言化 Login.tsx,使用 useIntl 和 FormattedMessage
- 添加登录页面的中英文翻译映射到语言文件
- 更新 i18n mock handlers 以支持 zh_CN/en_US 格式
- 扩展 LoginPage POM 添加多语言验证方法
- 创建完整的登录页面多语言显示测试套件

文件变更:
- src/pages/security/Login.tsx
- src/assets/i18n/messages/zh.js
- src/assets/i18n/messages/en.js
- cypress/support/mock/handlers/i18n.ts
- cypress/support/pageObjects/LoginPage.ts
- cypress/e2e/security/login-i18n.cy.ts (新增)
dengdx 6 gün önce
ebeveyn
işleme
cde2d936bd

+ 393 - 0
cypress/e2e/security/login-i18n.cy.ts

@@ -0,0 +1,393 @@
+import { mockI18nSuccess, mockAllRequiredAPIs } from '../../support/mock/handlers/i18n';
+import { mockLoginSuccess, mockLoginFail } from '../../support/mock/handlers/user';
+import LoginPage from '../../support/pageObjects/LoginPage';
+
+describe('Login Page - I18n Display Test', () => {
+  const loginPage = new LoginPage();
+
+  beforeEach(() => {
+    cy.clearAllSessionStorage();
+    cy.clearAllLocalStorage();
+    mockAllRequiredAPIs();
+  });
+
+  describe('中文显示测试', () => {
+    it('应该正确显示中文登录页面', () => {
+      // Mock 中文多语言资源
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      // 设置浏览器语言为中文
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      // 访问登录页面
+      loginPage.visit();
+
+      // 等待多语言资源加载完成
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 验证所有文本元素显示为中文
+      loginPage.verifyLanguage('zh');
+
+      // 验证输入框占位符为中文
+      loginPage.verifyPlaceholders('zh');
+
+      // 验证页面上所有中文文本存在
+      cy.get('body').should('contain', '用户名');
+      cy.get('body').should('contain', '密码');
+      cy.get('body').should('contain', '登录');
+      cy.get('body').should('contain', '急诊');
+    });
+
+    it('应该显示中文的表单验证消息', () => {
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 不输入任何内容,直接点击登录按钮
+      loginPage.getLoginButton().click();
+
+      // 验证中文验证消息显示
+      cy.contains('请输入用户名').should('be.visible');
+    });
+
+    it('应该显示中文的登录成功消息', () => {
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+      mockLoginSuccess();
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 执行登录
+      loginPage.login('admin', '123456');
+      cy.wait('@loginSuccess');
+
+      // 验证中文成功消息
+      cy.contains('登录成功').should('be.visible', { timeout: 10000 });
+    });
+
+    it('应该显示中文的登录失败消息', () => {
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+      mockLoginFail();
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 执行登录
+      loginPage.login('wronguser', 'wrongpassword');
+      cy.wait('@loginFail');
+
+      // 验证中文失败消息
+      cy.contains('登录失败').should('be.visible', { timeout: 10000 });
+    });
+  });
+
+  describe('英文显示测试', () => {
+    it('应该正确显示英文登录页面', () => {
+      // Mock 英文多语言资源
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      // 设置浏览器语言为英文
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      // 访问登录页面
+      loginPage.visit();
+
+      // 等待多语言资源加载完成
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 验证所有文本元素显示为英文
+      loginPage.verifyLanguage('en');
+
+      // 验证输入框占位符为英文
+      loginPage.verifyPlaceholders('en');
+
+      // 验证页面上所有英文文本存在
+      cy.get('body').should('contain', 'Username');
+      cy.get('body').should('contain', 'Password');
+      cy.get('body').should('contain', 'Login');
+      cy.get('body').should('contain', 'Emergency');
+    });
+
+    it('应该显示英文的表单验证消息', () => {
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 不输入任何内容,直接点击登录按钮
+      loginPage.getLoginButton().click();
+
+      // 验证英文验证消息显示
+      cy.contains('Please enter username').should('be.visible');
+    });
+
+    it('应该显示英文的登录成功消息', () => {
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+      mockLoginSuccess();
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 执行登录
+      loginPage.login('admin', '123456');
+      cy.wait('@loginSuccess');
+
+      // 验证英文成功消息
+      cy.contains('Login successful').should('be.visible', { timeout: 10000 });
+    });
+
+    it('应该显示英文的登录失败消息', () => {
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+      mockLoginFail();
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 执行登录
+      loginPage.login('wronguser', 'wrongpassword');
+      cy.wait('@loginFail');
+
+      // 验证英文失败消息
+      cy.contains('Login failed').should('be.visible', { timeout: 10000 });
+    });
+  });
+
+  describe('语言切换测试', () => {
+    it('应该能从中文切换到英文', () => {
+      // 首先加载中文
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 验证中文显示
+      loginPage.verifyLanguage('zh');
+
+      // 切换到英文
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 验证英文显示
+      loginPage.verifyLanguage('en');
+    });
+
+    it('应该能从英文切换到中文', () => {
+      // 首先加载英文
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 验证英文显示
+      loginPage.verifyLanguage('en');
+
+      // 切换到中文
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 验证中文显示
+      loginPage.verifyLanguage('zh');
+    });
+  });
+
+  describe('Redux 状态验证', () => {
+    it('中文模式下 Redux 状态应该正确', () => {
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 验证 Redux 状态
+      cy.window().its('store').invoke('getState').then((state) => {
+        expect(state.i18n.currentLocale).to.equal('zh_CN');
+        expect(state.i18n.loading).to.be.false;
+        expect(state.i18n.error).to.be.null;
+        expect(state.i18n.messages).to.have.property('login.username', '用户名');
+        expect(state.i18n.messages).to.have.property('login.password', '密码');
+        expect(state.i18n.messages).to.have.property('login.submit', '登录');
+        expect(state.i18n.messages).to.have.property('login.emergency', '急诊');
+      });
+    });
+
+    it('英文模式下 Redux 状态应该正确', () => {
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 验证 Redux 状态
+      cy.window().its('store').invoke('getState').then((state) => {
+        expect(state.i18n.currentLocale).to.equal('en_US');
+        expect(state.i18n.loading).to.be.false;
+        expect(state.i18n.error).to.be.null;
+        expect(state.i18n.messages).to.have.property('login.username', 'Username');
+        expect(state.i18n.messages).to.have.property('login.password', 'Password');
+        expect(state.i18n.messages).to.have.property('login.submit', 'Login');
+        expect(state.i18n.messages).to.have.property('login.emergency', 'Emergency');
+      });
+    });
+  });
+
+  describe('视觉验证(手动查看)', () => {
+    it('[视觉] 中文登录页面完整显示', () => {
+      mockI18nSuccess('zh_CN');
+      mockAllRequiredAPIs('zh_CN');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'zh-CN',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nZH_CNSuccess');
+
+      // 等待页面完全加载
+      loginPage.getUsernameInput().should('be.visible');
+      loginPage.getPasswordInput().should('be.visible');
+      loginPage.getLoginButton().should('be.visible');
+      loginPage.getEmergencyButton().should('be.visible');
+
+      // 添加等待时间供手动查看
+      cy.wait(500);
+    });
+
+    it('[视觉] 英文登录页面完整显示', () => {
+      mockI18nSuccess('en_US');
+      mockAllRequiredAPIs('en_US');
+
+      cy.window().then((win) => {
+        Object.defineProperty(win.navigator, 'language', {
+          value: 'en-US',
+          writable: false
+        });
+      });
+
+      loginPage.visit();
+      cy.wait('@getI18nEN_USSuccess');
+
+      // 等待页面完全加载
+      loginPage.getUsernameInput().should('be.visible');
+      loginPage.getPasswordInput().should('be.visible');
+      loginPage.getLoginButton().should('be.visible');
+      loginPage.getEmergencyButton().should('be.visible');
+
+      // 添加等待时间供手动查看
+      cy.wait(500);
+    });
+  });
+});

+ 86 - 51
cypress/support/mock/handlers/i18n.ts

@@ -78,18 +78,18 @@ export function mockGetLanguageListEmpty() {
  * @url /dr/api/v1/pub/trans/{locale}/{locale}.js
  * @access 公开接口
  * 
- * @param {string} locale - 语言代码(zh/en)
+ * @param {string} locale - 语言代码(zh_CN/en_US
  * 
  * @returns {Object} 翻译键值对
  * 
  * @example
- * mockI18nSuccess('zh');
+ * mockI18nSuccess('en_US');
  * cy.wait('@getI18nZHSuccess');
  * 
  * @see docs/DR.md - 章节3
  */
-export function mockI18nSuccess(locale: 'zh' | 'en') {
-  const mockData = locale === 'zh' ? {
+export function mockI18nSuccess(locale: 'zh_CN' | 'en_US') {
+  const mockData = locale === 'zh_CN' ? {
     greeting: '你好,世界!',
     name: '张三',
     patient: '患者管理',
@@ -115,7 +115,23 @@ export function mockI18nSuccess(locale: 'zh' | 'en') {
     'register.patientName': '患者姓名',
     'register.gender': '性别',
     'register.gender.male': '男',
-    'register.gender.female': '女'
+    'register.gender.female': '女',
+    // Login page
+    'login.username': '用户名',
+    'login.username.placeholder': '请输入用户名',
+    'login.username.required': '请输入用户名',
+    'login.password': '密码',
+    'login.password.placeholder': '请输入密码',
+    'login.password.required': '请输入密码',
+    'login.submit': '登录',
+    'login.emergency': '急诊',
+    'login.success': '登录成功',
+    'login.failed': '登录失败',
+    'login.failedDetail': ',详情:',
+    'login.networkError': ',网络错误:',
+    'login.unknownError': '未知错误',
+    'login.emergency.success': '急诊模式启动成功',
+    'login.emergency.failed': '急诊操作失败:'
   } : {
     greeting: 'Hello, world!',
     name: 'John Doe',
@@ -142,10 +158,26 @@ export function mockI18nSuccess(locale: 'zh' | 'en') {
     'register.patientName': 'Patient Name',
     'register.gender': 'Gender',
     'register.gender.male': 'Male',
-    'register.gender.female': 'Female'
+    'register.gender.female': 'Female',
+    // Login page
+    'login.username': 'Username',
+    'login.username.placeholder': 'Enter username',
+    'login.username.required': 'Please enter username',
+    'login.password': 'Password',
+    'login.password.placeholder': 'Enter password',
+    'login.password.required': 'Please enter password',
+    'login.submit': 'Login',
+    'login.emergency': 'Emergency',
+    'login.success': 'Login successful',
+    'login.failed': 'Login failed',
+    'login.failedDetail': ', details: ',
+    'login.networkError': ', network error: ',
+    'login.unknownError': 'Unknown error',
+    'login.emergency.success': 'Emergency mode activated successfully',
+    'login.emergency.failed': 'Emergency operation failed: '
   };
 
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       statusCode: 200,
       body: mockData
@@ -154,8 +186,8 @@ export function mockI18nSuccess(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源失败的 mock (404错误)
-export function mockI18nError(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nError(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       statusCode: 404,
       body: {
@@ -167,8 +199,8 @@ export function mockI18nError(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源服务器错误的 mock (500错误)
-export function mockI18nServerError(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nServerError(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       statusCode: 500,
       body: {
@@ -180,8 +212,8 @@ export function mockI18nServerError(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源超时的 mock
-export function mockI18nTimeout(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nTimeout(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       delay: 30000, // 30秒延迟,模拟超时
       statusCode: 200,
@@ -191,8 +223,8 @@ export function mockI18nTimeout(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源格式错误的 mock
-export function mockI18nInvalidFormat(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nInvalidFormat(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       statusCode: 200,
       body: "invalid json format" // 返回非JSON格式数据
@@ -201,8 +233,8 @@ export function mockI18nInvalidFormat(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源空数据的 mock
-export function mockI18nEmptyData(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nEmptyData(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       statusCode: 200,
       body: {} // 返回空对象
@@ -211,8 +243,8 @@ export function mockI18nEmptyData(locale: 'zh' | 'en') {
 }
 
 // 封装获取多语言资源网络错误的 mock
-export function mockI18nNetworkError(locale: 'zh' | 'en') {
-  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale}.js`, (req) => {
+export function mockI18nNetworkError(locale: 'zh_CN' | 'en_US') {
+  cy.intercept('GET', `/dr/api/v1/pub/trans/${locale}/${locale.split('_')[0]}.js`, (req) => {
     req.reply({
       forceNetworkError: true
     });
@@ -220,69 +252,72 @@ export function mockI18nNetworkError(locale: 'zh' | 'en') {
 }
 
 // 封装软件信息API的 mock,避免影响页面加载
-export function mockSoftwareInfo() {
+export function mockSoftwareInfo(locale: 'zh_CN' | 'en_US' = 'zh_CN') {
   cy.intercept('GET', '/dr/api/v1/pub/software_info', (req) => {
     req.reply({
       statusCode: 200,
       body: {
         code: "0x000000",
         data: {
-          FPD: "Simulator",
-          GEN: "Simulator",
-          guest: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTY3MjAxMTUsImlkIjoyLCJuYW1lIjoiZ3Vlc3QifQ.cDkkxM2mkiCQf7T87WsCMewITk13c7jSDoniT7gDHXQ",
+          FPD: "Physics",
+          GEN: "Physics",
+          current_locale: locale,
+          default_locale: "zh_CN",
+          guest: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjA0OTc2NDksImlkIjoyLCJuYW1lIjoiZ3Vlc3QifQ.hUV5_GOzlWKDeFioJCsHMG2IXa0oJPfNNlPsr69ypqc",
           language: ["en", "zh"],
-          product: "DROS",
+          product: "VETDROS",
           server: {
             auth: {
-              build: "2025-08-25 17:45:18",
+              build: "2025-10-08 11:06:08",
               desc: "Authentication Server repo",
-              submodule: ["3a167dd4[rpc_idl]"],
-              version: "0.3.0-13-g8b85622"
+              submodule: ["0baa8db0[rpc_idl]"],
+              version: "0.4.0-21-g4a37cc7"
             },
             dcmtk: {
-              build: "2025-08-25 13:43:16",
+              build: "2025-09-29 16:35:39",
               desc: "Dcmtk Server repo",
-              submodule: ["0fc2b1e4[rpc_idl]"],
-              version: "0.3.0-12-gff618d4"
+              submodule: ["c2039235[rpc_idl]"],
+              version: "0.4.0-2-g36bc18f"
             },
             imgProc: {
-              build: "2025-08-25 13:46:23",
+              build: "2025-09-29 16:36:36",
               desc: "Img Proc Server repo",
               submodule: [
-                "5e507af7[auto_wwwl]",
-                "3a75bb1f[collimator_circle]",
-                "e7b69785[collimator_rect]",
-                "6b7fbbd1[enhance]",
+                "d00bfca5[auto_wwwl]",
+                "89fd9218[collimator_circle]",
+                "c9582bbe[collimator_rect]",
+                "98c510bd[enhance]",
+                "6f9bf7de[enhance_v3]",
                 "5905e001[rpc_idl]"
               ],
-              version: "0.3.0-7-gbb2ee0b"
+              version: "0.4.0-7-gca1c8a4"
             },
             protocol: {
-              build: "2025-08-25 17:45:23",
+              build: "2025-10-08 11:06:13",
               desc: "Protocol Server repo",
-              submodule: ["3a167dd4[rpc_idl]"],
-              version: "0.3.0-7-g1954756"
+              submodule: ["0baa8db0[rpc_idl]"],
+              version: "0.4.0-8-g47990eb"
             },
             resource: {
-              build: "2025-08-25 17:45:27",
+              build: "2025-10-08 11:06:17",
               desc: "Resource Server repo",
-              submodule: ["0fc2b1e4[rpc_idl]"],
-              version: "0.3.0-12-g60e37c1"
+              submodule: ["9725e654[rpc_idl]"],
+              version: "0.4.0-9-g10766e8"
             },
             study: {
-              build: "2025-08-25 17:45:25",
+              build: "2025-10-08 11:06:15",
               desc: "Study Server repo",
-              submodule: ["3a167dd4[rpc_idl]"],
-              version: "0.3.0-11-g784ba1b"
+              submodule: ["8595ea2d[rpc_idl]"],
+              version: "0.5.0-1-g7620712"
             },
             task: {
-              build: "2025-08-25 17:45:29",
+              build: "2025-10-08 11:06:19",
               desc: "Task Server repo",
-              submodule: ["0fc2b1e4[rpc_idl]"],
-              version: "0.3.0-20-ge9ec04a"
+              submodule: ["8595ea2d[rpc_idl]"],
+              version: "0.4.0-11-g494849d"
             }
           },
-          sn: "2edbc382-044adc78-95bed11b-51c9328a"
+          sn: "19d5d2eb-8b720370-7d617b19-670dd1ae"
         },
         description: "Success",
         solution: ""
@@ -306,7 +341,7 @@ export function mockLogger() {
 }
 
 // 封装所有必要的API mock,避免影响页面加载
-export function mockAllRequiredAPIs() {
-  mockSoftwareInfo();
+export function mockAllRequiredAPIs(locale: 'zh_CN' | 'en_US' = 'zh_CN') {
+  mockSoftwareInfo(locale);
   mockLogger();
 }

+ 46 - 0
cypress/support/pageObjects/LoginPage.ts

@@ -40,6 +40,52 @@ class LoginPage {
     this.getPasswordInput().type(password);
     this.getSubmitButton().click();
   }
+
+  // 获取用户名标签文本
+  getUsernameLabel() {
+    return cy.get('label').contains(/用户名|Username/);
+  }
+
+  // 获取密码标签文本
+  getPasswordLabel() {
+    return cy.get('label').contains(/密码|Password/);
+  }
+
+  // 获取登录按钮
+  getLoginButton() {
+    return cy.get('[data-testid="login-submit-button"]');
+  }
+
+  // 获取急诊按钮
+  getEmergencyButton() {
+    return cy.contains('button', /急诊|Emergency/);
+  }
+
+  // 验证页面是否为指定语言
+  verifyLanguage(locale: 'zh' | 'en') {
+    if (locale === 'zh') {
+      this.getUsernameLabel().should('contain', '用户名');
+      this.getPasswordLabel().should('contain', '密码');
+      this.getLoginButton().should('contain', '登录');
+      this.getEmergencyButton().should('contain', '急诊');
+    } else {
+      this.getUsernameLabel().should('contain', 'Username');
+      this.getPasswordLabel().should('contain', 'Password');
+      this.getLoginButton().should('contain', 'Login');
+      this.getEmergencyButton().should('contain', 'Emergency');
+    }
+  }
+
+  // 验证占位符文本
+  verifyPlaceholders(locale: 'zh' | 'en') {
+    if (locale === 'zh') {
+      this.getUsernameInput().should('have.attr', 'placeholder', '请输入用户名');
+      this.getPasswordInput().should('have.attr', 'placeholder', '请输入密码');
+    } else {
+      this.getUsernameInput().should('have.attr', 'placeholder', 'Enter username');
+      this.getPasswordInput().should('have.attr', 'placeholder', 'Enter password');
+    }
+  }
 }
 
 export default LoginPage;

+ 15 - 0
src/assets/i18n/messages/en.js

@@ -199,4 +199,19 @@ export default {
   'worklistTable.CreationTime': 'Creation Time',
   'worklistTable.MappedStatus': 'Mapped Status',
   'worklistTable.IsDelete': 'Is Deleted',
+  'login.username': 'Username',
+  'login.username.placeholder': 'Enter username',
+  'login.username.required': 'Please enter username',
+  'login.password': 'Password',
+  'login.password.placeholder': 'Enter password',
+  'login.password.required': 'Please enter password',
+  'login.submit': 'Login',
+  'login.emergency': 'Emergency',
+  'login.success': 'Login successful',
+  'login.failed': 'Login failed',
+  'login.failedDetail': ', details: ',
+  'login.networkError': ', network error: ',
+  'login.unknownError': 'Unknown error',
+  'login.emergency.success': 'Emergency mode activated successfully',
+  'login.emergency.failed': 'Emergency operation failed: ',
 };

+ 15 - 0
src/assets/i18n/messages/zh.js

@@ -199,4 +199,19 @@ export default {
   'worklistTable.CreationTime': '创建时间',
   'worklistTable.MappedStatus': '映射状态',
   'worklistTable.IsDelete': '是否已删除',
+  'login.username': '用户名',
+  'login.username.placeholder': '请输入用户名',
+  'login.username.required': '请输入用户名',
+  'login.password': '密码',
+  'login.password.placeholder': '请输入密码',
+  'login.password.required': '请输入密码',
+  'login.submit': '登录',
+  'login.emergency': '急诊',
+  'login.success': '登录成功',
+  'login.failed': '登录失败',
+  'login.failedDetail': ',详情:',
+  'login.networkError': ',网络错误:',
+  'login.unknownError': '未知错误',
+  'login.emergency.success': '急诊模式启动成功',
+  'login.emergency.failed': '急诊操作失败:',
 };

+ 35 - 13
src/pages/security/Login.tsx

@@ -2,6 +2,7 @@ import React, { useRef, useEffect } from 'react';
 import { Row, Col, Form, Input, Button, Card, message, Image } from 'antd';
 import type { InputRef } from 'antd';
 import { useSelector } from 'react-redux';
+import { useIntl, FormattedMessage } from 'react-intl';
 import { RootState } from '../../states/store';
 import { SystemMode } from '../../states/systemModeSlice';
 import loginBrand from 'src/assets/imgs/login-brand.jpeg';
@@ -15,6 +16,7 @@ import handleEmergencyOperation from '../../domain/patient/handleEmergencyOperat
 import { setSystemMode } from '../../states/systemModeSlice';
 
 const Login: React.FC = () => {
+  const intl = useIntl();
   const [form] = Form.useForm();
   const dispatch = useDispatch();
   const systemMode = useSelector((state: RootState) => state.systemMode.mode);
@@ -65,13 +67,15 @@ const Login: React.FC = () => {
         // 5. 分发 redux action
         dispatch(setUserInfo(userInfo));
         dispatch(setSystemMode(SystemMode.Normal));
-        message.success('登录成功'); //todo 更详细的提示与异步过程
+        message.success(intl.formatMessage({ id: 'login.success' }));
       } else {
-        message.error(`登录失败,详情:${result.description}`);
+        message.error(
+          `${intl.formatMessage({ id: 'login.failed' })}${intl.formatMessage({ id: 'login.failedDetail' })}${result.description}`
+        );
       }
     } catch (e) {
       message.error(
-        `登录失败,网络错误: ${e instanceof Error ? e.message : '未知错误'}`
+        `${intl.formatMessage({ id: 'login.failed' })}${intl.formatMessage({ id: 'login.networkError' })} ${e instanceof Error ? e.message : intl.formatMessage({ id: 'login.unknownError' })}`
       );
     }
   };
@@ -83,11 +87,11 @@ const Login: React.FC = () => {
     e.preventDefault(); // 阻止表单提交
     try {
       await handleEmergencyOperation();
-      message.success('急诊模式启动成功');
+      message.success(intl.formatMessage({ id: 'login.emergency.success' }));
     } catch (e) {
       dispatch(setSystemMode(SystemMode.Normal));
       message.error(
-        `急诊操作失败: ${e instanceof Error ? e.message : '未知错误'}`
+        `${intl.formatMessage({ id: 'login.emergency.failed' })} ${e instanceof Error ? e.message : intl.formatMessage({ id: 'login.unknownError' })}`
       );
       console.error('Emergency operation failed:', e);
     }
@@ -115,25 +119,43 @@ const Login: React.FC = () => {
             className="space-y-6 "
           >
             <Form.Item
-              label="用户名"
+              label={<FormattedMessage id="login.username" />}
               name="username"
-              rules={[{ required: true, message: '请输入用户名' }]}
+              rules={[
+                {
+                  required: true,
+                  message: intl.formatMessage({
+                    id: 'login.username.required',
+                  }),
+                },
+              ]}
             >
               <Input
                 ref={usernameInputRef}
                 size="large"
-                placeholder="请输入用户名"
+                placeholder={intl.formatMessage({
+                  id: 'login.username.placeholder',
+                })}
                 data-testid="login-username-input"
               />
             </Form.Item>
             <Form.Item
-              label="密码"
+              label={<FormattedMessage id="login.password" />}
               name="password"
-              rules={[{ required: true, message: '请输入密码' }]}
+              rules={[
+                {
+                  required: true,
+                  message: intl.formatMessage({
+                    id: 'login.password.required',
+                  }),
+                },
+              ]}
             >
               <Input.Password
                 size="large"
-                placeholder="请输入密码"
+                placeholder={intl.formatMessage({
+                  id: 'login.password.placeholder',
+                })}
                 className="py-0 px-[11px]"
                 data-testid="login-password-input"
               />
@@ -148,7 +170,7 @@ const Login: React.FC = () => {
                     className="w-full"
                     data-testid="login-submit-button"
                   >
-                    登录
+                    <FormattedMessage id="login.submit" />
                   </Button>
                 </Col>
                 <Col span={12}>
@@ -158,7 +180,7 @@ const Login: React.FC = () => {
                     className="w-full"
                     onClick={handleEmergencyClick}
                   >
-                    急诊
+                    <FormattedMessage id="login.emergency" />
                   </Button>
                 </Col>
               </Row>