diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9338b..4c6d38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新记录 +## 未发布 + +- 修复管理后台手机端成功/错误提示遮挡顶部导航的问题:窄屏提示改为底部显示,桌面保留右上角,并支持手动关闭。新增提示显示期间的窄屏导航回归测试,覆盖 320/390px 与 100%/150% 字号。 + ## v0.3.4 — 2026-11-04 - 文字通知默认不朗读,只在大屏上显示;朗读次数可选 0–3 遍。原声通知播放 1–3 遍。大屏需升级到本版本,旧版大屏收到 0 遍时仍会朗读一遍。 diff --git a/tests/e2e/admin-layout.spec.ts b/tests/e2e/admin-layout.spec.ts index 6bd7e2d..0dbb8fa 100644 --- a/tests/e2e/admin-layout.spec.ts +++ b/tests/e2e/admin-layout.spec.ts @@ -10,12 +10,19 @@ const administrator = { status: 'active', }; -async function mockAdmin(page: Page, siteName = settings.site_name, icpNumber = settings.icp_number) { +async function mockAdmin( + page: Page, + siteName = settings.site_name, + icpNumber = settings.icp_number, + uiScale = 100, +) { const siteSettings = { ...settings, site_name: siteName, icp_number: icpNumber }; const responses: Record = { '/api/v1/config': siteSettings, - '/api/v1/auth/refresh': { access_token: 'test-token', user: administrator }, + '/api/v1/auth/refresh': { access_token: 'test-token', user: { ...administrator, ui_scale: uiScale } }, '/api/v1/admin/settings': siteSettings, + '/api/v1/admin/schools': [], + '/api/v1/admin/classes': [], '/api/v1/admin/users': [ { id: 'teacher-1', @@ -49,6 +56,38 @@ async function mockAdmin(page: Page, siteName = settings.site_name, icpNumber = }); } +test('管理员成功提示显示时窄屏导航仍可点击', async ({ page }) => { + for (const width of [320, 390]) { + for (const scale of [100, 150]) { + await mockAdmin(page, settings.site_name, settings.icp_number, scale); + await page.route('**/api/v1/admin/schools', (route) => + route.fulfill({ + json: route.request().method() === 'POST' ? { id: 'school-toast', name: '提示验收学校' } : [], + }), + ); + await page.setViewportSize({ width, height: 844 }); + await page.goto(adminURL + 'schools'); + await page.getByRole('button', { name: '新建学校' }).click(); + const dialog = page.getByRole('dialog'); + await dialog.getByLabel('学校名称').fill('提示验收学校'); + await dialog.getByRole('button', { name: '创建学校' }).click(); + await expect(dialog).toBeHidden(); + const toast = page.locator('[data-sonner-toast]').filter({ hasText: '学校已创建' }); + await expect(toast).toBeVisible(); + // Hold the real toast open instead of waiting for it to expire or forcing the click. + await toast.hover(); + const navigation = page.getByRole('navigation', { name: '管理后台导航' }); + const toastBox = await toast.boundingBox(); + const navigationBox = await navigation.boundingBox(); + expect(toastBox!.y).toBeGreaterThan(navigationBox!.y + navigationBox!.height); + await navigation.getByRole('link', { name: '班级管理' }).click({ timeout: 2000 }); + await expect(page.getByRole('heading', { name: '班级管理', exact: true })).toBeVisible(); + await expect(toast).toBeVisible(); + await noOverflow(page); + } + } +}); + async function noOverflow(page: Page) { expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); } diff --git a/web/apps/admin/src/main.tsx b/web/apps/admin/src/main.tsx index 42c6b09..d204876 100644 --- a/web/apps/admin/src/main.tsx +++ b/web/apps/admin/src/main.tsx @@ -5,6 +5,7 @@ import { setAuthScope } from '@classding/api'; import { SiteSettingsProvider } from '@classding/ui/components/classding'; import { Toaster } from '@classding/ui/components/ui/sonner'; import { TooltipProvider } from '@classding/ui/components/ui/tooltip'; +import { useIsMobile } from '@classding/ui/hooks/use-mobile'; import '@classding/ui/globals.css'; import App from './App'; @@ -13,13 +14,19 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 15_000 } }, }); +function AdminToaster() { + const isMobile = useIsMobile(); + // Keep success/error feedback clear of the narrow-screen header and navigation. + return ; +} + createRoot(document.getElementById('root')!).render( - +