From 1a37c759078dd641fb8cc48e1276485114d534ec Mon Sep 17 00:00:00 2001 From: shangfangjian Date: Fri, 18 Sep 2026 11:04:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(a11y):=20ink-400=20=E5=AF=B9=E6=AF=94?= =?UTF-8?q?=E5=BA=A6=E8=BE=BE=E6=A0=87,=E6=8E=A5=E5=85=A5=20Noto=20Serif?= =?UTF-8?q?=20SC=20webfont,=E8=A1=A5=E7=BA=B8=E5=BC=A0=E5=99=AA=E7=82=B9?= =?UTF-8?q?=E4=B8=8E=E5=85=A5=E5=9C=BA=E7=BC=96=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ink-400 #9C9587(2.90:1)→ #756F61(paper-50 4.87:1 / paper-100 4.55:1),正文级小字全部达标 - serif 栈插入 Noto Serif SC(fontsource 子集加载),Windows 不再跌进 SimSun - 首页加 feTurbulence 纸张噪点层(.grain);报头/赛程行/预测版 rise-in 错峰入场(60ms 步进) - reduce 动效用户:隐藏态与动画同在 no-preference 块,不会白屏 - Switch 提到模块顶层(修 render 重建丢焦点);a→Link(SPA 内链);h3→h2/h4→h3 修标题层级 - 按钮/输入热区抬高(btn 40px / btn-sm 32px / field py-2);index.css 硬编码色值改 :root 令牌 --- frontend/package-lock.json | 10 +++++ frontend/package.json | 1 + frontend/src/App.tsx | 15 ++++---- frontend/src/admin/AdminLayout.tsx | 14 +++---- frontend/src/index.css | 56 ++++++++++++++++++++++----- frontend/src/main.tsx | 5 +++ frontend/src/pages/Matches.tsx | 61 ++++++++++++++++-------------- frontend/tailwind.config.js | 6 ++- 8 files changed, 116 insertions(+), 52 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b4859ac..623dca1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "profeto-frontend", "version": "0.1.0", "dependencies": { + "@fontsource/noto-serif-sc": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.6" @@ -709,6 +710,15 @@ "node": ">=12" } }, + "node_modules/@fontsource/noto-serif-sc": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@fontsource/noto-serif-sc/-/noto-serif-sc-5.3.0.tgz", + "integrity": "sha512-0/zaEFkidiWldE62rTeD74x8ygUsQvejiSNtO0LQxQk3qpaHnlMZ3w4C7yH80B4KTIg8VKeFP4oSgwWMchY9+g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5eca45c..2d42d3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource/noto-serif-sc": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.6" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b52a89c..815d65f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,7 @@ * - /admin/* → 管理后台 */ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router-dom' import { ErrorBoundary } from './components/ErrorBoundary' import Matches from './pages/Matches' import { adminRoutes } from './admin/routes' @@ -23,9 +23,10 @@ function dateLine(): string { function HomePage() { return ( -
+ // grain: 纸张噪点氛围层;报头、赛程行、预测版各自带 rise-in 入场 +
{/* ── 报头:粗线 + 居中刊名 + 报眉 ── */} -
+

