Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2190ea2af6 |
+20
-6
@@ -21,26 +21,39 @@ function dateLine(): string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 期号:当年第几天,报纸连载的仪式感 */
|
||||||
|
function issueNo(): number {
|
||||||
|
const now = new Date()
|
||||||
|
const start = new Date(now.getFullYear(), 0, 0)
|
||||||
|
return Math.floor((now.getTime() - start.getTime()) / 86400000)
|
||||||
|
}
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
return (
|
return (
|
||||||
// grain: 纸张噪点氛围层;报头、赛程行、预测版各自带 rise-in 入场
|
// grain: 纸张噪点氛围层;报头、赛程行、预测版各自带 rise-in 入场
|
||||||
<div className="grain min-h-screen bg-paper-50">
|
<div className="grain min-h-screen bg-paper-50">
|
||||||
{/* ── 报头:粗线 + 居中刊名 + 报眉 ── */}
|
{/* ── 报头:Oxford 双线 + 大字刊名 + 期号报眉 ── */}
|
||||||
<header className="masthead-rule rise-in">
|
<header className="masthead-rule rise-in">
|
||||||
<div className="mx-auto max-w-5xl px-5 sm:px-8">
|
<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">
|
<div className="border-b border-ink-900 py-6 text-center sm:py-8">
|
||||||
<h1 className="font-serif text-4xl font-bold tracking-widest text-ink-900">
|
<h1 className="font-serif text-5xl font-bold leading-none tracking-[0.16em] text-ink-900 sm:text-6xl">
|
||||||
先知
|
先知
|
||||||
<span className="ml-3 align-baseline font-serif text-base font-normal italic tracking-normal text-ink-500">
|
<span className="wordmark ml-3 align-baseline text-base font-normal italic tracking-normal text-ink-500 sm:text-lg">
|
||||||
Profeto
|
Profeto
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-2xs tracking-[0.4em] text-ink-500">
|
<p className="mt-3 text-2xs tracking-[0.4em] text-ink-500">
|
||||||
足球比分预测 · 五路专家 · 终裁汇总
|
足球比分预测 · 五路专家 · 终裁汇总
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-b border-ink-200 py-2 text-2xs text-ink-500">
|
<div className="flex items-center justify-between border-b border-ink-200 py-2 text-2xs text-ink-500">
|
||||||
<span>{dateLine()}</span>
|
<span className="tabular-nums">
|
||||||
|
<span className="text-ink-700">第 {issueNo()} 期</span>
|
||||||
|
<span className="mx-2 text-ink-300" aria-hidden="true">|</span>
|
||||||
|
{dateLine()}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-3">
|
||||||
|
<span className="tracking-[0.3em] text-ink-400">体育版</span>
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
className="flex min-h-[32px] items-center gap-1.5 text-press transition-colors hover:text-press-dark"
|
className="flex min-h-[32px] items-center gap-1.5 text-press transition-colors hover:text-press-dark"
|
||||||
@@ -48,6 +61,7 @@ function HomePage() {
|
|||||||
<span aria-hidden="true">⚙</span>
|
<span aria-hidden="true">⚙</span>
|
||||||
管理后台
|
管理后台
|
||||||
</Link>
|
</Link>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ export default function AdminLayout() {
|
|||||||
<div className="flex items-center justify-between border-b border-ink-900 px-5 py-4">
|
<div className="flex items-center justify-between border-b border-ink-900 px-5 py-4">
|
||||||
<h1 className="font-serif text-lg font-bold tracking-widest text-ink-900">
|
<h1 className="font-serif text-lg font-bold tracking-widest text-ink-900">
|
||||||
先知
|
先知
|
||||||
<span className="ml-2 align-baseline font-serif text-xs font-normal italic tracking-normal text-ink-500">
|
<span className="wordmark ml-2 align-baseline text-xs font-normal italic tracking-normal text-ink-500">
|
||||||
Profeto
|
Profeto
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-2xs tracking-[0.25em] text-ink-400">ADMIN</span>
|
<span className="text-2xs tracking-[0.25em] text-ink-400">ADMIN</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导航:选中项以印报红方块标记,同前台胜平负选中样式 */}
|
{/* 导航:选中项以墨色方块标记(红色只留给异常状态,与前台纪律一致) */}
|
||||||
<nav className="flex-1 overflow-y-auto px-3 py-4" aria-label="管理导航">
|
<nav className="flex-1 overflow-y-auto px-3 py-4" aria-label="管理导航">
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-0.5">
|
||||||
{NAV_ITEMS.map(item => (
|
{NAV_ITEMS.map(item => (
|
||||||
@@ -108,7 +108,7 @@ export default function AdminLayout() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex min-h-[44px] items-center gap-2.5 px-3 py-2.5 text-sm transition-colors ${
|
`flex min-h-[44px] items-center gap-2.5 px-3 py-2.5 text-sm transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-press-wash/60 font-medium text-press'
|
? 'bg-paper-200/70 font-medium text-ink-900'
|
||||||
: 'text-ink-500 hover:bg-paper-100 hover:text-ink-900'
|
: 'text-ink-500 hover:bg-paper-100 hover:text-ink-900'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ export default function AdminLayout() {
|
|||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-1.5 w-1.5 flex-shrink-0 ${isActive ? 'bg-press' : 'bg-transparent'}`}
|
className={`inline-block h-1.5 w-1.5 flex-shrink-0 ${isActive ? 'bg-ink-900' : 'bg-transparent'}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="text-base leading-none opacity-50" aria-hidden="true">
|
<span className="text-base leading-none opacity-50" aria-hidden="true">
|
||||||
|
|||||||
+26
-4
@@ -57,9 +57,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
/* ── 报头双线:粗线在上、细线在下 ── */
|
/* ── 报头 Oxford 粗细双线:3px 粗线 + 2px 空 + 1px 细线 ── */
|
||||||
.masthead-rule {
|
.masthead-rule {
|
||||||
border-top: 3px solid var(--c-ink-900);
|
border-top: 3px solid var(--c-ink-900);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.masthead-rule::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--c-ink-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 刊名斜体专用:Georgia 提供真斜体(其 old-style 数字不适合比分,已从主栈让位) ── */
|
||||||
|
.wordmark {
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 纵排文字:预测比分两侧的队名,中文报纸原生版面语言 ── */
|
||||||
|
.v-name {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 纸张噪点:feTurbulence 颗粒,给纸面一点纤维感 ──
|
/* ── 纸张噪点:feTurbulence 颗粒,给纸面一点纤维感 ──
|
||||||
@@ -116,16 +137,17 @@
|
|||||||
transition-colors hover:border-ink-400 focus:border-press;
|
transition-colors hover:border-ink-400 focus:border-press;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 版面切换文字标签(联赛/状态/模式) ── */
|
/* ── 版面切换文字标签(联赛/状态/模式) ──
|
||||||
|
激活态用墨色:印报红只留给 live/终裁/印章/错误,维持 10% 纪律 */
|
||||||
.tab {
|
.tab {
|
||||||
@apply relative whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900;
|
@apply relative whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900;
|
||||||
}
|
}
|
||||||
.tab-on {
|
.tab-on {
|
||||||
@apply font-medium text-press;
|
@apply font-medium text-ink-900;
|
||||||
}
|
}
|
||||||
.tab-on::after {
|
.tab-on::after {
|
||||||
content: '';
|
content: '';
|
||||||
@apply absolute inset-x-0 bottom-0 h-0.5 bg-press;
|
@apply absolute inset-x-0 bottom-0 h-0.5 bg-ink-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 小节标题:宋体加粗 + 墨色底线 ── */
|
/* ── 小节标题:宋体加粗 + 墨色底线 ── */
|
||||||
|
|||||||
+106
-13
@@ -370,9 +370,15 @@ export default function Matches() {
|
|||||||
{loading && <SkeletonRows n={4} />}
|
{loading && <SkeletonRows n={4} />}
|
||||||
|
|
||||||
{!loading && matches.length === 0 && (
|
{!loading && matches.length === 0 && (
|
||||||
<div className="border-y border-ink-200 py-14 text-center">
|
/* 空态做成报纸「启事」体:外粗线 + 内细线框 */
|
||||||
<p className="font-serif text-sm text-ink-600">本版暂无赛程</p>
|
<div className="border-b border-ink-900 py-10 text-center">
|
||||||
<p className="mt-1.5 text-xs text-ink-400">请先通过采集接口导入 {leagueName} 的比赛数据</p>
|
<div className="mx-auto max-w-sm border border-ink-300 px-6 py-6">
|
||||||
|
<p className="font-serif text-xs tracking-[0.5em] text-ink-500">启事</p>
|
||||||
|
<p className="mt-3 font-serif text-sm font-bold text-ink-800">本版暂无赛程</p>
|
||||||
|
<p className="mt-1.5 text-xs leading-relaxed text-ink-500">
|
||||||
|
请先通过采集接口导入 {leagueName} 的比赛数据,再回到本版查看。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -386,12 +392,17 @@ export default function Matches() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
className={`rise-in border-b border-ink-200 px-1 py-3 transition-colors hover:bg-paper-100 ${
|
className={`rise-in group relative border-b border-ink-200 px-1 py-3 transition-colors hover:bg-paper-100 ${
|
||||||
active ? 'bg-press-wash/50' : ''
|
active ? 'bg-press-wash/50' : ''
|
||||||
}`}
|
}`}
|
||||||
// 入场错峰:60ms 步进,封顶 420ms,长列表不拖沓
|
// 入场错峰:60ms 步进,封顶 420ms,长列表不拖沓
|
||||||
style={{ animationDelay: `${Math.min(idx * 60, 420)}ms` }}
|
style={{ animationDelay: `${Math.min(idx * 60, 420)}ms` }}
|
||||||
>
|
>
|
||||||
|
{/* 悬停书签:左缘红刻度,报眼式标记 */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-y-0 left-0 w-0.5 bg-press opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
<div className="flex flex-col gap-2 sm:grid sm:grid-cols-[88px_minmax(0,1fr)_64px_minmax(0,1fr)_56px_auto] sm:items-center sm:gap-x-3 sm:gap-y-0">
|
<div className="flex flex-col gap-2 sm:grid sm:grid-cols-[88px_minmax(0,1fr)_64px_minmax(0,1fr)_56px_auto] sm:items-center sm:gap-x-3 sm:gap-y-0">
|
||||||
{/* 日期 + 状态:移动端同行,桌面端日期单独归列 */}
|
{/* 日期 + 状态:移动端同行,桌面端日期单独归列 */}
|
||||||
<div className="flex items-center justify-between sm:contents">
|
<div className="flex items-center justify-between sm:contents">
|
||||||
@@ -486,6 +497,89 @@ export default function Matches() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 终裁印章:预测版右上角的红印,全站强调色的点睛之处 */
|
||||||
|
function FinalSeal() {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute right-1 top-0 flex h-9 w-9 -rotate-6 items-center justify-center rounded-[2px] bg-press"
|
||||||
|
>
|
||||||
|
<span className="font-serif text-[13px] font-bold leading-[1.2] tracking-[0.15em] text-paper-50">
|
||||||
|
终裁
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预测比分:版面核心,大号宋体 + 纵排队名 + 终裁印章 */
|
||||||
|
function ScoreMast({
|
||||||
|
homeName,
|
||||||
|
awayName,
|
||||||
|
home,
|
||||||
|
away,
|
||||||
|
}: {
|
||||||
|
homeName: string
|
||||||
|
awayName: string
|
||||||
|
home: number | null
|
||||||
|
away: number | null
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative text-center">
|
||||||
|
<FinalSeal />
|
||||||
|
<div className="flex items-center justify-center gap-5 sm:gap-7">
|
||||||
|
<span className="v-name font-serif text-sm text-ink-700">{homeName}</span>
|
||||||
|
<p className="font-serif text-5xl font-bold tabular-nums leading-none text-ink-900 sm:text-6xl">
|
||||||
|
{home ?? '-'}
|
||||||
|
<span className="mx-2 font-normal text-ink-300 sm:mx-3">:</span>
|
||||||
|
{away ?? '-'}
|
||||||
|
</p>
|
||||||
|
<span className="v-name font-serif text-sm text-ink-700">{awayName}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-2xs tracking-[0.5em] text-ink-400">预测比分</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 五路专家倾向条:home_edge 的分歧可视化,红=利主,墨=利客,条高=强度 */
|
||||||
|
function ExpertLeans({ reports }: { reports: AgentReport[] }) {
|
||||||
|
const withEdge = reports.filter(
|
||||||
|
(r): r is AgentReport & { home_edge: number } => r.status === 'ok' && r.home_edge !== null,
|
||||||
|
)
|
||||||
|
if (withEdge.length === 0) return null
|
||||||
|
const summary = withEdge
|
||||||
|
.map(r => `${AGENT_LABELS[r.agent] ?? r.agent} ${r.home_edge > 0 ? '利主' : '利客'} ${Math.abs(r.home_edge).toFixed(2)}`)
|
||||||
|
.join(',')
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mt-3 flex items-end justify-center gap-5"
|
||||||
|
role="img"
|
||||||
|
aria-label={`五路专家倾向:${summary}`}
|
||||||
|
>
|
||||||
|
{withEdge.map((r, i) => {
|
||||||
|
const up = r.home_edge >= 0
|
||||||
|
const h = Math.round(Math.min(1, Math.abs(r.home_edge)) * 50)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={r.agent}
|
||||||
|
className="flex flex-col items-center gap-1.5"
|
||||||
|
title={`${AGENT_LABELS[r.agent] ?? r.agent} · 主队优势 ${(r.home_edge > 0 ? '+' : '') + r.home_edge.toFixed(2)}`}
|
||||||
|
>
|
||||||
|
<div className="relative h-16 w-2 bg-ink-200">
|
||||||
|
<span className="absolute inset-x-0 top-1/2 h-px bg-ink-400" aria-hidden="true" />
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`absolute inset-x-0 ${up ? 'bottom-1/2 bg-press' : 'top-1/2 bg-ink-600'}`}
|
||||||
|
style={{ height: `${h}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-serif text-2xs text-ink-500">{CN_NUM[i] ?? i + 1}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function PredictionPanel({
|
function PredictionPanel({
|
||||||
prediction,
|
prediction,
|
||||||
match,
|
match,
|
||||||
@@ -514,15 +608,13 @@ function PredictionPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-7 px-4 py-6 sm:px-5">
|
<div className="space-y-7 px-4 py-6 sm:px-5">
|
||||||
{/* ── 预测比分:版面核心,大号宋体 ── */}
|
{/* ── 预测比分:版面核心,纵排队名 + 终裁印章 ── */}
|
||||||
<div className="text-center">
|
<ScoreMast
|
||||||
<p className="font-serif text-5xl font-bold tabular-nums leading-none text-ink-900 sm:text-6xl">
|
homeName={homeName}
|
||||||
{prediction.pred_home_goals ?? '-'}
|
awayName={awayName}
|
||||||
<span className="mx-3 font-normal text-ink-300">:</span>
|
home={prediction.pred_home_goals}
|
||||||
{prediction.pred_away_goals ?? '-'}
|
away={prediction.pred_away_goals}
|
||||||
</p>
|
/>
|
||||||
<p className="mt-3 text-2xs tracking-[0.5em] text-ink-400">预测比分</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 胜平负 ── */}
|
{/* ── 胜平负 ── */}
|
||||||
<div className="border-y border-ink-200 py-4">
|
<div className="border-y border-ink-200 py-4">
|
||||||
@@ -552,6 +644,7 @@ function PredictionPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<ExpertLeans reports={prediction.agent_outputs} />
|
||||||
{prediction.agent_outputs.map((r, i) => (
|
{prediction.agent_outputs.map((r, i) => (
|
||||||
<AgentCard key={r.agent} report={r} no={CN_NUM[i] ?? String(i + 1)} />
|
<AgentCard key={r.agent} report={r} no={CN_NUM[i] ?? String(i + 1)} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ export default {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
// 标题与比分:宋体血统,报纸版面的骨架
|
// 标题与比分:宋体血统,报纸版面的骨架
|
||||||
// 拉丁字形走 Georgia(含真斜体,供 Profeto 刊名),中文走 Noto Serif SC webfont;
|
// Noto Serif SC 站首位:拉丁与数字走它的直线字形(Georgia 是 old-style 数字,
|
||||||
// 原栈在 Windows 上会跌进 SimSun(点阵感),webfont 兜底解决跨端不一致
|
// 比分 3:1 会高低不齐);Georgia 只保留给刊名斜体(.wordmark)
|
||||||
serif: [
|
serif: [
|
||||||
'Georgia',
|
|
||||||
'"Noto Serif SC"',
|
'"Noto Serif SC"',
|
||||||
|
'Georgia',
|
||||||
'"Times New Roman"',
|
'"Times New Roman"',
|
||||||
'"Songti SC"',
|
'"Songti SC"',
|
||||||
'STSong',
|
'STSong',
|
||||||
|
|||||||
Reference in New Issue
Block a user