diff --git a/src/giffgaff/js/giffgaff-app.js b/src/giffgaff/js/giffgaff-app.js index 2ff64f1..72d4854 100644 --- a/src/giffgaff/js/giffgaff-app.js +++ b/src/giffgaff/js/giffgaff-app.js @@ -20,6 +20,7 @@ import { downloadQRCode } from './modules/utils.js'; import { t, tl } from '../../js/modules/i18n.js'; +import { isMobileBrowser, showMobileWarning } from '../../js/modules/browser-utils.js'; import captchaManager from '../../js/modules/captcha-manager.js'; import Logger from '../../js/modules/logger.js'; @@ -428,6 +429,11 @@ class GiffgaffApp { const { elements } = uiController; console.log('[Giffgaff] === OAuth 登录开始 ==='); + // 手机环境检测:非PC环境弹出提醒 + if (isMobileBrowser()) { + showMobileWarning(); + } + // 检查服务时间 if (!isServiceTimeAvailable()) { console.log('[Giffgaff] 服务时间外,显示警告'); diff --git a/src/js/modules/browser-utils.js b/src/js/modules/browser-utils.js new file mode 100644 index 0000000..bfa48ae --- /dev/null +++ b/src/js/modules/browser-utils.js @@ -0,0 +1,32 @@ +/** + * 浏览器环境检测与提示工具 + * 提供手机浏览器检测和环境警告功能,支持会话级去重 + */ +import { tl } from './i18n.js'; +import NotificationManager from './notification-manager.js'; + +const DEDUP_KEY = 'esim-mobile-warning-shown'; + +/** + * 检测当前是否为手机浏览器 + * @returns {boolean} 是否为手机浏览器 + */ +export function isMobileBrowser() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +/** + * 显示手机环境警告通知 + * 每个会话仅展示一次,避免重复点击造成通知堆叠 + */ +export function showMobileWarning() { + try { + if (sessionStorage.getItem(DEDUP_KEY)) return; + sessionStorage.setItem(DEDUP_KEY, '1'); + } catch (_) {} + + NotificationManager.warning( + tl('当前为手机浏览器环境,部分功能可能受限。如遇错误请切换到 PC 环境操作。'), + 7000 + ); +} diff --git a/src/js/modules/i18n-data.js b/src/js/modules/i18n-data.js index 73b02e7..2b39122 100644 --- a/src/js/modules/i18n-data.js +++ b/src/js/modules/i18n-data.js @@ -689,6 +689,7 @@ export const TRANSLATIONS = { }; export const LITERAL_TRANSLATIONS = { + '当前为手机浏览器环境,部分功能可能受限。如遇错误请切换到 PC 环境操作。': 'You are using a mobile browser. Some features may be limited. If you encounter errors, please switch to a PC.', '点击返回此步骤': 'Click to revisit this step', '请按顺序完成前序步骤': 'Complete earlier steps in order', '已选择OAuth 2.0登录方式': 'OAuth 2.0 login selected', diff --git a/src/simyo/js/simyo-app.js b/src/simyo/js/simyo-app.js index 5888e19..4f6fe6c 100644 --- a/src/simyo/js/simyo-app.js +++ b/src/simyo/js/simyo-app.js @@ -15,6 +15,7 @@ import { delay } from './modules/utils.js'; import { t, tl } from '../../js/modules/i18n.js'; +import { isMobileBrowser, showMobileWarning } from '../../js/modules/browser-utils.js'; import Logger from '../../js/modules/logger.js'; class SimyoApp { @@ -187,6 +188,11 @@ class SimyoApp { const loginStatus = document.getElementById('loginStatus'); console.log('[Simyo] === 登录开始 ==='); + // 手机环境检测:非PC环境弹出提醒 + if (isMobileBrowser()) { + showMobileWarning(); + } + if (!phoneNumberEl || !passwordEl) { console.error('[Simyo] 登录表单元素未找到!'); return; @@ -235,6 +241,11 @@ class SimyoApp { const statusEl = document.getElementById('applyNewEsimStatus'); console.log('[Simyo] === 请求设备更换 ==='); + // 手机环境检测:非PC环境弹出提醒 + if (isMobileBrowser()) { + showMobileWarning(); + } + try { applyBtn.innerHTML = ` ${tl('处理中...')}`; applyBtn.disabled = true; diff --git a/tests/simyo/simyo-app-device-change.test.js b/tests/simyo/simyo-app-device-change.test.js index ae51c9b..4497eae 100644 --- a/tests/simyo/simyo-app-device-change.test.js +++ b/tests/simyo/simyo-app-device-change.test.js @@ -8,6 +8,9 @@ describe('SimyoApp 设备更换主流程自动化', () => { let mockSkipDeviceChange; let mockApplyNewEsim; let mockVerifyCode; + let mockIsMobileBrowser; + let mockShowMobileWarning; + let mockAuthLogin; beforeEach(async () => { jest.resetModules(); @@ -18,6 +21,8 @@ describe('SimyoApp 设备更换主流程自动化', () => {
+ +
@@ -76,13 +81,16 @@ describe('SimyoApp 设备更换主流程自动化', () => { } })); - jest.doMock('../../src/simyo/js/modules/auth-handler.js', () => ({ - authHandler: { - login: jest.fn(), - isLoggedIn: jest.fn(() => true), - getSessionToken: jest.fn(() => 'token') - } - })); + jest.doMock('../../src/simyo/js/modules/auth-handler.js', () => { + mockAuthLogin = jest.fn(); + return { + authHandler: { + login: mockAuthLogin, + isLoggedIn: jest.fn(() => true), + getSessionToken: jest.fn(() => 'token') + } + }; + }); jest.doMock('../../src/simyo/js/modules/esim-service.js', () => ({ esimService: { @@ -111,6 +119,15 @@ describe('SimyoApp 设备更换主流程自动化', () => { delay: jest.fn(() => Promise.resolve()) })); + jest.doMock('../../src/js/modules/browser-utils.js', () => { + mockIsMobileBrowser = jest.fn(() => false); + mockShowMobileWarning = jest.fn(); + return { + isMobileBrowser: mockIsMobileBrowser, + showMobileWarning: mockShowMobileWarning + }; + }); + jest.doMock('../../src/js/modules/i18n.js', () => ({ t: (key, params = {}) => { if (!params || Object.keys(params).length === 0) return key; @@ -161,5 +178,36 @@ describe('SimyoApp 设备更换主流程自动化', () => { expect(mockVerifyCode).toHaveBeenCalledWith('123456'); expect(mockSkipDeviceChange).toHaveBeenCalledTimes(1); }); + + it('手机浏览器登录时应弹出环境警告', async () => { + mockIsMobileBrowser.mockReturnValue(true); + document.getElementById('phoneNumber').value = '0612345678'; + document.getElementById('password').value = 'pass'; + mockAuthLogin.mockResolvedValue({ success: true, mode: 'standard' }); + + await app.handleLogin(); + + expect(mockShowMobileWarning).toHaveBeenCalledTimes(1); + }); + + it('手机浏览器设备更换时应弹出环境警告', async () => { + mockIsMobileBrowser.mockReturnValue(true); + mockApplyNewEsim.mockResolvedValueOnce({ success: true, message: 'ok' }); + + await app.handleApplyNewEsim(); + + expect(mockShowMobileWarning).toHaveBeenCalledTimes(1); + }); + + it('PC浏览器登录时不应弹出环境警告', async () => { + mockIsMobileBrowser.mockReturnValue(false); + document.getElementById('phoneNumber').value = '0612345678'; + document.getElementById('password').value = 'pass'; + mockAuthLogin.mockResolvedValue({ success: true, mode: 'standard' }); + + await app.handleLogin(); + + expect(mockShowMobileWarning).not.toHaveBeenCalled(); + }); });