首页:去掉日期选择器,顶部增加「进行中」比赛区块

- 移除日期选择器(今日按钮 + date input + 清除按钮)及相关 date 状态
- 顶部新增「进行中 · 实时比分」独立区块:红色脉冲圆点 + 主客比分
- 进行中区块仅在默认「未开赛」视图显示,切换已完赛/全部时隐藏
- 进行中数据独立请求(status=in_play,limit 20),不影响主列表逻辑
- 清理 todayStr(),改用 dateKey()统一日期格式化

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
shangfangjian
2026-09-20 20:57:27 +08:00
co-authored by new-provider/LongCat-2.0 <
parent 9d7f8dfeee
commit dd538d66d7
+54 -34
View File
@@ -189,12 +189,12 @@ function OutcomeLine({
export default function Matches() { export default function Matches() {
const [league, setLeague] = useState('E0') const [league, setLeague] = useState('E0')
const [status, setStatus] = useState('scheduled') const [status, setStatus] = useState('scheduled')
const [date, setDate] = useState('') // 日期筛选(空=全部),"today"=今日
const [matches, setMatches] = useState<Match[]>([]) const [matches, setMatches] = useState<Match[]>([])
const [nextCursor, setNextCursor] = useState<string | null>(null) const [nextCursor, setNextCursor] = useState<string | null>(null)
const [loadingMore, setLoadingMore] = useState(false) const [loadingMore, setLoadingMore] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showAllUpcoming, setShowAllUpcoming] = useState(false) // 默认仅展示未来 3 天;true 展开全部 const [showAllUpcoming, setShowAllUpcoming] = useState(false) // 默认仅展示未来 3 天;true 展开全部
const [liveMatches, setLiveMatches] = useState<Match[]>([]) // 进行中比赛(顶部独立区块)
const [predictingId, setPredictingId] = useState<number | null>(null) const [predictingId, setPredictingId] = useState<number | null>(null)
const [prediction, setPrediction] = useState<Prediction | null>(null) const [prediction, setPrediction] = useState<Prediction | null>(null)
const [predictionFor, setPredictionFor] = useState<Match | null>(null) const [predictionFor, setPredictionFor] = useState<Match | null>(null)
@@ -228,8 +228,6 @@ export default function Matches() {
setError(null) setError(null)
try { try {
const params = new URLSearchParams({ league, status, limit: '50' }) const params = new URLSearchParams({ league, status, limit: '50' })
if (date === 'today') params.set('date', todayStr())
else if (date) params.set('date', date)
const res = await fetch(`/api/v1/matches?${params}`) const res = await fetch(`/api/v1/matches?${params}`)
if (seq !== loadSeq.current) return // 已有更新的请求,丢弃本次结果 if (seq !== loadSeq.current) return // 已有更新的请求,丢弃本次结果
if (!res.ok) throw new Error(`HTTP ${res.status}`) if (!res.ok) throw new Error(`HTTP ${res.status}`)
@@ -243,7 +241,7 @@ export default function Matches() {
} finally { } finally {
if (seq === loadSeq.current) setLoading(false) if (seq === loadSeq.current) setLoading(false)
} }
}, [league, status, date]) }, [league, status])
// 加载下一页(游标分页) // 加载下一页(游标分页)
const loadMore = async () => { const loadMore = async () => {
@@ -252,8 +250,6 @@ export default function Matches() {
setLoadingMore(true) setLoadingMore(true)
try { try {
const params = new URLSearchParams({ league, status, limit: '50', cursor: nextCursor }) const params = new URLSearchParams({ league, status, limit: '50', cursor: nextCursor })
if (date === 'today') params.set('date', todayStr())
else if (date) params.set('date', date)
const res = await fetch(`/api/v1/matches?${params}`) const res = await fetch(`/api/v1/matches?${params}`)
if (seq !== loadSeq.current) return if (seq !== loadSeq.current) return
if (!res.ok) throw new Error(`HTTP ${res.status}`) if (!res.ok) throw new Error(`HTTP ${res.status}`)
@@ -269,21 +265,32 @@ export default function Matches() {
} }
} }
useEffect(() => { load() }, [load]) // 加载进行中比赛(顶部独立区块)
const loadLive = useCallback(async () => {
try {
const params = new URLSearchParams({ league, status: 'in_play', limit: '20' })
const res = await fetch(`/api/v1/matches?${params}`)
if (!res.ok) return
const data = await res.json()
setLiveMatches(data.items ?? [])
} catch {
/* ignore:进行中非核心功能 */
}
}, [league])
/** 今日日期 YYYY-MM-DD(本地时区,非 UTC) */ useEffect(() => { load(); loadLive() }, [load, loadLive])
function todayStr(): string {
const d = new Date() /** 日期 key 辅助:YYYY-MM-DD(本地时区) */
function dateKey(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
} }
/** 未来 3 天窗口:今天 00:00 → 第 3 天 00:00(即今天/明天/后天) */ /** 未来 3 天窗口:今天 00:00 → 第 3 天 00:00(即今天/明天/后天) */
function addDays(d: Date, n: number): string { function addDays(d: Date, n: number): string {
const x = new Date(d) const x = new Date(d)
x.setFullYear(x.getFullYear(), x.getMonth(), x.getDate() + n) x.setFullYear(x.getFullYear(), x.getMonth(), x.getDate() + n)
return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, '0')}-${String(x.getDate()).padStart(2, '0')}` return dateKey(x)
} }
const todayKey = todayStr() const todayKey = dateKey(new Date())
const windowEnd = addDays(new Date(), 3) // 不含 const windowEnd = addDays(new Date(), 3) // 不含
/** 比赛是否在未来 3 天内(用于默认视图过滤) */ /** 比赛是否在未来 3 天内(用于默认视图过滤) */
@@ -423,27 +430,6 @@ export default function Matches() {
/> />
</span> </span>
<span className="inline-flex items-center gap-2">
<span className="text-2xs text-ink-400"></span>
<span className="inline-flex items-center gap-1">
<button
onClick={() => setDate(date === 'today' ? '' : 'today')}
className={`btn btn-sm px-2 ${date === 'today' ? 'btn-solid' : ''}`}
title="只看今日"
></button>
<input
type="date"
value={date === 'today' ? todayStr() : date}
onChange={e => setDate(e.target.value)}
className="field px-1.5 py-1 text-xs"
aria-label="按日期筛选"
/>
{date && (
<button onClick={() => setDate('')} className="text-ink-400 hover:text-ink-900 text-sm px-0.5" aria-label="清除日期" title="清除">×</button>
)}
</span>
</span>
<span className="ml-auto inline-flex items-center gap-3"> <span className="ml-auto inline-flex items-center gap-3">
<span className="tabular-nums"> <span className="tabular-nums">
{isScheduledView && !showAllUpcoming && hasHiddenUpcoming {isScheduledView && !showAllUpcoming && hasHiddenUpcoming
@@ -478,6 +464,40 @@ export default function Matches() {
/> />
)} )}
{/* ── 进行中比赛(顶部独立区块,仅未开赛视图展示) ── */}
{isScheduledView && liveMatches.length > 0 && (
<section aria-label="进行中" className="border border-ink-900 bg-paper-100">
<div className="flex items-center gap-2 border-b border-ink-900 px-3 py-2">
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-press" />
<span className="text-xs font-medium tracking-wide text-ink-700"> · </span>
<span className="text-2xs text-ink-400">{liveMatches.length} </span>
</div>
<div className="divide-y divide-ink-200">
{liveMatches.map(m => {
const homeName = m.home_team_zh || m.home_team
const awayName = m.away_team_zh || m.away_team
return (
<div key={m.id} className="flex items-center justify-between gap-3 px-3 py-2.5">
<div className="flex items-center gap-2 min-w-0">
<span className="w-12 shrink-0 text-center font-serif text-lg font-bold tabular-nums text-ink-900">
{m.home_goals ?? '-'}
</span>
<span className="min-w-0 truncate text-xs text-ink-700">{homeName}</span>
</div>
<span className="shrink-0 text-2xs text-ink-400">vs</span>
<div className="flex items-center gap-2 min-w-0">
<span className="min-w-0 truncate text-right text-xs text-ink-700">{awayName}</span>
<span className="w-12 shrink-0 text-center font-serif text-lg font-bold tabular-nums text-ink-900">
{m.away_goals ?? '-'}
</span>
</div>
</div>
)
})}
</div>
</section>
)}
{/* ── 赛程栏:表格化,行间细线,按日期分组 ── */} {/* ── 赛程栏:表格化,行间细线,按日期分组 ── */}
<section aria-label="赛程"> <section aria-label="赛程">
{loading && <SkeletonRows n={4} />} {loading && <SkeletonRows n={4} />}