refactor(ui): 合并双组件库为 components/ui 基元层,设计令牌变量化,报头字体子集自托管

- 新建 components/ui/(Button/Input/Select/Modal/Tabs/Spinner/Skeleton 等),变体用联合类型约束,根治幻影变体
- admin/components.tsx 与 matches/ui.tsx 改为再导出壳,消除 Spinner 三份重复
- 日期工具收口到 lib/date.ts,滚动监听收口到 lib/useScroll.ts
- index.css 引入 :root RGB 三元组令牌,tailwind.config 接 <alpha-value>(修复透明度修饰符静默失效)
- 刘建毛草 5MB 全字库(jsDelivr 未锁版) → 1KB 自托管子集(仅「先知」二字,unicode-range 限定)
This commit is contained in:
2026-09-22 23:44:46 +08:00
parent a7e8b75503
commit 2cfc9ccc0f
18 changed files with 1226 additions and 569 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* 日期工具(纯函数,零依赖)。
*
* 从 `pages/matches/ui.tsx` 抽出 —— 它们原本与 UI 组件混在一个文件里,
* 但并不是组件,而是可独立测试的纯函数。
*/
/** 日期 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')}`
}
/** 未来第 n 天的日期 key */
function addDays(d: Date, n: number): string {
const x = new Date(d)
x.setFullYear(x.getFullYear(), x.getMonth(), x.getDate() + n)
return dateKey(x)
}
/** UTC ISO → 本地日期 YYYY-MM-DD(用于分组) */
export function toLocalDateKey(iso: string): string {
const d = new Date(iso)
return dateKey(d)
}
/** 日期分组头显示:今日/明日/周几 · 年月日 */
export function formatDateHeader(dateKeyStr: string): string {
if (!dateKeyStr) return '未开赛'
const d = new Date(dateKeyStr + 'T00:00:00')
if (isNaN(d.getTime())) return dateKeyStr
const today = new Date()
const todayKey = dateKey(today)
const tmr = new Date(today)
tmr.setDate(tmr.getDate() + 1)
const tmrKey = dateKey(tmr)
const weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
if (dateKeyStr === todayKey) return `今日 ${weekday}`
if (dateKeyStr === tmrKey) return `明日 ${weekday}`
return `${d.getMonth() + 1}${d.getDate()}${weekday}`
}
/**
* 按本地日期分组(非 UTC),保持时间序。
*
* 泛型约束 `{ match_date: string }` 而非具体业务类型 —— 使本工具
* 与 Matches 页面解耦,任何带日期字段的列表都能复用。
*/
export function groupByDate<T extends { match_date: string }>(list: T[]): Array<[string, T[]]> {
const map = new Map<string, T[]>()
for (const m of list) {
const key = toLocalDateKey(m.match_date)
const arr = map.get(key)
if (arr) arr.push(m)
else map.set(key, [m])
}
return [...map.entries()]
}
/** 比赛是否在未来 3 天内(用于默认视图过滤):今天 00:00 → 第 3 天 00:00 */
export function withinNext3Days(matchDate: string): boolean {
const key = toLocalDateKey(matchDate)
return key >= dateKey(new Date()) && key < addDays(new Date(), 3)
}