@@ -40,13 +41,13 @@ function HomePage() {

{dateLine()} - 管理后台 - +
diff --git a/frontend/src/admin/AdminLayout.tsx b/frontend/src/admin/AdminLayout.tsx index bf9b952..cf75856 100644 --- a/frontend/src/admin/AdminLayout.tsx +++ b/frontend/src/admin/AdminLayout.tsx @@ -6,7 +6,7 @@ */ import { useState, useEffect, useCallback } from 'react' -import { NavLink, Outlet, useLocation } from 'react-router-dom' +import { NavLink, Link, Outlet, useLocation } from 'react-router-dom' import { fetchHealth } from './dal' const NAV_ITEMS = [ @@ -133,13 +133,13 @@ export default function AdminLayout() { {/* 底部 */}
- 返回前台版面 - +
@@ -170,13 +170,13 @@ export default function AdminLayout() { /> {healthOk === null ? '检测中' : healthOk ? '系统正常' : '系统异常'} - 前台 - +
diff --git a/frontend/src/index.css b/frontend/src/index.css index a56d78c..5f2009a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,6 +3,12 @@ @tailwind utilities; @layer base { + /* 原生 CSS 里引用的令牌:与 tailwind.config 保持同步,避免硬编码色值散落 */ + :root { + --c-press: #9E1B1B; + --c-ink-900: #17140F; + } + html { -webkit-text-size-adjust: 100%; text-rendering: optimizeLegibility; @@ -19,7 +25,7 @@ /* 统一焦点环:印报红细线,键盘可达 */ :focus-visible { - outline: 2px solid #9e1b1b; + outline: 2px solid var(--c-press); outline-offset: 2px; } @@ -53,29 +59,61 @@ @layer components { /* ── 报头双线:粗线在上、细线在下 ── */ .masthead-rule { - border-top: 3px solid #17140f; + border-top: 3px solid var(--c-ink-900); } - /* ── 按钮:方正边框式,悬停反白 ── */ + /* ── 纸张噪点:feTurbulence 颗粒,给纸面一点纤维感 ── + 加在页面根元素上,fixed 全屏覆盖,不响应指针 */ + .grain::after { + content: ''; + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; + opacity: 0.04; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + } + + /* ── 入场:整页一次编排好的升起序列 ── + 只动 opacity/transform;错峰由元素上的 animation-delay 内联指定。 + 隐藏态必须与动画同在一个 no-preference 块里,否则 reduce 用户会看到永久空白 */ + @media (prefers-reduced-motion: no-preference) { + .rise-in { + opacity: 0; + animation: rise-in 0.48s cubic-bezier(0.16, 1, 0.3, 1) both; + } + } + @keyframes rise-in { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: none; + } + } + + /* ── 按钮:方正边框式,悬停反白;热区压到 40px(紧凑报纸行距下的折中) ── */ .btn { - @apply inline-flex items-center justify-center gap-1.5 border border-ink-300 bg-transparent px-3 py-1.5 + @apply inline-flex min-h-[40px] items-center justify-center gap-1.5 border border-ink-300 bg-transparent px-3 py-1.5 text-sm text-ink-700 transition-colors duration-150 hover:border-ink-900 hover:bg-ink-900 hover:text-paper-50 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-ink-300 disabled:hover:bg-transparent disabled:hover:text-ink-700; } .btn-sm { - @apply px-2.5 py-1 text-xs; + @apply min-h-[32px] px-2.5 py-1 text-xs; } .btn-solid { @apply border-ink-900 bg-ink-900 text-paper-50 hover:border-press hover:bg-press; } - /* ── 表单控件:方正、无圆角 ── */ + /* ── 表单控件:方正、无圆角;焦点态保留全局焦点环,只追加红边 ── */ .field { - @apply border border-ink-300 bg-transparent px-2.5 py-1.5 text-sm text-ink-800 - transition-colors hover:border-ink-400 - focus:border-press focus:outline-none; + @apply border border-ink-300 bg-transparent px-2.5 py-2 text-sm text-ink-800 + transition-colors hover:border-ink-400 focus:border-press; } /* ── 版面切换文字标签(联赛/状态/模式) ── */ diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 964aeb4..d59d762 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +// 中文衬线 webfont:按需子集加载(fontsource 按 unicode-range 切片,只拉用到的字块), +// font-display: swap 由包内 CSS 自带,不会白屏 +import '@fontsource/noto-serif-sc/400.css' +import '@fontsource/noto-serif-sc/600.css' +import '@fontsource/noto-serif-sc/700.css' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Matches.tsx b/frontend/src/pages/Matches.tsx index 5b32364..7e0977d 100644 --- a/frontend/src/pages/Matches.tsx +++ b/frontend/src/pages/Matches.tsx @@ -176,6 +176,30 @@ function OutcomeLine({ ) } +/** 状态/模式一组的文字切换(模块级组件:定义在组件体内会每次 render 重建,导致焦点丢失) */ +function Switch({ value, onChange, items }: { + value: string + onChange: (v: string) => void + items: { v: string; label: string; title?: string }[] +}) { + return ( + + {items.map((it, i) => ( + + {i > 0 && } + + + ))} + + ) +} + export default function Matches() { const [league, setLeague] = useState('E0') const [status, setStatus] = useState('scheduled') @@ -276,28 +300,6 @@ export default function Matches() { const leagueName = LEAGUES.find(l => l.code === league)?.name ?? league - /** 状态/模式一组的文字切换 */ - const Switch = ({ value, onChange, items }: { - value: string - onChange: (v: string) => void - items: { v: string; label: string; title?: string }[] - }) => ( - - {items.map((it, i) => ( - - {i > 0 && } - - - ))} - - ) - return (
{/* ── 联赛版面切换 ── */} @@ -374,7 +376,7 @@ export default function Matches() {
)} - {!loading && matches.map(m => { + {!loading && matches.map((m, idx) => { const st = STATUS_META[m.match_status] ?? { label: m.match_status, cls: 'text-ink-400' } const homeName = m.home_team_zh || m.home_team const awayName = m.away_team_zh || m.away_team @@ -384,9 +386,11 @@ export default function Matches() { return (
{/* 日期 + 状态:移动端同行,桌面端日期单独归列 */} @@ -496,12 +500,13 @@ function PredictionPanel({ const okReports = (prediction.agent_outputs ?? []).filter(r => r.status === 'ok') return ( -
+ // rise-in:预测版面整块入场,内部区块再错峰 +
{/* 版头 */}
-

+

预测版 · {homeName} 对 {awayName} -

+ {prediction.provider} / {prediction.model} {prediction.latency_ms !== null && ` · ${(prediction.latency_ms / 1000).toFixed(1)}s`} @@ -557,7 +562,7 @@ function PredictionPanel({ {/* ── 终裁意见:引文式,红竖线 ── */} {prediction.reasoning && (
-

终裁意见

+

终裁意见

{prediction.reasoning} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2217c23..219b792 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -12,12 +12,13 @@ export default { 300: '#DDD7C7', }, // 墨色:暖黑灰阶,替代冷调 slate + // 400 档压在 4.5:1 线上(paper-100 底 4.55:1 / paper-50 底 4.87:1),可作正文级小字 ink: { 50: '#FAF9F7', 100: '#F0EEE9', 200: '#E2DFD7', 300: '#C9C4B8', - 400: '#9C9587', + 400: '#756F61', 500: '#6E675B', 600: '#524C42', 700: '#3B362E', @@ -33,8 +34,11 @@ export default { }, fontFamily: { // 标题与比分:宋体血统,报纸版面的骨架 + // 拉丁字形走 Georgia(含真斜体,供 Profeto 刊名),中文走 Noto Serif SC webfont; + // 原栈在 Windows 上会跌进 SimSun(点阵感),webfont 兜底解决跨端不一致 serif: [ 'Georgia', + '"Noto Serif SC"', '"Times New Roman"', '"Songti SC"', 'STSong',