design: 全站设计打磨(语义色 token/弹窗动效与无障碍/报头收敛/交互小项) #19
+5
-58
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user