diff --git a/app/layout.tsx b/app/layout.tsx index 4622646..7bbe426 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -31,10 +31,13 @@ async function getSystemSettings() { export async function generateMetadata(): Promise { const settings = await getSystemSettings() + const siteTitle = !settings?.siteName || settings.siteName === '由壹点3D素材' + ? '由壹点·品牌中心' + : settings.siteName return { - title: settings?.siteName || '由壹点3D素材', - description: '3D素材分享平台', + title: siteTitle, + description: '品牌中心部门看板与创意资产协作平台', icons: { icon: '/api/favicon', shortcut: '/api/favicon', diff --git a/components/UpdateLogModal.test.tsx b/components/UpdateLogModal.test.tsx new file mode 100644 index 0000000..7aa1afb --- /dev/null +++ b/components/UpdateLogModal.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import UpdateLogModal from '@/components/UpdateLogModal' + +const updateLog = { + version: '2026.08.15', + date: '2026-08-15', + title: '系统更新', + items: [ + { + type: 'feature' as const, + title: '品牌中心看板上线', + description: '看板首页已经切换到全新的视觉标准。', + }, + { + type: 'fix' as const, + title: '修复展示问题', + description: '修复部分页面的展示异常。', + }, + ], +} + +describe('UpdateLogModal', () => { + test('使用可访问的弹窗结构展示更新清单', () => { + render() + + expect(screen.getByRole('dialog', { name: '系统更新' })).toHaveAttribute('aria-modal', 'true') + expect(screen.getByText('Version 2026.08.15 · 2026-08-15')).toBeInTheDocument() + expect(screen.getByText('01')).toBeInTheDocument() + expect(screen.getByText('02')).toBeInTheDocument() + expect(screen.getByText('新增')).toBeInTheDocument() + expect(screen.getByText('修复')).toBeInTheDocument() + expect(screen.getByRole('button', { name: '关闭' })).toHaveFocus() + }) + + test('支持 Escape、确认按钮和焦点循环', () => { + const onClose = jest.fn() + const opener = document.createElement('button') + document.body.appendChild(opener) + opener.focus() + + const { unmount } = render() + const closeButton = screen.getByRole('button', { name: '关闭' }) + const confirmButton = screen.getByRole('button', { name: '知道了' }) + + confirmButton.focus() + fireEvent.keyDown(document, { key: 'Tab' }) + expect(closeButton).toHaveFocus() + + fireEvent.keyDown(document, { key: 'Escape' }) + expect(onClose).toHaveBeenCalledTimes(1) + + fireEvent.click(confirmButton) + expect(onClose).toHaveBeenCalledTimes(2) + + unmount() + expect(opener).toHaveFocus() + opener.remove() + }) +}) diff --git a/components/UpdateLogModal.tsx b/components/UpdateLogModal.tsx index 7eeb77b..a82da97 100644 --- a/components/UpdateLogModal.tsx +++ b/components/UpdateLogModal.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' // 更新日志配置 - 每次有新功能或优化时更新这个版本号 const CURRENT_VERSION = '2026.07.04' @@ -1022,17 +1022,47 @@ interface UpdateLogModalProps { } function UpdateLogModal({ isOpen, onClose, updateLog, showAllHistory = false }: UpdateLogModalProps) { + const dialogRef = useRef(null) + const closeButtonRef = useRef(null) + useEffect(() => { - if (isOpen) { - // 阻止背景滚动 - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = 'unset' + if (!isOpen) return + + const previousOverflow = document.body.style.overflow + const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose() + return + } + + if (event.key !== 'Tab' || !dialogRef.current) return + + const focusableElements = Array.from( + dialogRef.current.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'), + ) + const firstElement = focusableElements[0] + const lastElement = focusableElements[focusableElements.length - 1] + + if (event.shiftKey && document.activeElement === firstElement) { + event.preventDefault() + lastElement?.focus() + } else if (!event.shiftKey && document.activeElement === lastElement) { + event.preventDefault() + firstElement?.focus() + } } + + document.body.style.overflow = 'hidden' + document.addEventListener('keydown', handleKeyDown) + closeButtonRef.current?.focus() + return () => { - document.body.style.overflow = 'unset' + document.body.style.overflow = previousOverflow + document.removeEventListener('keydown', handleKeyDown) + previousFocus?.focus() } - }, [isOpen]) + }, [isOpen, onClose]) if (!isOpen) return null @@ -1042,122 +1072,129 @@ function UpdateLogModal({ isOpen, onClose, updateLog, showAllHistory = false }: // 如果没有可显示的内容,不渲染弹窗 if (logsToShow.length === 0) return null - const getTypeIcon = (type: string) => { + const getTypeLabel = (type: UpdateItem['type']) => { switch (type) { case 'feature': - return '✨' + return '新增' case 'improvement': - return '🚀' + return '优化' case 'fix': - return '🔧' - default: - return '📝' + return '修复' } } - const getTypeColor = (type: string) => { + const getTypeDotColor = (type: UpdateItem['type']) => { switch (type) { case 'feature': - return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800' + return 'bg-[#6e79c8]' case 'improvement': - return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' + return 'bg-[#157a4b]' case 'fix': - return 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800' - default: - return 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700' + return 'bg-[#1a1c1d]' } } return ( -
+
{/* 背景遮罩 */} -