refactor(frontend): 报头抽组件 Masthead + 回到顶部抽组件 BackTop

- Masthead:统一 HomePage/StandingsLayout 双份复制实现,当前版面
  印报红高亮 + aria-current;⚙ 字符改线条 SVG;管理类入口加 title
  提示需登录,访客不再被无声踢到登录页
- BackTop:两页各一份的浮动按钮收敛为共享组件,去 rounded-full/
  shadow-lg,与全站方角无阴影语言对齐
This commit is contained in:
WorkBuddy
2026-09-22 16:55:59 +08:00
parent b9beddacf7
commit 9c77efaa4b
3 changed files with 112 additions and 58 deletions
+5 -58
View File
@@ -11,48 +11,17 @@
* - 未登录访问管理 → AdminLayout 门禁 → 登录页(不静默失败)
*/
import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router-dom'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { ErrorBoundary } from './components/ErrorBoundary'
import Masthead from './components/Masthead'
import Matches from './pages/Matches'
import Standings from './pages/Standings'
import { adminRoutes } from './admin/routes'
/** 报眉日期行 */
function dateLine(): string {
return new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
})
}
function StandingsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-paper-50">
<header className="masthead-rule">
<div className="mx-auto max-w-5xl px-5 sm:px-8">
<div className="border-b border-ink-900 py-5 text-center sm:py-6">
<h1 className="font-brush text-5xl text-ink-900 sm:text-6xl">
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink-200 py-2 text-2xs text-ink-500">
<span>{dateLine()}</span>
<nav className="flex items-center gap-4" aria-label="页面导航">
<Link to="/" className="text-ink-500 hover:text-press transition-colors">
/
</Link>
<Link to="/admin/eval" className="text-ink-500 hover:text-press transition-colors">
</Link>
<Link to="/admin" className="flex items-center gap-1 text-ink-500 hover:text-press transition-colors">
<span aria-hidden="true"></span>
</Link>
</nav>
</div>
</div>
</header>
<Masthead active="standings" />
<main className="mx-auto max-w-5xl px-5 py-6 sm:px-8 sm:py-8">
{children}
@@ -70,30 +39,8 @@ function StandingsLayout({ children }: { children: React.ReactNode }) {
function HomePage() {
return (
<div className="min-h-screen bg-paper-50">
{/* ── 报头:粗线 + 居中刊名 + 日期与分区链接 ── */}
<header className="masthead-rule">
<div className="mx-auto max-w-5xl px-5 sm:px-8">
<div className="border-b border-ink-900 py-5 text-center sm:py-6">
<h1 className="font-brush text-5xl text-ink-900 sm:text-6xl">
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink-200 py-2 text-2xs text-ink-500">
<span>{dateLine()}</span>
<nav className="flex items-center gap-4" aria-label="页面导航">
<Link to="/standings" className="text-ink-500 hover:text-press transition-colors">
</Link>
<Link to="/admin/eval" className="text-ink-500 hover:text-press transition-colors">
</Link>
<Link to="/admin" className="flex items-center gap-1 text-ink-500 hover:text-press transition-colors">
<span aria-hidden="true"></span>
</Link>
</nav>
</div>
</div>
</header>
{/* ── 报头:粗线 + 居中刊名 + 日期与分区链接(共用 Masthead) ── */}
<Masthead active="home" />
<main className="mx-auto max-w-5xl px-5 py-6 sm:px-8 sm:py-8">
<Matches />
+30
View File
@@ -0,0 +1,30 @@
/**
* 回到顶部浮动按钮(前台两页共用,此前 Matches/Standings 各复制一份)。
* 方角纸片风:去掉早期版本的 rounded-full + shadow-lg,与全站方角
* 无阴影语言对齐;出现/隐藏仅动画 transform 与 opacity。
*/
import { useEffect, useState } from 'react'
export default function BackTop({ threshold = 300 }: { threshold?: number }) {
const [show, setShow] = useState(false)
useEffect(() => {
const handleScroll = () => setShow(window.scrollY > threshold)
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [threshold])
return (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className={`fixed bottom-6 right-6 z-40 flex h-10 w-10 items-center justify-center border border-ink-300 bg-paper-50 text-ink-600 transition-all duration-300 hover:border-ink-900 hover:bg-ink-900 hover:text-paper-50 ${
show ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-4 opacity-0'
}`}
aria-label="回到顶部"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
</svg>
</button>
)
}
+77
View File
@@ -0,0 +1,77 @@
/**
* 报头组件(前台共用)。
*
* 此前 HomePage 与 StandingsLayout 各自复制一份报头,导航项已经漂移
* (首页有「积分榜」、积分榜页有「比赛/预测」),且无当前页高亮。
* 现在统一为 Masthead:
* - active 声明当前版面,对应导航项加印报红高亮 + aria-current
* - 「管理」入口改用与全站一致的线条 SVG(替代字符 ⚙)
* - 管理类入口带 title 提示,避免访客被无声踢到登录页
* 页脚文案各页不同,仍由调用方自行渲染。
*/
import { Link } from 'react-router-dom'
export type MastheadActive = 'home' | 'standings'
function GearIcon() {
return (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68 1.65 1.65 0 0 0 10 3.17V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
)
}
function MastheadLink({
to,
active = false,
title,
children,
}: {
to: string
/** 当前版面才高亮;管理类入口无高亮概念 */
active?: boolean
title?: string
children: React.ReactNode
}) {
return (
<Link
to={to}
title={title}
aria-current={active ? 'page' : undefined}
className={`transition-colors ${
active ? 'font-medium text-press' : 'text-ink-500 hover:text-press'
}`}
>
{children}
</Link>
)
}
export default function Masthead({ active }: { active: MastheadActive }) {
return (
<header className="masthead-rule">
<div className="mx-auto max-w-5xl px-5 sm:px-8">
<div className="border-b border-ink-900 py-5 text-center sm:py-6">
<h1 className="font-brush text-5xl text-ink-900 sm:text-6xl">
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink-200 py-2 text-2xs text-ink-500">
<span>{new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' })}</span>
<nav className="flex items-center gap-4" aria-label="页面导航">
<MastheadLink to="/" active={active === 'home'}> / </MastheadLink>
<MastheadLink to="/standings" active={active === 'standings'}></MastheadLink>
{/* 管理类页面对访客需要登录,入口处先说明,避免无声跳登录页 */}
<MastheadLink to="/admin/eval" title="评估页面向管理员开放,需登录"></MastheadLink>
<MastheadLink to="/admin" title="管理后台,需登录">
<span className="inline-flex items-center gap-1">
<GearIcon />
</span>
</MastheadLink>
</nav>
</div>
</div>
</header>
)
}