feat(theme): complete Minecraft overhaul for all pages including admin/utility

这个提交包含在:
X
2026-02-15 17:06:24 -08:00
父节点 e4742ff5ea
当前提交 3a98882a71
修改 24 个文件,包含 1203 行新增208 行删除

查看文件

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import { Edit, Gift, Plus, RefreshCw, ScrollText, Search, Trash2, Coins } from "lucide-react";
type RedeemItem = {
id: number;
@@ -167,7 +168,8 @@ export default function AdminRedeemPage() {
return (
<main className="mx-auto max-w-7xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<Gift size={24} />
{tx("管理员:积分兑换管理", "Admin: Redeem Management")}
</h1>
<p className="mt-1 text-sm text-zinc-600">
@@ -241,10 +243,17 @@ export default function AdminRedeemPage() {
<div className="mt-3 flex flex-wrap gap-2">
<button
className="rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50"
className="rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50 flex items-center gap-2"
onClick={() => void submit()}
disabled={saving}
>
{saving ? (
<RefreshCw size={14} className="animate-spin" />
) : editingId ? (
<Edit size={14} />
) : (
<Plus size={14} />
)}
{saving
? tx("保存中...", "Saving...")
: editingId
@@ -261,17 +270,21 @@ export default function AdminRedeemPage() {
{tx("清空表单", "Clear form")}
</button>
<button
className="rounded border px-4 py-2 text-sm"
className="rounded border px-4 py-2 text-sm flex items-center gap-2"
onClick={() => void load()}
disabled={loading}
>
<RefreshCw size={14} className={loading ? "animate-spin" : ""} />
{tx("刷新数据", "Refresh data")}
</button>
</div>
</section>
<section className="mt-4 rounded-xl border bg-white p-4">
<h2 className="text-base font-semibold">{tx("兑换物品列表", "Redeem Items")}</h2>
<h2 className="text-base font-semibold flex items-center gap-2">
<Coins size={18} />
{tx("兑换物品列表", "Redeem Items")}
</h2>
<div className="mt-3 divide-y">
{items.map((item) => (
<article key={item.id} className="py-2 text-sm">
@@ -291,10 +304,12 @@ export default function AdminRedeemPage() {
</p>
<p className="text-xs text-zinc-500">{item.description || "-"}</p>
<div className="mt-1 flex gap-2">
<button className="rounded border px-2 py-1 text-xs" onClick={() => edit(item)}>
<button className="rounded border px-2 py-1 text-xs flex items-center gap-1" onClick={() => edit(item)}>
<Edit size={12} />
{tx("编辑", "Edit")}
</button>
<button className="rounded border px-2 py-1 text-xs" onClick={() => void deactivate(item.id)}>
<button className="rounded border px-2 py-1 text-xs flex items-center gap-1" onClick={() => void deactivate(item.id)}>
<Trash2 size={12} />
{tx("下架", "Disable")}
</button>
</div>
@@ -308,7 +323,10 @@ export default function AdminRedeemPage() {
<section className="mt-4 rounded-xl border bg-white p-4">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-base font-semibold">{tx("兑换记录", "Redeem Records")}</h2>
<h2 className="text-base font-semibold flex items-center gap-2">
<ScrollText size={18} />
{tx("兑换记录", "Redeem Records")}
</h2>
<input
className="rounded border px-3 py-1 text-xs"
placeholder={tx("按 user_id 筛选(可选)", "Filter by user_id (optional)")}

查看文件

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import { RefreshCw, Save, Shield, UserCog, Users } from "lucide-react";
type AdminUser = {
id: number;
@@ -75,7 +76,8 @@ export default function AdminUsersPage() {
return (
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<Users size={24} />
{tx("管理员用户与积分", "Admin Users & Rating")}
</h1>
<p className="mt-2 text-sm text-zinc-600">
@@ -85,10 +87,11 @@ export default function AdminUsersPage() {
<div className="mt-4 flex flex-wrap gap-2">
<button
className="rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50"
className="rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50 flex items-center gap-2"
onClick={() => void load()}
disabled={loading}
>
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
{loading ? tx("刷新中...", "Refreshing...") : tx("刷新用户列表", "Refresh users")}
</button>
</div>
@@ -103,7 +106,10 @@ export default function AdminUsersPage() {
<tr>
<th className="px-3 py-2">ID</th>
<th className="px-3 py-2">{tx("用户名", "Username")}</th>
<th className="px-3 py-2">Rating</th>
<th className="px-3 py-2 flex items-center gap-1">
<Shield size={14} />
Rating
</th>
<th className="px-3 py-2">{tx("创建时间", "Created At")}</th>
<th className="px-3 py-2">{tx("操作", "Action")}</th>
</tr>
@@ -130,9 +136,10 @@ export default function AdminUsersPage() {
<td className="px-3 py-2 text-zinc-600">{fmtTs(user.created_at)}</td>
<td className="px-3 py-2">
<button
className="rounded border px-3 py-1 text-xs hover:bg-zinc-100"
className="rounded border px-3 py-1 text-xs hover:bg-zinc-100 flex items-center gap-1"
onClick={() => void updateRating(user.id, Math.max(0, Number(user.rating) || 0))}
>
<Save size={12} />
{tx("保存", "Save")}
</button>
</td>

查看文件

@@ -2,6 +2,8 @@
import dynamic from "next/dynamic";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { FileCode, ArrowLeft } from "lucide-react";
import { API_BASE, apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
@@ -71,9 +73,16 @@ export default function ApiDocsPage() {
return (
<main className="mx-auto max-w-7xl px-3 py-5 max-[390px]:px-2 sm:px-4 md:px-6 md:py-6">
<h1 className="mb-4 text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
{tx("API 文档Swagger", "API Docs (Swagger)")}
</h1>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<FileCode size={24} />
{tx("API 文档Swagger", "API Docs (Swagger)")}
</h1>
<Link href="/imports" className="flex items-center gap-1 text-sm text-zinc-600 hover:text-zinc-900">
<ArrowLeft size={16} />
{tx("返回", "Back")}
</Link>
</div>
<div className="overflow-x-auto rounded-xl border bg-white p-2">
<SwaggerUI url={specUrl} docExpansion="list" defaultModelsExpandDepth={1} />
</div>

查看文件

@@ -3,6 +3,7 @@
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Eye, EyeOff, Key, LogIn, User, UserPlus } from "lucide-react";
import { API_BASE, apiFetch } from "@/lib/api";
import { readToken, saveToken } from "@/lib/auth";
@@ -104,9 +105,9 @@ export default function AuthPage() {
<div className="grid grid-cols-2 gap-2 rounded-none bg-black/20 p-1 text-sm">
<button
type="button"
className={`rounded-none px-3 py-2 border-[2px] transition-all ${mode === "login"
? "bg-[color:var(--mc-wood)] border-black text-white shadow-[2px_2px_0_rgba(0,0,0,0.4)]"
: "border-transparent text-zinc-500 hover:text-zinc-300"
className={`rounded-none px-3 py-2 border-[2px] transition-all flex items-center justify-center gap-2 ${mode === "login"
? "bg-[color:var(--mc-wood)] border-black text-white shadow-[2px_2px_0_rgba(0,0,0,0.4)]"
: "border-transparent text-zinc-500 hover:text-zinc-300"
}`}
onClick={() => {
setMode("login");
@@ -114,13 +115,14 @@ export default function AuthPage() {
}}
disabled={loading}
>
<LogIn size={16} />
{tx("登录服务器", "Login")}
</button>
<button
type="button"
className={`rounded-none px-3 py-2 border-[2px] transition-all ${mode === "register"
? "bg-[color:var(--mc-wood)] border-black text-white shadow-[2px_2px_0_rgba(0,0,0,0.4)]"
: "border-transparent text-zinc-500 hover:text-zinc-300"
className={`rounded-none px-3 py-2 border-[2px] transition-all flex items-center justify-center gap-2 ${mode === "register"
? "bg-[color:var(--mc-wood)] border-black text-white shadow-[2px_2px_0_rgba(0,0,0,0.4)]"
: "border-transparent text-zinc-500 hover:text-zinc-300"
}`}
onClick={() => {
setMode("register");
@@ -128,13 +130,17 @@ export default function AuthPage() {
}}
disabled={loading}
>
<UserPlus size={16} />
{tx("新玩家注册", "New Player")}
</button>
</div>
<div className="mt-5 space-y-4 font-mono">
<div>
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("玩家代号", "Username")}</label>
<label className="text-sm font-bold text-[color:var(--mc-stone)] flex items-center gap-2">
<User size={14} />
{tx("玩家代号", "Username")}
</label>
<input
className="mt-1 w-full"
value={username}
@@ -146,7 +152,10 @@ export default function AuthPage() {
<div>
<div className="flex items-center justify-between">
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("极其机密的口令", "Secret Password")}</label>
<label className="text-sm font-bold text-[color:var(--mc-stone)] flex items-center gap-2">
<Key size={14} />
{tx("极其机密的口令", "Secret Password")}
</label>
<span className={`text-xs ${strength.color}`}>{strength.label}</span>
</div>
<input
@@ -161,7 +170,10 @@ export default function AuthPage() {
{mode === "register" && (
<div>
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("确认口令", "Confirm Secret")}</label>
<label className="text-sm font-bold text-[color:var(--mc-stone)] flex items-center gap-2">
<Key size={14} />
{tx("确认口令", "Confirm Secret")}
</label>
<input
type={showPassword ? "text" : "password"}
className="mt-1 w-full"
@@ -180,6 +192,7 @@ export default function AuthPage() {
onChange={(e) => setShowPassword(e.target.checked)}
className="accent-[color:var(--mc-wood)]"
/>
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
{tx("显示口令", "Reveal Secret")}
</label>

查看文件

@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import { Activity, AlertCircle, List, Play, RefreshCw, Server, Trash2, Zap } from "lucide-react";
type BackendLogItem = {
id: number;
@@ -277,7 +278,8 @@ export default function BackendLogsPage() {
return (
<main className="mx-auto max-w-7xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<div className="flex flex-wrap items-center justify-between gap-3">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<Server size={24} />
{tx("后台日志(题解异步队列)", "Backend Logs (Async Solution Queue)")}
</h1>
<div className="flex w-full flex-wrap items-center gap-2 text-sm sm:w-auto sm:justify-end">
@@ -288,10 +290,11 @@ export default function BackendLogsPage() {
{tx("缺失答案题目", "Problems missing answers")} {missingProblems}
</span>
<button
className="rounded border px-3 py-1 disabled:opacity-50"
className="rounded border px-3 py-1 disabled:opacity-50 flex items-center gap-1"
onClick={() => void triggerMissingSolutions()}
disabled={triggerLoading}
>
<Zap size={14} />
{triggerLoading ? tx("手动补全中...", "Triggering...") : tx("手动补全(可选)", "Manual fill (optional)")}
</button>
<select
@@ -303,7 +306,8 @@ export default function BackendLogsPage() {
<option value={100}>{tx("最近 100 条", "Latest 100")}</option>
<option value={200}>{tx("最近 200 条", "Latest 200")}</option>
</select>
<button className="rounded border px-3 py-1 sm:ml-auto" onClick={() => void refresh()} disabled={loading}>
<button className="rounded border px-3 py-1 sm:ml-auto flex items-center gap-1" onClick={() => void refresh()} disabled={loading}>
<RefreshCw size={14} className={loading ? "animate-spin" : ""} />
{tx("刷新", "Refresh")}
</button>
</div>
@@ -401,7 +405,10 @@ export default function BackendLogsPage() {
<section className="mt-4 grid gap-3 md:grid-cols-2">
<article className="rounded-xl border bg-white p-3">
<h2 className="text-sm font-medium">{tx("正在处理Running", "Running Jobs")}</h2>
<h2 className="text-sm font-medium flex items-center gap-2">
<Activity size={16} className="text-emerald-600" />
{tx("正在处理Running", "Running Jobs")}
</h2>
<p className="mt-1 text-xs text-zinc-600">
{tx("当前题目 ID", "Current problem IDs:")}
{runningIds.length ? runningIds.join(", ") : tx("无", "None")}
@@ -423,7 +430,10 @@ export default function BackendLogsPage() {
</article>
<article className="rounded-xl border bg-white p-3">
<h2 className="text-sm font-medium">{tx("待处理队列Queued", "Queued Jobs")}</h2>
<h2 className="text-sm font-medium flex items-center gap-2">
<List size={16} className="text-amber-600" />
{tx("待处理队列Queued", "Queued Jobs")}
</h2>
<p className="mt-1 text-xs text-zinc-600">
{tx("待处理题目 ID预览", "Queued problem IDs (preview):")}
{queuedIds.length ? queuedIds.join(", ") : tx("无", "None")}

查看文件

@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
import { useUiPreferences } from "@/components/ui-preference-provider";
import { Calendar, Swords, Timer, Trophy, Shield } from "lucide-react";
type Contest = {
id: number;
@@ -44,7 +45,7 @@ export default function ContestsPage() {
<h1 className={`text-xl font-bold max-[390px]:text-lg sm:text-2xl ${isMc ? "text-[color:var(--mc-diamond)] mc-text-shadow" : ""}`}>
{isMc ? (
<span className="flex items-center gap-2">
<span></span>
<Swords size={24} />
{tx("突袭公告板", "Raid Board")}
</span>
) : (
@@ -66,13 +67,19 @@ export default function ContestsPage() {
>
<div className="flex items-start justify-between">
<div>
<h2 className={`text-lg font-medium ${isMc ? "text-[color:var(--mc-gold)]" : ""}`}>
{isMc && <span className="mr-2">🛡</span>}
<h2 className={`text-lg font-medium flex items-center gap-2 ${isMc ? "text-[color:var(--mc-gold)]" : ""}`}>
{isMc && <Shield size={20} />}
{c.title}
</h2>
<div className={`mt-2 text-xs ${isMc ? "text-zinc-400" : "text-zinc-500"}`}>
<p>{tx("开始", "Start")}: {new Date(c.starts_at * 1000).toLocaleString()}</p>
<p>{tx("结束", "End")}: {new Date(c.ends_at * 1000).toLocaleString()}</p>
<div className={`mt-2 text-xs flex flex-col gap-1 ${isMc ? "text-zinc-400" : "text-zinc-500"}`}>
<p className="flex items-center gap-2">
{isMc && <Calendar size={14} />}
{tx("开始", "Start")}: {new Date(c.starts_at * 1000).toLocaleString()}
</p>
<p className="flex items-center gap-2">
{isMc && <Timer size={14} />}
{tx("结束", "End")}: {new Date(c.ends_at * 1000).toLocaleString()}
</p>
</div>
</div>
{isMc && (

查看文件

@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "react";
import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import { Activity, HardDrive, Play, RefreshCw, Server, FileText, CheckCircle, XCircle, Clock } from "lucide-react";
type ImportJob = {
id: number;
@@ -253,7 +254,8 @@ export default function ImportsPage() {
return (
<main className="mx-auto max-w-7xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<HardDrive size={24} />
{tx("题库导入/出题任务", "Import / Generation Jobs")}
</h1>
@@ -336,7 +338,17 @@ export default function ImportsPage() {
onClick={() => void runImport()}
disabled={loading || running}
>
{running ? tx("导入中...", "Importing...") : tx("启动导入任务", "Start Import Job")}
{running ? (
<span className="flex items-center justify-center gap-2">
<Activity size={16} className="animate-spin" />
{tx("导入中...", "Importing...")}
</span>
) : (
<span className="flex items-center justify-center gap-2">
<Play size={16} />
{tx("启动导入任务", "Start Import Job")}
</span>
)}
</button>
{runMode === "luogu" && (
<label className="flex w-full items-center gap-2 text-sm sm:w-auto">
@@ -348,10 +360,12 @@ export default function ImportsPage() {
{tx("启动前清空历史题库", "Clear old problem set before start")}
</label>
)}
<button className="rounded border px-3 py-2 text-sm" onClick={() => void refresh()} disabled={loading}>
<button className="rounded border px-3 py-2 text-sm flex items-center gap-2" onClick={() => void refresh()} disabled={loading}>
<RefreshCw size={14} className={loading ? "animate-spin" : ""} />
{tx("刷新", "Refresh")}
</button>
<span className={`text-sm ${running ? "text-emerald-700" : "text-zinc-600"}`}>
<span className={`text-sm flex items-center gap-1 ${running ? "text-emerald-700" : "text-zinc-600"}`}>
{running ? <Activity size={14} className="animate-pulse" /> : <Server size={14} />}
{running ? tx("运行中", "Running") : tx("空闲", "Idle")}
</span>
</div>
@@ -500,6 +514,6 @@ export default function ImportsPage() {
</div>
</div>
</section>
</main>
</main >
);
}

查看文件

@@ -6,6 +6,16 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import {
Book,
Code2,
FileQuestion,
Library,
Map as MapIcon,
RefreshCw,
Shield,
Sword,
} from "lucide-react";
type Article = {
id: number;
@@ -198,13 +208,17 @@ export default function KbListPage() {
return (
<main className="mx-auto max-w-5xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<div className="flex flex-wrap items-center justify-between gap-3">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">{tx("学习知识库", "Knowledge Base")}</h1>
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<Library size={28} />
{tx("学习知识库", "Knowledge Base")}
</h1>
{canManageRefresh ? (
<button
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
className="rounded border px-3 py-1 text-sm disabled:opacity-50 flex items-center gap-2"
disabled={triggerLoading || refreshStatus?.running}
onClick={() => void triggerRefresh()}
>
<RefreshCw size={14} className={triggerLoading || refreshStatus?.running ? "animate-spin" : ""} />
{triggerLoading
? tx("更新触发中...", "Triggering...")
: refreshStatus?.running
@@ -232,11 +246,9 @@ export default function KbListPage() {
? tx("读取中...", "Loading...")
: refreshStatus?.running
? tx(`运行中(开始于 ${fmtTs(refreshStatus.last_started_at)}`, `Running (started at ${fmtTs(refreshStatus.last_started_at)})`)
: tx(`空闲(最近结束 ${fmtTs(refreshStatus?.last_finished_at ?? null)},退出码 ${
refreshStatus?.last_exit_code ?? "-"
}`, `Idle (last finished ${fmtTs(refreshStatus?.last_finished_at ?? null)}, exit code ${
refreshStatus?.last_exit_code ?? "-"
})`))}
: tx(`空闲(最近结束 ${fmtTs(refreshStatus?.last_finished_at ?? null)},退出码 ${refreshStatus?.last_exit_code ?? "-"
}`, `Idle (last finished ${fmtTs(refreshStatus?.last_finished_at ?? null)}, exit code ${refreshStatus?.last_exit_code ?? "-"
})`))}
</p>
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("加载中...", "Loading...")}</p>}
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
@@ -244,17 +256,20 @@ export default function KbListPage() {
<section className="mt-4 space-y-5">
{[
["roadmap", tx("学习总路线", "Learning Roadmap")],
["cpp", tx("C++ 基础", "C++ Fundamentals")],
["cspj", "CSP-J"],
["csps", "CSP-S"],
["other", tx("其他资料", "Other Resources")],
].map(([key, label]) => {
const group = grouped[key] ?? [];
["roadmap", tx("学习总路线", "Learning Roadmap"), MapIcon],
["cpp", tx("C++ 基础", "C++ Fundamentals"), Code2],
["cspj", "CSP-J", Sword],
["csps", "CSP-S", Shield],
["other", tx("其他资料", "Other Resources"), FileQuestion],
].map(([key, label, Icon]) => {
const group = grouped[key as string] ?? [];
if (!group.length) return null;
return (
<div key={key} className="space-y-3">
<h2 className="text-sm font-semibold text-zinc-700">{label}</h2>
<div key={key as string} className="space-y-3">
<h2 className="text-sm font-semibold text-zinc-700 flex items-center gap-2">
{Icon && <Icon size={18} />}
{label as string}
</h2>
{group.map((a) => (
<Link
key={a.slug}

查看文件

@@ -10,8 +10,8 @@ import "@/themes/minecraft/theme.css";
import "./globals.css";
export const metadata: Metadata = {
title: "CSP Online Learning & Contest Platform",
description: "Quests, Cursed Tome review, Raids, Knowledge Base, and C++ runner.",
title: "CSP Quest World - 8-bit Adventure",
description: "Join the 8-bit adventure! Solve algorithms, raid contests, and build your legend in the CSP Quest World.",
};
export default function RootLayout({

查看文件

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
import { useUiPreferences } from "@/components/ui-preference-provider";
import { Crown, Medal, Trophy, User, Calendar } from "lucide-react";
type Row = {
user_id: number;
@@ -50,10 +51,10 @@ export default function LeaderboardPage() {
const getRankIcon = (index: number) => {
if (!isMc) return `#${index + 1}`;
switch (index) {
case 0: return "🏆";
case 1: return "🥈";
case 2: return "🥉";
default: return `#${index + 1}`;
case 0: return <Trophy size={20} className="text-[color:var(--mc-gold)]" />;
case 1: return <Medal size={20} className="text-zinc-300" />;
case 2: return <Medal size={20} className="text-orange-700" />;
default: return <span className="font-mono">#{index + 1}</span>;
}
};
@@ -62,7 +63,7 @@ export default function LeaderboardPage() {
<h1 className={`text-xl font-bold max-[390px]:text-lg sm:text-2xl ${isMc ? "text-[color:var(--mc-diamond)] mc-text-shadow" : ""}`}>
{isMc ? (
<span className="flex items-center gap-2">
<span>🏰</span>
<Crown size={24} />
{tx("名人堂", "Hall of Fame")}
</span>
) : (
@@ -101,15 +102,25 @@ export default function LeaderboardPage() {
<thead className={`${isMc ? "bg-black/30 text-zinc-300" : "bg-zinc-100 text-left"}`}>
<tr>
<th className="px-3 py-2 text-left">{tx("排名", "Rank")}</th>
<th className="px-3 py-2 text-left">{tx("用户", "User")}</th>
<th className="px-3 py-2 text-left">
<div className="flex items-center gap-2">
{isMc && <User size={16} />}
{tx("用户", "User")}
</div>
</th>
<th className="px-3 py-2 text-left">Rating</th>
<th className="px-3 py-2 text-left">{tx("注册时间", "Registered At")}</th>
<th className="px-3 py-2 text-left">
<div className="flex items-center gap-2">
{isMc && <Calendar size={16} />}
{tx("注册时间", "Registered At")}
</div>
</th>
</tr>
</thead>
<tbody className={isMc ? "divide-y divide-zinc-700" : ""}>
{items.map((row, i) => (
<tr key={row.user_id} className={isMc ? "hover:bg-white/5 transition-colors" : "border-t"}>
<td className={`px-3 py-2 font-bold ${getRankColor(i)}`}>{getRankIcon(i)}</td>
<td className={`px-3 py-2 font-bold flex items-center justify-center ${getRankColor(i)}`}>{getRankIcon(i)}</td>
<td className={`px-3 py-2 font-medium ${getRankColor(i)}`}>{row.username}</td>
<td className="px-3 py-2 text-[color:var(--mc-emerald)]">{row.rating}</td>
<td className="px-3 py-2 text-zinc-500">

查看文件

@@ -1,6 +1,18 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import {
ArrowRightLeft,
Calendar,
CheckCircle2,
History,
IdCard,
RefreshCw,
ShoppingBag,
TrendingUp,
TrendingDown,
Zap,
} from "lucide-react";
import { PixelAvatar } from "@/components/pixel-avatar";
import { apiFetch, listRatingHistory, type RatingHistoryItem } from "@/lib/api";
@@ -243,17 +255,19 @@ export default function MePage() {
Level {Math.floor(profile.rating / 100)}
</span>
</div>
<p className="text-xs text-[color:var(--mc-stone-dark)]">UID: {profile.id}</p>
</div>
<div className="mt-4 space-y-2 border-t border-black/20 pt-4">
<div className="flex justify-between text-sm">
<span className="text-zinc-800">{tx("绿宝石 (Rating)", "Emeralds (Rating)")}</span>
<span className="font-bold text-[color:var(--mc-green)] text-shadow-sm">{profile.rating}</span>
<div className="flex justify-between text-sm items-center">
<span className="text-zinc-800 flex items-center gap-1">
<IdCard size={14} className="text-zinc-500" />
UID
</span>
<span className="text-zinc-600 font-mono">{profile.id}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-800">{tx("加入时间", "Joined")}</span>
<span className="text-zinc-600">{new Date(profile.created_at * 1000).toLocaleDateString()}</span>
<div className="flex justify-between text-sm items-center">
<span className="text-zinc-800 flex items-center gap-1">
<Calendar size={14} className="text-zinc-500" />
{tx("加入时间", "Joined")}
</span>
<span className="text-zinc-600 font-mono">{new Date(profile.created_at * 1000).toLocaleDateString()}</span>
</div>
</div>
</section>
@@ -271,13 +285,16 @@ export default function MePage() {
<div key={idx} className="bg-[color:var(--mc-surface-soft)] p-3 border-2 border-[color:var(--mc-stone-dark)] relative group hover:border-[color:var(--mc-stone)] transition-colors">
<div className="flex items-start gap-3">
<div className={`w-5 h-5 border-2 border-[color:var(--mc-stone-dark)] flex items-center justify-center bg-black/30 mt-0.5 ${task.completed ? 'bg-[color:var(--mc-green)]/20' : ''}`}>
{task.completed && <span className="text-[color:var(--mc-green)] text-sm"></span>}
{task.completed && <CheckCircle2 size={16} className="text-[color:var(--mc-green)]" />}
</div>
<div className="flex-1">
<div className="flex justify-between items-start mb-1">
<h3 className="text-[color:var(--mc-plank-light)] text-lg font-bold leading-tight">
<h3 className="text-[color:var(--mc-plank-light)] text-lg font-bold leading-tight flex items-center gap-2">
{task.title}
<span className="ml-2 text-[color:var(--mc-gold)] text-base font-minecraft">+{task.reward} XP</span>
<span className="ml-2 text-[color:var(--mc-gold)] text-base font-minecraft flex items-center gap-1">
<Zap size={14} />
+{task.reward} XP
</span>
</h3>
</div>
<p className="text-[color:var(--mc-stone)] text-base leading-snug">
@@ -299,18 +316,21 @@ export default function MePage() {
<div className="grid gap-2">
<div className="flex gap-2 text-black">
<select
className="flex-1 rounded-none border-2 border-black bg-[color:var(--surface)] px-2 py-1 text-base font-bold"
value={selectedItemId}
onChange={(e) => setSelectedItemId(Number(e.target.value))}
>
<option value={0}>{tx("选择战利品...", "Select loot...")}</option>
{items.map((item) => (
<option key={item.id} value={item.id}>
{itemName(item.name)}
</option>
))}
</select>
<div className="relative flex-1">
<ShoppingBag className="absolute left-2 top-2 text-black/50 pointer-events-none" size={16} />
<select
className="w-full rounded-none border-2 border-black bg-[color:var(--surface)] px-2 py-1 pl-8 text-base font-bold appearance-none"
value={selectedItemId}
onChange={(e) => setSelectedItemId(Number(e.target.value))}
>
<option value={0}>{tx("选择战利品...", "Select loot...")}</option>
{items.map((item) => (
<option key={item.id} value={item.id}>
{itemName(item.name)}
</option>
))}
</select>
</div>
<input
className="w-20 rounded-none border-2 border-black bg-[color:var(--surface)] px-2 py-1 text-base font-bold text-center"
type="number"
@@ -340,10 +360,11 @@ export default function MePage() {
<option value="studyday">{tx("工作日价格", "Workday Price")}</option>
</select>
<button
className="mc-btn mc-btn-success text-xs px-4"
className="mc-btn mc-btn-success text-xs px-4 flex items-center gap-2"
onClick={() => void redeem()}
disabled={redeemLoading || !selectedItemId}
>
<ArrowRightLeft size={14} />
{tx("交易", "Trade")}
</button>
</div>
@@ -355,12 +376,16 @@ export default function MePage() {
{/* Rating History Section */}
<section className="mt-4 rounded-none border-[3px] border-black bg-[color:var(--mc-surface)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<h2 className="text-base font-bold text-black mb-2">{tx("积分变动记录", "Rating History")}</h2>
<h2 className="text-base font-bold text-black mb-2 flex items-center gap-2">
<History size={18} />
{tx("积分变动记录", "Rating History")}
</h2>
<div className="max-h-60 overflow-y-auto space-y-1">
{historyItems.map((item, idx) => (
<div key={idx} className="flex justify-between text-xs text-zinc-800 border-b border-zinc-200 pb-1">
<span>
<span className={`font-bold ${item.change > 0 ? 'text-[color:var(--mc-green)]' : 'text-[color:var(--mc-red)]'}`}>
<span className={`font-bold flex items-center gap-1 ${item.change > 0 ? 'text-[color:var(--mc-green)]' : 'text-[color:var(--mc-red)]'}`}>
{item.change > 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
{item.change > 0 ? `+${item.change}` : item.change}
</span>
<span className="ml-2">{item.note}</span>
@@ -381,10 +406,11 @@ export default function MePage() {
<div className="flex items-center justify-between gap-2 mb-2">
<h2 className="text-base font-bold text-black">{tx("交易记录", "Trade History")}</h2>
<button
className="text-xs text-[color:var(--mc-stone-dark)] underline"
className="text-xs text-[color:var(--mc-stone-dark)] underline flex items-center gap-1 hover:text-black"
onClick={() => void loadAll()}
disabled={loading}
>
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
{tx("刷新", "Refresh")}
</button>
</div>

查看文件

@@ -0,0 +1,43 @@
"use client";
import Link from "next/link";
import { useUiPreferences } from "@/components/ui-preference-provider";
import { useI18nText } from "@/lib/i18n";
import { AlertTriangle } from "lucide-react";
export default function NotFound() {
const { theme } = useUiPreferences();
const { tx } = useI18nText();
const isMc = theme === "minecraft";
return (
<div className={`flex min-h-[80vh] flex-col items-center justify-center p-4 text-center ${isMc ? "text-white" : ""}`}>
{isMc ? (
<div className="space-y-6">
<div className="text-6xl animate-bounce">👾</div>
<h1 className="text-4xl font-bold text-[#AA0000] mc-text-shadow">
{tx("这里是虚空...", "THE VOID")}
</h1>
<p className="text-xl text-zinc-400">
{tx("你来到了世界的尽头。", "You have reached the end of the world.")}
</p>
<div className="pt-4">
<Link href="/" className="mc-btn bg-[color:var(--mc-wood)] text-white px-8 py-3 text-lg inline-flex items-center gap-2">
<span>🏠</span>
{tx("重生 (返回首页)", "Respawn (Home)")}
</Link>
</div>
</div>
) : (
<div className="space-y-4">
<AlertTriangle size={64} className="mx-auto text-zinc-300" />
<h1 className="text-2xl font-bold text-zinc-800">404 - Page Not Found</h1>
<p className="text-zinc-600">The page you are looking for does not exist.</p>
<Link href="/" className="inline-block rounded bg-zinc-900 px-6 py-2 text-white hover:bg-zinc-700">
Back to Home
</Link>
</div>
)}
</div>
);
}

查看文件

@@ -3,6 +3,20 @@
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Book,
ChevronLeft,
ChevronRight,
Globe,
Search,
Shield,
Sword,
Tag,
Trophy,
Filter,
ArrowUpDown
} from "lucide-react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
@@ -87,6 +101,7 @@ const QUICK_CARDS = [
titleEn: "CSP-J Trials",
descZh: "普及组入门任务",
descEn: "Junior Tier Quests",
icon: Sword,
},
{
presetKey: "csp-s",
@@ -94,6 +109,7 @@ const QUICK_CARDS = [
titleEn: "CSP-S Challenges",
descZh: "提高组进阶任务",
descEn: "Senior Tier Quests",
icon: Shield,
},
{
presetKey: "noip-junior",
@@ -101,6 +117,7 @@ const QUICK_CARDS = [
titleEn: "NOIP Basics",
descZh: "算法与思维",
descEn: "Algorithm & Logic",
icon: Book,
},
] as const;
@@ -269,35 +286,43 @@ export default function ProblemsPage() {
<button
key={card.presetKey}
type="button"
className={`rounded-xl border px-4 py-3 text-left transition ${active
? "bg-[color:var(--mc-grass-dark)] text-white"
: "bg-[color:var(--mc-plank)] text-black hover:bg-[color:var(--mc-plank-light)]"
className={`rounded-xl border px-4 py-3 text-left transition flex items-center gap-4 ${active
? "bg-[color:var(--mc-grass-dark)] text-white"
: "bg-[color:var(--mc-plank)] text-black hover:bg-[color:var(--mc-plank-light)]"
}`}
onClick={() => selectPreset(card.presetKey)}
>
<p className="text-base font-bold mc-text-shadow-sm">{isZh ? card.titleZh : card.titleEn}</p>
<p className={`mt-1 text-xs ${active ? "text-zinc-100" : "text-zinc-800"}`}>
{isZh ? card.descZh : card.descEn}
</p>
<div className={`p-2 border-2 border-black bg-black/10 rounded-sm ${active ? "bg-white/20" : ""}`}>
<card.icon size={24} />
</div>
<div>
<p className="text-base font-bold mc-text-shadow-sm">{isZh ? card.titleZh : card.titleEn}</p>
<p className={`mt-1 text-xs ${active ? "text-zinc-100" : "text-zinc-800"}`}>
{isZh ? card.descZh : card.descEn}
</p>
</div>
</button>
);
})}
</section>
<section className="mt-4 grid gap-3 rounded-xl border bg-[color:var(--mc-stone-dark)] p-4 md:grid-cols-2 lg:grid-cols-6 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<select
className="rounded-none border-2 border-black bg-[color:var(--surface)] text-white px-3 py-2 text-sm"
value={presetKey}
onChange={(e) => {
selectPreset(e.target.value);
}}
>
{PRESETS.map((item) => (
<option key={item.key} value={item.key}>
{isZh ? item.labelZh : item.labelEn}
</option>
))}
</select>
<div className="relative">
<Filter className="absolute left-2 top-2.5 h-4 w-4 text-zinc-400" />
<select
className="w-full rounded-none border-2 border-black bg-[color:var(--surface)] text-white pl-8 pr-3 py-2 text-sm appearance-none"
value={presetKey}
onChange={(e) => {
selectPreset(e.target.value);
}}
>
{PRESETS.map((item) => (
<option key={item.key} value={item.key}>
{isZh ? item.labelZh : item.labelEn}
</option>
))}
</select>
</div>
<input
className="rounded-none border-2 border-black bg-[color:var(--surface)] text-white px-3 py-2 text-sm lg:col-span-2"
@@ -343,10 +368,11 @@ export default function ProblemsPage() {
</select>
<button
className="mc-btn mc-btn-primary"
className="mc-btn mc-btn-primary flex items-center justify-center gap-2"
onClick={applySearch}
disabled={loading}
>
<Search size={16} />
{loading ? tx("加载中...", "Loading...") : tx("搜索", "Search")}
</button>
</section>
@@ -422,13 +448,17 @@ export default function ProblemsPage() {
<div className="flex flex-wrap gap-1">
{tags.length === 0 && <span className="text-[color:var(--mc-stone-dark)]">-</span>}
{tags.map((tag) => (
<span key={tag} className="border border-black bg-[color:var(--mc-stone-dark)] px-2 py-0.5 text-xs text-white">
<span key={tag} className="border border-black bg-[color:var(--mc-stone-dark)] px-2 py-0.5 text-xs text-white flex items-center gap-1">
<Tag size={10} />
{tag}
</span>
))}
</div>
</td>
<td className="px-3 py-2 text-[color:var(--mc-stone)]">{problem.source || "-"}</td>
<td className="px-3 py-2 text-[color:var(--mc-stone)] flex items-center gap-1">
<Globe size={12} />
{problem.source || "-"}
</td>
</tr>
);
})}
@@ -450,21 +480,21 @@ export default function ProblemsPage() {
<div className="mt-4 flex flex-col gap-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-2">
<button
className="mc-btn"
className="mc-btn px-2"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={loading || page <= 1}
>
{tx("上一页", "Prev")}
<ChevronLeft size={16} />
</button>
<span className="text-[color:var(--mc-diamond)] font-bold">
{isZh ? `${page} / ${totalPages}` : `Page ${page} / ${totalPages}`}
</span>
<button
className="mc-btn"
className="mc-btn px-2"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={loading || page >= totalPages}
>
{tx("下一页", "Next")}
<ChevronRight size={16} />
</button>
</div>

查看文件

@@ -4,6 +4,7 @@ import { useState } from "react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
import { AlertTriangle, Code2, Monitor, Play, Terminal, Timer } from "lucide-react";
type RunResult = {
status: string;
@@ -50,13 +51,17 @@ export default function RunPage() {
return (
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl flex items-center gap-2">
<Terminal size={24} />
{tx("在线 C++ 编写 / 编译 / 运行", "Online C++ Editor / Compile / Run")}
</h1>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
<section className="rounded-xl border bg-white p-4">
<h2 className="text-sm font-medium">{tx("代码", "Code")}</h2>
<h2 className="text-sm font-medium flex items-center gap-2">
<Code2 size={16} />
{tx("代码", "Code")}
</h2>
<textarea
className="mt-2 h-72 w-full rounded border p-3 font-mono text-sm sm:h-[420px]"
value={code}
@@ -77,24 +82,42 @@ export default function RunPage() {
onClick={() => void run()}
disabled={loading}
>
{loading ? tx("运行中...", "Running...") : tx("运行", "Run")}
{loading ? (
<span className="flex items-center justify-center gap-2">
<Play size={16} className="animate-spin" />
{tx("运行中...", "Running...")}
</span>
) : (
<span className="flex items-center justify-center gap-2">
<Play size={16} />
{tx("运行", "Run")}
</span>
)}
</button>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
{result && (
<div className="mt-4 space-y-3 text-sm">
<p>
{tx("状态", "Status")}: <b>{result.status}</b> · {tx("耗时", "Time")}: {result.time_ms}ms
<p className="flex items-center gap-2">
{tx("状态", "Status")}: <b>{result.status}</b> ·
<Timer size={14} />
{tx("耗时", "Time")}: {result.time_ms}ms
</p>
<div>
<h3 className="font-medium">stdout</h3>
<h3 className="font-medium flex items-center gap-1">
<Monitor size={14} />
stdout
</h3>
<pre className="mt-1 overflow-auto rounded bg-zinc-900 p-3 text-xs text-zinc-100">
{result.stdout || "(empty)"}
</pre>
</div>
<div>
<h3 className="font-medium">stderr</h3>
<h3 className="font-medium flex items-center gap-1 text-red-500">
<AlertTriangle size={14} />
stderr
</h3>
<pre className="mt-1 overflow-auto rounded bg-zinc-900 p-3 text-xs text-zinc-100">
{result.stderr || "(empty)"}
</pre>

查看文件

@@ -6,6 +6,21 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
import { useUiPreferences } from "@/components/ui-preference-provider";
import {
AlertTriangle,
Check,
Clock,
FileText,
Filter,
Search,
Timer,
Trophy,
User,
Wrench,
X,
Zap,
Scroll
} from "lucide-react";
type Submission = {
id: number;
@@ -46,20 +61,27 @@ export default function SubmissionsPage() {
/** Map raw status codes to themed display text */
const statusLabel = (raw: string) => {
if (!isMc) return raw;
const map: Record<string, string> = {
Accepted: "✅ " + tx("通过", "Accepted"),
AC: "AC",
WA: "❌ WA",
"Wrong Answer": "❌ " + tx("答案错误", "Wrong Answer"),
TLE: "⏰ TLE",
"Time Limit Exceeded": "⏰ " + tx("超时", "TLE"),
MLE: "💾 MLE",
RE: "💥 RE",
"Runtime Error": "💥 " + tx("运行错误", "RE"),
CE: "🔧 CE",
"Compile Error": "🔧 " + tx("编译错误", "CE"),
};
return map[raw] ?? raw;
switch (raw) {
case "Accepted":
case "AC":
return <span className="flex items-center gap-1 text-[color:var(--mc-green)]"><Check size={14} /> AC</span>;
case "WA":
case "Wrong Answer":
return <span className="flex items-center gap-1 text-[color:var(--mc-red)]"><X size={14} /> WA</span>;
case "TLE":
case "Time Limit Exceeded":
return <span className="flex items-center gap-1 text-[color:var(--mc-gold)]"><Clock size={14} /> TLE</span>;
case "MLE":
return <span className="flex items-center gap-1 text-[color:var(--mc-red)]"><Zap size={14} /> MLE</span>;
case "RE":
case "Runtime Error":
return <span className="flex items-center gap-1 text-orange-500"><AlertTriangle size={14} /> RE</span>;
case "CE":
case "Compile Error":
return <span className="flex items-center gap-1 text-zinc-500"><Wrench size={14} /> CE</span>;
default:
return raw;
}
};
const load = async () => {
@@ -89,8 +111,8 @@ export default function SubmissionsPage() {
<h1 className={`text-xl font-bold max-[390px]:text-lg sm:text-2xl ${isMc ? "text-[color:var(--mc-diamond)] mc-text-shadow" : ""}`}>
{isMc ? (
<span className="flex items-center gap-2">
<span>📜</span>
{tx("施法记录", "Spell Cast Log")}
<Scroll size={24} />
<span>{tx("施法记录", "Spell Cast Log")}</span>
</span>
) : (
tx("提交记录", "Submissions")
@@ -126,17 +148,25 @@ export default function SubmissionsPage() {
onChange={(e) => setContestId(e.target.value)}
/>
<button
className={`px-4 py-2 disabled:opacity-50 ${isMc
className={`px-4 py-2 disabled:opacity-50 flex items-center justify-center gap-2 ${isMc
? "mc-btn"
: "rounded bg-zinc-900 text-white"}`}
onClick={() => void load()}
disabled={loading}
>
{loading
? tx("搜索中...", "Searching...")
: isMc
? tx("🔍 搜索记录", "🔍 Search Logs")
: tx("筛选", "Filter")}
{loading ? (
<span className="flex items-center gap-2">
<Filter size={16} className="animate-spin" />
{tx("搜索中...", "Searching...")}
</span>
) : isMc ? (
<span className="flex items-center gap-2">
<Search size={16} />
{tx("搜索记录", "Search Logs")}
</span>
) : (
tx("筛选", "Filter")
)}
</button>
</div>
@@ -180,12 +210,37 @@ export default function SubmissionsPage() {
<thead className={isMc ? "bg-black/30 text-zinc-300 text-left" : "bg-zinc-100 text-left"}>
<tr>
<th className="px-3 py-2">ID</th>
<th className="px-3 py-2">{isMc ? tx("冒险者", "Player") : tx("用户", "User")}</th>
<th className="px-3 py-2">{tx("任务", "Quest")}</th>
<th className="px-3 py-2">{tx("状态", "Status")}</th>
<th className="px-3 py-2">{tx("分数", "Score")}</th>
<th className="px-3 py-2">
<div className="flex items-center gap-1">
{isMc && <User size={14} />}
{isMc ? tx("冒险者", "Player") : tx("用户", "User")}
</div>
</th>
<th className="px-3 py-2">
<div className="flex items-center gap-1">
{isMc && <FileText size={14} />}
{tx("任务", "Quest")}
</div>
</th>
<th className="px-3 py-2">
<div className="flex items-center gap-1">
{isMc && <Check size={14} />}
{tx("状态", "Status")}
</div>
</th>
<th className="px-3 py-2">
<div className="flex items-center gap-1">
{isMc && <Trophy size={14} />}
{tx("分数", "Score")}
</div>
</th>
<th className="px-3 py-2">{isMc ? tx("绿宝石 Δ", "Emerald Δ") : tx("Rating 变化", "Rating Delta")}</th>
<th className="px-3 py-2">{tx("耗时(ms)", "Time(ms)")}</th>
<th className="px-3 py-2">
<div className="flex items-center gap-1">
{isMc && <Timer size={14} />}
{tx("耗时(ms)", "Time(ms)")}
</div>
</th>
<th className="px-3 py-2">{tx("详情", "Detail")}</th>
</tr>
</thead>

查看文件

@@ -7,6 +7,7 @@ import { apiFetch } from "@/lib/api";
import { readToken } from "@/lib/auth";
import { useI18nText } from "@/lib/i18n";
import { useUiPreferences } from "@/components/ui-preference-provider";
import { BookX, Trash2, RefreshCw, RotateCcw, Save, Search, Skull } from "lucide-react";
type WrongBookItem = {
user_id: number;
@@ -80,11 +81,14 @@ export default function WrongBookPage() {
<h1 className={`text-xl font-bold max-[390px]:text-lg sm:text-2xl ${isMc ? "text-[color:var(--mc-diamond)] mc-text-shadow" : ""}`}>
{isMc ? (
<span className="flex items-center gap-2">
<span>📜</span>
<Skull size={24} />
{tx("诅咒卷轴", "Cursed Scrolls")}
</span>
) : (
tx("错题本", "Wrong Book")
<span className="flex items-center gap-2">
<BookX size={24} />
{tx("错题本", "Wrong Book")}
</span>
)}
</h1>
<p className={`mt-2 text-sm ${isMc ? "text-zinc-400" : "text-zinc-600"}`}>
@@ -102,9 +106,19 @@ export default function WrongBookPage() {
disabled={loading}
>
{loading
? tx("搜索中...", "Searching...")
? isMc ? (
<span className="flex items-center gap-2">
<Search size={16} className="animate-spin" />
{tx("搜索中...", "Searching...")}
</span>
) : tx("搜索中...", "Searching...")
: isMc
? tx("🔍 重新搜索", "🔍 Search Again")
? (
<span className="flex items-center gap-2">
<RefreshCw size={16} />
{tx("重新搜索", "Search Again")}
</span>
)
: tx("刷新", "Refresh")}
</button>
</div>
@@ -133,7 +147,12 @@ export default function WrongBookPage() {
: "hover:bg-zinc-100"}`}
href={`/problems/${item.problem_id}`}
>
{isMc ? tx("⚔️ 重新挑战", "⚔️ Retry Quest") : tx("查看任务", "View Quest")}
{isMc ? (
<span className="flex items-center gap-1">
<RotateCcw size={12} />
{tx("重新挑战", "Retry Quest")}
</span>
) : tx("查看任务", "View Quest")}
</Link>
{item.last_submission_id && (
<Link
@@ -160,7 +179,12 @@ export default function WrongBookPage() {
: "hover:bg-zinc-100"}`}
onClick={() => void removeItem(item.problem_id)}
>
{isMc ? tx("🗑️ 移除诅咒", "🗑️ Remove Curse") : tx("移除", "Remove")}
{isMc ? (
<span className="flex items-center gap-1">
<Trash2 size={14} />
{tx("移除诅咒", "Remove Curse")}
</span>
) : tx("移除", "Remove")}
</button>
</div>
@@ -185,7 +209,12 @@ export default function WrongBookPage() {
: "hover:bg-zinc-100"}`}
onClick={() => void updateNote(item.problem_id, item.note)}
>
{isMc ? tx("💾 保存笔记", "💾 Save Notes") : tx("保存备注", "Save Note")}
{isMc ? (
<span className="flex items-center gap-1">
<Save size={12} />
{tx("保存笔记", "Save Notes")}
</span>
) : tx("保存备注", "Save Note")}
</button>
</div>
))}