feat: Minecraft theme overhaul, fix points bug, add history

这个提交包含在:
X
2026-02-15 09:41:54 -08:00
父节点 37266bb846
当前提交 ef6d71ef54
修改 28 个文件,包含 1821 行新增1053 行删除

查看文件

@@ -13,9 +13,9 @@ type AuthErr = { ok: false; error: string };
type AuthResp = AuthOk | AuthErr;
function passwordScore(password: string, isZh: boolean): { label: string; color: string } {
if (password.length >= 12) return { label: isZh ? "" : "Strong", color: "text-emerald-600" };
if (password.length >= 8) return { label: isZh ? "" : "Medium", color: "text-blue-600" };
return { label: isZh ? "" : "Weak", color: "text-orange-600" };
if (password.length >= 12) return { label: isZh ? "钻石级" : "Diamond Tier", color: "text-[color:var(--mc-diamond)]" };
if (password.length >= 8) return { label: isZh ? "铁级" : "Iron Tier", color: "text-zinc-400" };
return { label: isZh ? "木级" : "Wood Tier", color: "text-[color:var(--mc-wood)]" };
}
export default function AuthPage() {
@@ -83,132 +83,136 @@ export default function AuthPage() {
return (
<main className="mx-auto max-w-4xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-10">
<div className="grid gap-6 md:grid-cols-[1.1fr,1fr]">
<section className="rounded-2xl border bg-zinc-900 p-6 text-zinc-100">
<h1 className="text-2xl font-semibold">{tx("欢迎回来,开始刷题", "Welcome back, let's practice")}</h1>
<p className="mt-3 text-sm text-zinc-300">
{tx("登录后可提交评测、保存草稿、查看错题本和个人进度。", "After sign-in you can submit, save drafts, review wrong-book, and track your progress.")}
<section className="rounded-none border-[3px] border-black bg-[color:var(--mc-obsidian)] p-6 text-zinc-100 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<h1 className="text-2xl font-bold text-[color:var(--mc-diamond)] mc-text-shadow leading-relaxed">
{tx("欢迎回来,冒险者!", "Welcome Back, Adventurer!")}
</h1>
<p className="mt-3 text-sm text-[color:var(--mc-stone)] font-mono">
{tx("登录服务器以访问任务布告栏、保存冒险进度、查看错题卷轴和个人成就。", "Login to access Quest Board, save Game Progress, review Grimoire, and track Achievements.")}
</p>
<div className="mt-6 space-y-2 text-sm text-zinc-300">
<p>{tx("• 题库按 CSP-J / CSP-S / NOIP 入门组织", "• Problem sets are organized by CSP-J / CSP-S / NOIP junior")}</p>
<p>{tx("• 题目页支持本地草稿与试运行", "• Problem page supports local draft and run")}</p>
<p>{tx("• 生成式题解异步入库,支持多解法", "• Generated solutions are queued asynchronously with multiple methods")}</p>
<div className="mt-6 space-y-2 text-sm text-[color:var(--mc-stone)] font-mono">
<p>{tx("• 任务按 CSP-J / CSP-S / NOIP 难度分级", "• Quests organized by CSP-J / CSP-S / NOIP Tiers")}</p>
<p>{tx("• 任务卷轴支持本地草稿与试运行", "• Quest Scrolls support local drafting and trial runs")}</p>
<p>{tx("• 先知题解异步生成,包含多种解法", "• Oracles provide asynchronous wisdom with multiple paths")}</p>
</div>
<p className="mt-6 text-xs text-zinc-400">
API Base: <span className="font-mono">{apiBase}</span>
<p className="mt-6 text-xs text-[color:var(--mc-stone-dark)]">
Server API: <span className="font-mono text-[color:var(--mc-red)]">{apiBase}</span>
</p>
</section>
<section className="rounded-2xl border bg-white p-6">
<div className="grid grid-cols-2 gap-2 rounded-lg bg-zinc-100 p-1 text-sm">
<section className="rounded-none border-[3px] border-black bg-[color:var(--surface)] p-6 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<div className="grid grid-cols-2 gap-2 rounded-none bg-black/20 p-1 text-sm">
<button
type="button"
className={`rounded-md px-3 py-2 ${
mode === "login" ? "bg-white shadow-sm" : "text-zinc-600"
}`}
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"
}`}
onClick={() => {
setMode("login");
setResp(null);
}}
disabled={loading}
>
{tx("登录", "Sign In")}
{tx("登录服务器", "Login")}
</button>
<button
type="button"
className={`rounded-md px-3 py-2 ${
mode === "register" ? "bg-white shadow-sm" : "text-zinc-600"
}`}
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"
}`}
onClick={() => {
setMode("register");
setResp(null);
}}
disabled={loading}
>
{tx("注册", "Register")}
{tx("新玩家注册", "New Player")}
</button>
</div>
<div className="mt-5 space-y-4">
<div className="mt-5 space-y-4 font-mono">
<div>
<label className="text-sm font-medium">{tx("用户名", "Username")}</label>
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("玩家代号", "Username")}</label>
<input
className="mt-1 w-full rounded-lg border px-3 py-2"
className="mt-1 w-full"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={tx("例如:csp_student", "e.g. csp_student")}
placeholder={tx("例如:Steve", "e.g. Steve")}
/>
{usernameErr && <p className="mt-1 text-xs text-red-600">{usernameErr}</p>}
{usernameErr && <p className="mt-1 text-xs text-[color:var(--mc-red)]">{usernameErr}</p>}
</div>
<div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium">{tx("密码", "Password")}</label>
<span className={`text-xs ${strength.color}`}>{tx("强度", "Strength")}: {strength.label}</span>
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("极其机密的口令", "Secret Password")}</label>
<span className={`text-xs ${strength.color}`}>{strength.label}</span>
</div>
<input
type={showPassword ? "text" : "password"}
className="mt-1 w-full rounded-lg border px-3 py-2"
className="mt-1 w-full"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={tx("至少 6 ", "At least 6 chars")}
placeholder={tx("至少 6 个字符", "Min 6 chars")}
/>
{passwordErr && <p className="mt-1 text-xs text-red-600">{passwordErr}</p>}
{passwordErr && <p className="mt-1 text-xs text-[color:var(--mc-red)]">{passwordErr}</p>}
</div>
{mode === "register" && (
<div>
<label className="text-sm font-medium">{tx("确认密码", "Confirm Password")}</label>
<label className="text-sm font-bold text-[color:var(--mc-stone)]">{tx("确认口令", "Confirm Secret")}</label>
<input
type={showPassword ? "text" : "password"}
className="mt-1 w-full rounded-lg border px-3 py-2"
className="mt-1 w-full"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder={tx("再输入一次密码", "Enter password again")}
placeholder={tx("再输入口令", "Re-enter secret")}
/>
{confirmErr && <p className="mt-1 text-xs text-red-600">{confirmErr}</p>}
{confirmErr && <p className="mt-1 text-xs text-[color:var(--mc-red)]">{confirmErr}</p>}
</div>
)}
<label className="flex items-center gap-2 text-xs text-zinc-600">
<label className="flex items-center gap-2 text-xs text-[color:var(--mc-stone)] cursor-pointer select-none">
<input
type="checkbox"
checked={showPassword}
onChange={(e) => setShowPassword(e.target.checked)}
className="accent-[color:var(--mc-wood)]"
/>
{tx("显示密码", "Show password")}
{tx("显示口令", "Reveal Secret")}
</label>
<button
className="w-full rounded-lg bg-zinc-900 px-4 py-2 text-white hover:bg-zinc-800 disabled:opacity-50"
className={`w-full mc-btn ${mode === "register" ? "mc-btn-success" : ""}`}
onClick={() => void submit()}
disabled={!canSubmit}
>
{loading ? tx("提交中...", "Submitting...") : mode === "register" ? tx("注册并登录", "Register & Sign In") : tx("登录", "Sign In")}
{loading ? tx("连接中...", "Connecting...") : mode === "register" ? tx("创建档案并连接", "Create & Connect") : tx("连接服务器", "Connect")}
</button>
</div>
{resp && (
<div
className={`mt-4 rounded-lg border px-3 py-2 text-sm ${
resp.ok ? "border-emerald-300 bg-emerald-50 text-emerald-700" : "border-red-300 bg-red-50 text-red-700"
}`}
className={`mt-4 border-[2px] border-black px-3 py-2 text-sm shadow-[2px_2px_0_rgba(0,0,0,0.4)] ${resp.ok ? "bg-[color:var(--mc-grass-dark)] text-white" : "bg-[color:var(--mc-red)] text-white"
}`}
>
{resp.ok
? tx("登录成功正在跳转到题库...", "Signed in. Redirecting to problem set...")
: `${tx("操作失败:", "Action failed: ")}${resp.error}`}
? tx("连接成功正在传送至出生点...", "Connection Established! Teleporting to Spawn Point...")
: `${tx("连接失败:", "Connection Failed: ")}${resp.error}`}
</div>
)}
<p className="mt-4 text-xs text-zinc-500">
{tx("登录后 Token 自动保存在浏览器 localStorage,可直接前往", "Token is stored in browser localStorage after sign-in. You can go to")}
<Link className="mx-1 underline" href="/problems">
{tx("题库", "Problems")}
<p className="mt-4 text-xs text-[color:var(--mc-stone-dark)]">
{tx("令牌将保存在客户端存储中,可直接前往", "Token stored in client. Warp to")}
<Link className="mx-1 underline text-[color:var(--mc-diamond)] hover:text-[color:var(--mc-gold)]" href="/problems">
{tx("任务板", "Quest Board")}
</Link>
{tx("与", "and")}
<Link className="mx-1 underline" href="/me">
{tx("我的", "My Account")}
{tx("与", "or")}
<Link className="mx-1 underline text-[color:var(--mc-diamond)] hover:text-[color:var(--mc-gold)]" href="/me">
{tx("角色面板", "Character Sheet")}
</Link>
{tx("页面。", ".")}
{tx("。", ".")}
</p>
</section>
</div>

查看文件

@@ -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";
type Contest = {
id: number;
@@ -16,6 +17,8 @@ type Contest = {
export default function ContestsPage() {
const { tx } = useI18nText();
const { theme } = useUiPreferences();
const isMc = theme === "minecraft";
const [items, setItems] = useState<Contest[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
@@ -37,11 +40,18 @@ export default function ContestsPage() {
}, []);
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">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
{tx("模拟竞赛", "Contests")}
<main className="mx-auto max-w-5xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<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("突袭公告板", "Raid Board")}
</span>
) : (
tx("模拟竞赛", "Contests")
)}
</h1>
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("加载中...", "Loading...")}</p>}
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("正在寻找突袭目标...", "Scouting for raids...")}</p>}
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
<div className="mt-4 space-y-3">
@@ -49,13 +59,35 @@ export default function ContestsPage() {
<Link
key={c.id}
href={`/contests/${c.id}`}
className="block rounded-xl border bg-white p-4 hover:border-zinc-400"
className={`block rounded-xl border p-4 transition-transform active:scale-[0.99] ${isMc
? "bg-[color:var(--mc-stone-dark)] border-[3px] border-black shadow-[4px_4px_0_rgba(0,0,0,0.5)] text-white hover:border-white"
: "bg-white border-zinc-200 hover:border-zinc-400"
}`}
>
<h2 className="text-lg font-medium">{c.title}</h2>
<p className="mt-1 text-xs text-zinc-500">{tx("开始", "Start")}: {new Date(c.starts_at * 1000).toLocaleString()}</p>
<p className="text-xs text-zinc-500">{tx("结束", "End")}: {new Date(c.ends_at * 1000).toLocaleString()}</p>
<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>}
{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>
</div>
{isMc && (
<div className="hidden sm:block">
<span className="mc-btn px-3 py-1 text-xs">{tx("加入突袭", "Join Raid")}</span>
</div>
)}
</div>
</Link>
))}
{!loading && items.length === 0 && (
<div className={`p-8 text-center border-2 border-dashed ${isMc ? "border-zinc-700 text-zinc-500 bg-black/20" : "border-zinc-200 text-zinc-500"}`}>
<p>{tx("暂无比赛", "No raids active")}</p>
</div>
)}
</div>
</main>
);

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 25 KiB

之后

宽度:  |  高度:  |  大小: 19 KiB

查看文件

@@ -45,9 +45,47 @@ body {
}
@media print {
/* ── Force clean whitebackground "default" style for printing ── */
:root {
--background: #fff !important;
--foreground: #171717 !important;
--surface: #fff !important;
--surface-soft: #f4f4f5 !important;
--border: #d4d4d8 !important;
/* Override Minecraftspecific variables */
--mc-obsidian: transparent !important;
--mc-stone: #555 !important;
--mc-stone-dark: #333 !important;
--mc-plank: #f4f4f5 !important;
--mc-plank-light: #333 !important;
--mc-diamond: #111 !important;
--mc-gold: #111 !important;
--mc-red: #dc2626 !important;
--mc-green: #16a34a !important;
--mc-grass-top: #16a34a !important;
}
body {
background: #fff !important;
color: #000 !important;
font-family: Arial, Helvetica, sans-serif !important;
}
/* Reset all Minecraft fonts back to default for print */
* {
font-family: inherit !important;
text-shadow: none !important;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Arial, Helvetica, sans-serif !important;
color: #111 !important;
}
.print-hidden {
@@ -58,6 +96,13 @@ body {
display: block !important;
}
/* Hide nav, mobile tab bar, XP bar, and other chrome */
header,
.mobile-tab-bar,
.xp-bar-container {
display: none !important;
}
.problem-detail-grid {
display: block !important;
}
@@ -73,4 +118,47 @@ body {
background: #f4f4f5 !important;
color: #111827 !important;
}
}
/* Reset Minecraftstyled buttons, borders, and shadows */
.mc-btn,
[class*="mc-btn"] {
display: none !important;
}
/* Remove all themed backgrounds / borders / shadows */
main,
section,
div {
background-image: none !important;
box-shadow: none !important;
}
/* Ensure tables and links are readable */
a {
color: #111 !important;
text-decoration: underline !important;
}
table,
th,
td {
border-color: #d4d4d8 !important;
color: #111 !important;
}
/* Override markdown rendering backgrounds */
.problem-markdown {
color: #000 !important;
}
.problem-markdown pre {
background: #f4f4f5 !important;
color: #111 !important;
border: 1px solid #d4d4d8 !important;
}
.problem-markdown code {
background: #f4f4f5 !important;
color: #111 !important;
}
}

查看文件

@@ -11,7 +11,7 @@ import "./globals.css";
export const metadata: Metadata = {
title: "CSP Online Learning & Contest Platform",
description: "Problems, wrong-book review, contests, knowledge base, and C++ runner.",
description: "Quests, Cursed Tome review, Raids, Knowledge Base, and C++ runner.",
};
export default function RootLayout({

查看文件

@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { useI18nText } from "@/lib/i18n";
import { useUiPreferences } from "@/components/ui-preference-provider";
type Row = {
user_id: number;
@@ -14,6 +15,8 @@ type Row = {
export default function LeaderboardPage() {
const { tx } = useI18nText();
const { theme } = useUiPreferences();
const isMc = theme === "minecraft";
const [items, setItems] = useState<Row[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
@@ -34,22 +37,52 @@ export default function LeaderboardPage() {
void load();
}, []);
const getRankColor = (index: number) => {
if (!isMc) return "";
switch (index) {
case 0: return "text-[color:var(--mc-gold)] drop-shadow-sm"; // Gold
case 1: return "text-zinc-300"; // Iron
case 2: return "text-orange-700"; // Copper
default: return "text-zinc-400";
}
};
const getRankIcon = (index: number) => {
if (!isMc) return `#${index + 1}`;
switch (index) {
case 0: return "🏆";
case 1: return "🥈";
case 2: return "🥉";
default: return `#${index + 1}`;
}
};
return (
<main className="mx-auto max-w-4xl 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">
{tx("全站排行榜", "Global Leaderboard")}
<main className="mx-auto max-w-4xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<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("名人堂", "Hall of Fame")}
</span>
) : (
tx("全站排行榜", "Global Leaderboard")
)}
</h1>
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("加载中...", "Loading...")}</p>}
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("正在读取卷轴...", "Reading scrolls...")}</p>}
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
<div className="mt-4 rounded-xl border bg-white">
<div className={`mt-4 rounded-xl border ${isMc ? "border-[3px] border-black bg-[color:var(--mc-deep-slate)] shadow-[4px_4px_0_rgba(0,0,0,0.5)] text-white" : "bg-white border-zinc-200"}`}>
<div className="divide-y md:hidden">
{items.map((row, i) => (
<article key={row.user_id} className="space-y-1 p-3 text-sm">
<p className="font-medium">
#{i + 1} · {row.username}
</p>
<p className="text-xs text-zinc-600">Rating: {row.rating}</p>
<article key={row.user_id} className={`space-y-1 p-3 text-sm ${isMc ? "border-zinc-700" : ""}`}>
<div className="flex items-center justify-between">
<p className={`font-medium ${getRankColor(i)}`}>
<span className="mr-2 text-lg">{getRankIcon(i)}</span>
{row.username}
</p>
<span className="text-[color:var(--mc-emerald)] font-bold">{row.rating}</span>
</div>
<p className="text-xs text-zinc-500">
{tx("注册时间:", "Registered: ")}
{new Date(row.created_at * 1000).toLocaleString()}
@@ -58,28 +91,28 @@ export default function LeaderboardPage() {
))}
{!loading && items.length === 0 && (
<p className="px-3 py-5 text-center text-sm text-zinc-500">
{tx("暂无排行数据", "No ranking data yet")}
{tx("暂无数据", "No legends yet")}
</p>
)}
</div>
<div className="hidden overflow-x-auto md:block">
<table className="min-w-full text-sm">
<thead className="bg-zinc-100 text-left">
<thead className={`${isMc ? "bg-black/30 text-zinc-300" : "bg-zinc-100 text-left"}`}>
<tr>
<th className="px-3 py-2">{tx("排名", "Rank")}</th>
<th className="px-3 py-2">{tx("用户", "User")}</th>
<th className="px-3 py-2">Rating</th>
<th className="px-3 py-2">{tx("注册时间", "Registered At")}</th>
<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">Rating</th>
<th className="px-3 py-2 text-left">{tx("注册时间", "Registered At")}</th>
</tr>
</thead>
<tbody>
<tbody className={isMc ? "divide-y divide-zinc-700" : ""}>
{items.map((row, i) => (
<tr key={row.user_id} className="border-t">
<td className="px-3 py-2">{i + 1}</td>
<td className="px-3 py-2">{row.username}</td>
<td className="px-3 py-2">{row.rating}</td>
<td className="px-3 py-2">
<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-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">
{new Date(row.created_at * 1000).toLocaleString()}
</td>
</tr>
@@ -87,7 +120,7 @@ export default function LeaderboardPage() {
{!loading && items.length === 0 && (
<tr>
<td className="px-3 py-5 text-center text-zinc-500" colSpan={4}>
{tx("暂无排行数据", "No ranking data yet")}
{tx("暂无数据", "No legends yet")}
</td>
</tr>
)}

查看文件

@@ -14,6 +14,10 @@ type Me = {
created_at: number;
};
// Use a distinct style for inputs/selects
const inputClass = "w-full bg-[color:var(--mc-surface)] text-[color:var(--mc-plank-light)] border-2 border-[color:var(--mc-stone-dark)] px-3 py-2 text-base focus:border-[color:var(--mc-gold)] focus:outline-none transition-colors font-minecraft";
const labelClass = "text-sm text-[color:var(--mc-stone)] mb-1 block";
type RedeemItem = {
id: number;
name: string;
@@ -62,12 +66,21 @@ function fmtTs(v: number | null | undefined): string {
return new Date(v * 1000).toLocaleString();
}
function resolveRank(rating: number): { label: string; color: string; icon: string } {
if (rating >= 2000) return { label: "Netherite", color: "text-[color:var(--mc-red)]", icon: "🔥" };
if (rating >= 1500) return { label: "Diamond", color: "text-[color:var(--mc-diamond)]", icon: "💎" };
if (rating >= 1200) return { label: "Iron", color: "text-zinc-200", icon: "⚔️" };
if (rating >= 1000) return { label: "Stone", color: "text-[color:var(--mc-stone)]", icon: "🪨" };
return { label: "Wood", color: "text-[color:var(--mc-wood)]", icon: "🪵" };
}
export default function MePage() {
const { isZh, tx } = useI18nText();
const [token, setToken] = useState("");
const [profile, setProfile] = useState<Me | null>(null);
const [items, setItems] = useState<RedeemItem[]>([]);
const [records, setRecords] = useState<RedeemRecord[]>([]);
const [historyItems, setHistoryItems] = useState<RatingHistoryItem[]>([]);
const [dailyTasks, setDailyTasks] = useState<DailyTaskItem[]>([]);
const [dailyDayKey, setDailyDayKey] = useState("");
const [dailyTotalReward, setDailyTotalReward] = useState(0);
@@ -99,34 +112,17 @@ export default function MePage() {
if (isZh) return task.title;
if (task.code === "login_checkin") return "Daily Sign-in";
if (task.code === "daily_submit") return "Daily Submission";
if (task.code === "first_ac") return "Solve One Problem";
if (task.code === "code_quality") return "Code Quality";
if (task.code === "first_ac") return "First Blood";
if (task.code === "code_quality") return "Craftsman";
return task.title;
};
const taskDesc = (task: DailyTaskItem): string => {
if (isZh) return task.description;
if (task.code === "login_checkin") return "Sign in once today to get 1 point.";
if (task.code === "daily_submit") return "Submit once today to get 1 point.";
if (task.code === "first_ac") return "Get AC once today to get 1 point.";
if (task.code === "code_quality") return "Submit code longer than 10 lines once today to get 1 point.";
return task.description;
};
const itemName = (name: string): string => {
if (isZh) return name;
if (name === "私人玩游戏时间") return "Private Game Time";
if (name === "私人玩游戏时间") return "Game Time pass";
return name;
};
const itemDesc = (text: string): string => {
if (isZh) return text;
if (text === "全局用户可兑换:假期 1 小时 5 Rating;学习日/非节假日 1 小时 25 Rating。") {
return "Global redeem item: holiday 1 hour = 5 rating; study day/non-holiday 1 hour = 25 rating.";
}
return text;
};
const loadAll = async () => {
setLoading(true);
setError("");
@@ -136,15 +132,17 @@ export default function MePage() {
setToken(tk);
if (!tk) throw new Error(tx("请先登录", "Please sign in first"));
const [me, redeemItems, redeemRecords, daily] = await Promise.all([
const [me, redeemItems, redeemRecords, daily, history] = await Promise.all([
apiFetch<Me>("/api/v1/me", {}, tk),
apiFetch<RedeemItem[]>("/api/v1/me/redeem/items", {}, tk),
apiFetch<RedeemRecord[]>("/api/v1/me/redeem/records?limit=200", {}, tk),
apiFetch<DailyTaskPayload>("/api/v1/me/daily-tasks", {}, tk),
listRatingHistory(50),
]);
setProfile(me);
setItems(redeemItems ?? []);
setRecords(redeemRecords ?? []);
setHistoryItems(history ?? []);
setDailyTasks(daily?.tasks ?? []);
setDailyDayKey(daily?.day_key ?? "");
setDailyTotalReward(daily?.total_reward ?? 0);
@@ -171,9 +169,9 @@ export default function MePage() {
setMsg("");
try {
if (!token) throw new Error(tx("请先登录", "Please sign in first"));
if (!selectedItemId) throw new Error(tx("请选择兑换物品", "Please select a redeem item"));
if (!selectedItemId) throw new Error(tx("请选择交易物品", "Select trade item"));
if (!Number.isFinite(quantity) || quantity <= 0) {
throw new Error(tx("兑换数量必须大于 0", "Quantity must be greater than 0"));
throw new Error(tx("数量必须大于 0", "Amount > 0"));
}
const created = await apiFetch<RedeemCreateResp>(
@@ -192,12 +190,8 @@ export default function MePage() {
setMsg(
isZh
? `兑换成功:${created.item_name} × ${created.quantity}扣除 ${created.total_cost} 积分${
typeof created.rating_after === "number" ? `,当前 Rating ${created.rating_after}` : ""
}`
: `Redeemed successfully: ${itemName(created.item_name)} × ${created.quantity}, cost ${created.total_cost} rating${
typeof created.rating_after === "number" ? `, current rating ${created.rating_after}` : ""
}.`
? `交易成功:${created.item_name} × ${created.quantity}花费 ${created.total_cost} 绿宝石。`
: `Trade successful: ${itemName(created.item_name)} × ${created.quantity}, cost ${created.total_cost} Emeralds.`
);
setNote("");
await loadAll();
@@ -208,176 +202,186 @@ export default function MePage() {
}
};
const rank = resolveRank(profile?.rating ?? 0);
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">
{tx("我的信息与积分兑换", "My Profile & Redeem")}
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<h1 className="text-xl font-bold max-[390px]:text-lg sm:text-2xl text-[color:var(--mc-diamond)] mc-text-shadow">
{tx("冒险者档案 & 交易站", "Character Sheet & Trading Post")}
</h1>
{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>}
{msg && <p className="mt-3 text-sm text-emerald-700">{msg}</p>}
{loading && <p className="mt-3 text-sm text-[color:var(--mc-stone)]">{tx("读取存档中...", "Loading Save...")}</p>}
{error && <p className="mt-3 text-sm text-[color:var(--mc-red)]">{error}</p>}
{msg && <p className="mt-3 text-sm text-[color:var(--mc-green)]">{msg}</p>}
{profile && (
<section className="mt-4 rounded-xl border bg-white p-4 text-sm">
<div className="flex flex-wrap items-center gap-4">
<PixelAvatar
seed={`${profile.username}-${profile.id}`}
size={72}
className="border-zinc-700"
alt={`${profile.username} avatar`}
/>
<div className="space-y-1">
<p>ID: {profile.id}</p>
<p>{tx("用户名", "Username")}: {profile.username}</p>
<p>Rating: {profile.rating}</p>
<p>{tx("创建时间", "Created At")}: {fmtTs(profile.created_at)}</p>
<p className="text-xs text-zinc-500">
{tx("默认像素头像按账号随机生成,可作为主题角色形象。", "Default pixel avatar is randomly generated by account as your theme character.")}
</p>
</div>
</div>
</section>
)}
<div className="mt-4 grid gap-4 md:grid-cols-[1fr_2fr]">
<section className="rounded-none border-[3px] border-black bg-[color:var(--mc-plank)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<div className="flex flex-col items-center text-center">
<div className="relative mb-4">
<div className="absolute inset-0 bg-black opacity-20 translate-x-1 translate-y-1 rounded-none"></div>
<div className="border-[4px] border-white p-1 bg-[color:var(--mc-stone-dark)]">
<PixelAvatar
seed={`${profile.username}-${profile.id}`}
size={100}
className="border-none"
alt="avatar"
/>
</div>
</div>
<section className="mt-4 rounded-xl border bg-white p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="text-base font-semibold">{tx("每日任务", "Daily Tasks")}</h2>
<p className="text-xs text-zinc-600">
{dailyDayKey ? `${dailyDayKey} · ` : ""}
{tx("已获", "Earned")} {dailyGainedReward}/{dailyTotalReward} {tx("分", "pts")}
</p>
</div>
<div className="mt-3 divide-y">
{dailyTasks.map((task) => (
<article key={task.code} className="py-2 text-sm">
<div className="flex items-center justify-between gap-2">
<p className="font-medium">
{taskTitle(task)} · +{task.reward}
</p>
<span
className={`rounded px-2 py-0.5 text-xs ${
task.completed ? "bg-emerald-100 text-emerald-700" : "bg-zinc-100 text-zinc-600"
}`}
>
{task.completed ? tx("已完成", "Completed") : tx("未完成", "Incomplete")}
<h2 className="text-xl font-bold text-black mc-text-shadow-sm mb-1">{profile.username}</h2>
<div className={`text-sm font-bold ${rank.color} mb-2`}>
{rank.icon} {rank.label} Rank
</div>
<div className="w-full bg-black h-4 border border-white relative mb-1">
<div
className="h-full bg-[color:var(--mc-green)]"
style={{ width: `${Math.min(100, (profile.rating % 100))}%` }}
></div>
<span className="absolute inset-0 flex items-center justify-center text-[10px] text-white font-bold shadow-black drop-shadow-md">
Level {Math.floor(profile.rating / 100)}
</span>
</div>
<p className="mt-1 text-xs text-zinc-600">{taskDesc(task)}</p>
{task.completed && (
<p className="mt-1 text-xs text-zinc-500">
{tx("完成时间:", "Completed At: ")}
{fmtTs(task.completed_at)}
</p>
)}
</article>
))}
{!loading && dailyTasks.length === 0 && (
<p className="py-3 text-sm text-zinc-500">
{tx("今日任务尚未初始化,请稍后刷新。", "Today's tasks are not initialized yet. Please refresh later.")}
</p>
)}
</div>
</section>
<p className="text-xs text-[color:var(--mc-stone-dark)]">UID: {profile.id}</p>
</div>
<section className="mt-4 rounded-xl border bg-white p-4">
<h2 className="text-base font-semibold">{tx("积分兑换物品", "Redeem Items")}</h2>
<p className="mt-1 text-xs text-zinc-600">
{tx(
"示例规则:私人玩游戏时间(假期 1 小时=5 积分;学习日/非节假日 1 小时=25 积分)",
"Sample rule: Private Game Time (holiday 1h=5 points; study day/non-holiday 1h=25 points)"
)}
</p>
<div className="mt-3 grid gap-3 md:grid-cols-2">
{items.map((item) => (
<article key={item.id} className="rounded border bg-zinc-50 p-3 text-sm">
<div className="flex items-start justify-between gap-2">
<p className="font-medium">{itemName(item.name)}</p>
<button
className="rounded border px-2 py-1 text-xs hover:bg-zinc-100"
onClick={() => setSelectedItemId(item.id)}
>
{tx("选中", "Select")}
</button>
<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>
<p className="mt-1 text-xs text-zinc-600">{itemDesc(item.description) || "-"}</p>
<p className="mt-1 text-xs text-zinc-700">
{tx("假期", "Holiday")}: {item.holiday_cost} / {item.unit_label}
</p>
<p className="text-xs text-zinc-700">
{tx("学习日", "Study Day")}: {item.studyday_cost} / {item.unit_label}
</p>
</article>
))}
{!loading && items.length === 0 && (
<p className="text-sm text-zinc-500">
{tx("管理员尚未配置可兑换物品。", "No redeem items configured by admin yet.")}
</p>
)}
</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>
</div>
</section>
<div className="mt-4 rounded-lg border p-3">
<h3 className="text-sm font-medium">{tx("兑换表单", "Redeem Form")}</h3>
<div className="mt-2 grid gap-2 md:grid-cols-2">
<select
className="rounded border px-3 py-2 text-sm"
value={selectedItemId}
onChange={(e) => setSelectedItemId(Number(e.target.value))}
>
<option value={0}>{tx("请选择兑换物品", "Please select an item")}</option>
{items.map((item) => (
<option key={item.id} value={item.id}>
{itemName(item.name)}
</option>
))}
</select>
<div className="flex flex-col gap-4">
{/* Daily Tasks */}
<div className="bg-[color:var(--mc-surface)] border-4 border-black p-4 relative">
<h2 className="text-xl text-[color:var(--mc-dirt)] mb-4 flex justify-between items-center font-minecraft">
<span></span>
<span className="text-xs text-[color:var(--mc-gold)]">: {dailyGainedReward} / {dailyTotalReward} XP</span>
</h2>
<select
className="rounded border px-3 py-2 text-sm"
value={dayType}
onChange={(e) => setDayType(e.target.value === "studyday" ? "studyday" : "holiday")}
>
<option value="holiday">{tx("假期时间(按假期单价)", "Holiday time (holiday price)")}</option>
<option value="studyday">{tx("学习日/非节假日(按学习日单价)", "Study day/non-holiday (study-day price)")}</option>
</select>
<div className="space-y-3">
{dailyTasks.map((task, idx) => (
<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>}
</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">
{task.title}
<span className="ml-2 text-[color:var(--mc-gold)] text-base font-minecraft">+{task.reward} XP</span>
</h3>
</div>
<p className="text-[color:var(--mc-stone)] text-base leading-snug">
{task.description}
{task.completed && <span className="ml-2 text-[color:var(--mc-stone-dark)] italic text-sm">({fmtTs(task.completed_at)})</span>}
</p>
</div>
</div>
</div>
))}
</div>
</div>
<section className="flex-1 rounded-none border-[3px] border-black bg-[color:var(--mc-stone)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)] text-white">
<h2 className="text-xl text-[color:var(--mc-obsidian)] mb-6 flex items-center gap-2 border-b-2 border-[color:var(--mc-stone)]/30 pb-2 font-minecraft">
<span className="text-2xl">💎</span>
<span></span>
<span className="ml-auto text-sm text-[color:var(--mc-stone-dark)]">消耗: RATING</span>
</h2>
<input
className="rounded border px-3 py-2 text-sm"
type="number"
min={1}
max={24}
value={quantity}
onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))}
placeholder={tx("兑换时长(小时)", "Redeem duration (hours)")}
/>
<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>
<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"
min={1}
max={64}
value={quantity}
onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))}
/>
</div>
<input
className="rounded border px-3 py-2 text-sm"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder={tx("备注(可选)", "Note (optional)")}
/>
{selectedItem && (
<div className="bg-[color:var(--mc-stone)]/20 p-3 border border-[color:var(--mc-stone)]/30 rounded-none text-base text-[color:var(--mc-obsidian)]">
<p>{selectedItem.description}</p>
<p className="mt-1 text-[color:var(--mc-wood-dark)]">
: {dayType === 'holiday' ? selectedItem.holiday_cost : selectedItem.studyday_cost} Rating / {selectedItem.unit_label}
</p>
</div>
)}
<div className="flex gap-2">
<select
className="flex-1 rounded-none border-2 border-black bg-[color:var(--stone-dark)] text-black px-2 py-1 text-base"
value={dayType}
onChange={(e) => setDayType(e.target.value === "studyday" ? "studyday" : "holiday")}
>
<option value="holiday">{tx("假期特惠", "Holiday Price")}</option>
<option value="studyday">{tx("工作日价格", "Workday Price")}</option>
</select>
<button
className="mc-btn mc-btn-success text-xs px-4"
onClick={() => void redeem()}
disabled={redeemLoading || !selectedItemId}
>
{tx("交易", "Trade")}
</button>
</div>
</div>
</section>
</div>
</div>
)}
<p className="mt-2 text-xs text-zinc-600">
{tx("当前单价", "Current unit price")}: {unitCost} / {tx("小时", "hour")}{tx("预计扣分", "Estimated cost")}: {totalCost}
</p>
<button
className="mt-3 rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50"
onClick={() => void redeem()}
disabled={redeemLoading || !selectedItemId}
>
{redeemLoading ? tx("兑换中...", "Redeeming...") : tx("确认兑换", "Confirm Redeem")}
</button>
{/* 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>
<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)]'}`}>
{item.change > 0 ? `+${item.change}` : item.change}
</span>
<span className="ml-2">{item.note}</span>
</span>
<span className="text-[color:var(--mc-stone-dark)]">
{new Date(item.created_at * 1000).toLocaleString()}
</span>
</div>
))}
{!loading && historyItems.length === 0 && (
<p className="text-xs text-zinc-500">{tx("暂无记录。", "No history.")}</p>
)}
</div>
</section>
<section className="mt-4 rounded-xl border bg-white p-4">
<div className="flex items-center justify-between gap-2">
<h2 className="text-base font-semibold">{tx("兑换记录", "Redeem Records")}</h2>
{/* Trades 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)]">
<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="rounded border px-3 py-1 text-xs hover:bg-zinc-100"
className="text-xs text-[color:var(--mc-stone-dark)] underline"
onClick={() => void loadAll()}
disabled={loading}
>
@@ -385,21 +389,19 @@ export default function MePage() {
</button>
</div>
<div className="mt-3 divide-y">
<div className="max-h-60 overflow-y-auto space-y-1">
{records.map((row) => (
<article key={row.id} className="py-2 text-sm">
<p>
#{row.id} · {itemName(row.item_name)} · {row.quantity} {tx("小时", "hour")} ·{" "}
{row.day_type === "holiday" ? tx("假期", "Holiday") : tx("学习日", "Study Day")}
</p>
<p className="text-xs text-zinc-600">
{tx("单价", "Unit cost")} {row.unit_cost}{tx("总扣分", "Total cost")} {row.total_cost} · {fmtTs(row.created_at)}
</p>
{row.note && <p className="text-xs text-zinc-500">{tx("备注:", "Note: ")}{row.note}</p>}
</article>
<div key={row.id} className="flex justify-between text-xs text-zinc-800 border-b border-zinc-200 pb-1">
<span>
{itemName(row.item_name)} × {row.quantity}
</span>
<span className="text-[color:var(--mc-stone-dark)]">
-{row.total_cost} Gems · {new Date(row.created_at * 1000).toLocaleDateString()}
</span>
</div>
))}
{!loading && records.length === 0 && (
<p className="py-3 text-sm text-zinc-500">{tx("暂无兑换记录。", "No redeem records yet.")}</p>
<p className="text-xs text-zinc-500">{tx("暂无交易。", "No trades.")}</p>
)}
</div>
</section>

文件差异内容过多而无法显示 加载差异

查看文件

@@ -43,19 +43,19 @@ type Preset = {
const PRESETS: Preset[] = [
{
key: "csp-beginner-default",
labelZh: "CSP J/S 入门默认",
labelEn: "CSP J/S Beginner Default",
labelZh: "CSP J/S 入门预设",
labelEn: "CSP J/S Beginner Preset",
tags: ["csp-j", "csp-s", "noip-junior", "noip-senior"],
},
{
key: "csp-j",
labelZh: "仅 CSP-J / 普及",
labelZh: "仅 CSP-J / 普及",
labelEn: "CSP-J / Junior Only",
tags: ["csp-j", "noip-junior"],
},
{
key: "csp-s",
labelZh: "仅 CSP-S / 提高",
labelZh: "仅 CSP-S / 提高",
labelEn: "CSP-S / Senior Only",
tags: ["csp-s", "noip-senior"],
},
@@ -67,14 +67,14 @@ const PRESETS: Preset[] = [
},
{
key: "luogu-all",
labelZh: "洛谷导入全部",
labelZh: "洛谷全站导入",
labelEn: "All Luogu Imports",
sourcePrefix: "luogu:",
tags: [],
},
{
key: "all",
labelZh: "全站全部来源",
labelZh: "全部来源",
labelEn: "All Sources",
tags: [],
},
@@ -83,39 +83,39 @@ const PRESETS: Preset[] = [
const QUICK_CARDS = [
{
presetKey: "csp-j",
titleZh: "CSP-J 真题",
titleEn: "CSP-J Problems",
descZh: "普及组入门训练",
descEn: "Junior training set",
titleZh: "CSP-J 试炼",
titleEn: "CSP-J Trials",
descZh: "普及组入门任务",
descEn: "Junior Tier Quests",
},
{
presetKey: "csp-s",
titleZh: "CSP-S 真题",
titleEn: "CSP-S Problems",
descZh: "提高组进阶训练",
descEn: "Senior advanced set",
titleZh: "CSP-S 挑战",
titleEn: "CSP-S Challenges",
descZh: "提高组进阶任务",
descEn: "Senior Tier Quests",
},
{
presetKey: "noip-junior",
titleZh: "NOIP 入门",
titleEn: "NOIP Junior",
descZh: "基础算法与思维",
descEn: "Basic algorithm thinking",
titleZh: "NOIP 基础",
titleEn: "NOIP Basics",
descZh: "算法与思维",
descEn: "Algorithm & Logic",
},
] as const;
const DIFFICULTY_OPTIONS = [
{ value: "0", labelZh: "全部难度", labelEn: "All Levels" },
{ value: "1", labelZh: "1", labelEn: "1" },
{ value: "2", labelZh: "2", labelEn: "2" },
{ value: "3", labelZh: "3", labelEn: "3" },
{ value: "4", labelZh: "4", labelEn: "4" },
{ value: "5", labelZh: "5", labelEn: "5" },
{ value: "6", labelZh: "6", labelEn: "6" },
{ value: "7", labelZh: "7", labelEn: "7" },
{ value: "8", labelZh: "8", labelEn: "8" },
{ value: "9", labelZh: "9", labelEn: "9" },
{ value: "10", labelZh: "10", labelEn: "10" },
{ value: "0", labelZh: "全部难度", labelEn: "All Tiers" },
{ value: "1", labelZh: "1 - 木剑", labelEn: "1 - Wood" },
{ value: "2", labelZh: "2 - 木剑", labelEn: "2 - Wood" },
{ value: "3", labelZh: "3 - 石剑", labelEn: "3 - Stone" },
{ value: "4", labelZh: "4 - 石剑", labelEn: "4 - Stone" },
{ value: "5", labelZh: "5 - 铁剑", labelEn: "5 - Iron" },
{ value: "6", labelZh: "6 - 铁剑", labelEn: "6 - Iron" },
{ value: "7", labelZh: "7 - 钻石", labelEn: "7 - Diamond" },
{ value: "8", labelZh: "8 - 钻石", labelEn: "8 - Diamond" },
{ value: "9", labelZh: "9 - 下界合金", labelEn: "9 - Netherite" },
{ value: "10", labelZh: "10 - 下界合金", labelEn: "10 - Netherite" },
] as const;
function parseProfile(raw: string): ProblemProfile | null {
@@ -129,10 +129,19 @@ function parseProfile(raw: string): ProblemProfile | null {
}
function difficultyClass(diff: number): string {
if (diff <= 2) return "text-emerald-600";
if (diff <= 4) return "text-blue-600";
if (diff <= 6) return "text-orange-600";
return "text-rose-600";
if (diff <= 2) return "text-[color:var(--mc-wood)]";
if (diff <= 4) return "text-[color:var(--mc-stone-dark)]";
if (diff <= 6) return "text-zinc-100"; // Iron-ish
if (diff <= 8) return "text-[color:var(--mc-diamond)]";
return "text-[color:var(--mc-red)]"; // Netherite/Hard
}
function difficultyIcon(diff: number): string {
if (diff <= 2) return "🪵";
if (diff <= 4) return "🪨";
if (diff <= 6) return "⚔️";
if (diff <= 8) return "💎";
return "🔥";
}
function resolvePid(problem: Problem, profile: ProblemProfile | null): string {
@@ -232,23 +241,23 @@ export default function ProblemsPage() {
};
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">
<main className="mx-auto max-w-7xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
{tx("题库CSP J/S 入门)", "Problem Set (CSP J/S Beginner)")}
<h1 className="text-xl font-bold max-[390px]:text-lg sm:text-2xl text-[color:var(--mc-diamond)] mc-text-shadow">
{tx("任务布告栏", "Quest Board")}
</h1>
<p className="mt-1 text-sm text-zinc-600">
<p className="mt-1 text-sm text-[color:var(--mc-stone)]">
{tx(
"参考洛谷题库列表交互,默认聚焦 CSP-J / CSP-S / NOIP 入门训练。",
"Interaction style is inspired by Luogu problem list. Default focus: CSP-J / CSP-S / NOIP junior training."
"接受任务,赚取 XP,提升等级",
"Accept Quests, Earn XP, Level Up!"
)}
</p>
</div>
<div className="flex w-full flex-wrap items-center gap-3 text-sm sm:w-auto sm:justify-end">
<p className="text-zinc-600">{tx("", "Total")} {totalCount} {tx("题", "problems")}</p>
<Link className="w-full rounded border px-3 py-1 text-center hover:bg-zinc-100 sm:w-auto" href="/backend-logs">
{tx("查看后台日志", "View Backend Logs")}
<p className="text-[color:var(--mc-gold)]">{tx("总任务数: ", "Total Quests: ")} {totalCount}</p>
<Link className="mc-btn w-full text-center sm:w-auto" href="/backend-logs">
{tx("服务器日志", "Server Logs")}
</Link>
</div>
</div>
@@ -260,15 +269,14 @@ export default function ProblemsPage() {
<button
key={card.presetKey}
type="button"
className={`rounded-xl border px-4 py-3 text-left transition ${
active
? "border-zinc-900 bg-zinc-900 text-white"
: "bg-white text-zinc-900 hover:border-zinc-400"
}`}
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)]"
}`}
onClick={() => selectPreset(card.presetKey)}
>
<p className="text-base font-semibold">{isZh ? card.titleZh : card.titleEn}</p>
<p className={`mt-1 text-xs ${active ? "text-zinc-200" : "text-zinc-500"}`}>
<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>
</button>
@@ -276,9 +284,9 @@ export default function ProblemsPage() {
})}
</section>
<section className="mt-4 grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-2 lg:grid-cols-6">
<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 border px-3 py-2 text-sm"
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);
@@ -292,8 +300,8 @@ export default function ProblemsPage() {
</select>
<input
className="rounded border px-3 py-2 text-sm lg:col-span-2"
placeholder={tx("搜索题号/标题/题面关键词", "Search id/title/statement keywords")}
className="rounded-none border-2 border-black bg-[color:var(--surface)] text-white px-3 py-2 text-sm lg:col-span-2"
placeholder={tx("搜索任务 ID / 标题...", "Search Quest ID / Keyword...")}
value={keywordInput}
onChange={(e) => setKeywordInput(e.target.value)}
onKeyDown={(e) => {
@@ -302,7 +310,7 @@ export default function ProblemsPage() {
/>
<select
className="rounded border px-3 py-2 text-sm"
className="rounded-none border-2 border-black bg-[color:var(--surface)] text-white px-3 py-2 text-sm"
value={difficulty}
onChange={(e) => {
setDifficulty(e.target.value);
@@ -311,13 +319,13 @@ export default function ProblemsPage() {
>
{DIFFICULTY_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>
{tx("难度", "Difficulty")} {isZh ? item.labelZh : item.labelEn}
{tx("难度: ", "Tier: ")} {isZh ? item.labelZh : item.labelEn}
</option>
))}
</select>
<select
className="rounded border px-3 py-2 text-sm"
className="rounded-none border-2 border-black bg-[color:var(--surface)] text-white px-3 py-2 text-sm"
value={`${orderBy}:${order}`}
onChange={(e) => {
const [ob, od] = e.target.value.split(":");
@@ -326,16 +334,16 @@ export default function ProblemsPage() {
setPage(1);
}}
>
<option value="id:asc">{tx("号升序", "ID Asc")}</option>
<option value="id:desc">{tx("号降序", "ID Desc")}</option>
<option value="difficulty:asc">{tx("难度升序", "Difficulty Asc")}</option>
<option value="difficulty:desc">{tx("难度降序", "Difficulty Desc")}</option>
<option value="created_at:desc">{tx("最新导入", "Newest Imported")}</option>
<option value="id:asc">{tx("号升序", "ID Asc")}</option>
<option value="id:desc">{tx("号降序", "ID Desc")}</option>
<option value="difficulty:asc">{tx("难度升序", "Tier Asc")}</option>
<option value="difficulty:desc">{tx("难度降序", "Tier Desc")}</option>
<option value="created_at:desc">{tx("最新发布", "Newest")}</option>
<option value="title:asc">{tx("标题 A-Z", "Title A-Z")}</option>
</select>
<button
className="rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50"
className="mc-btn mc-btn-primary"
onClick={applySearch}
disabled={loading}
>
@@ -343,29 +351,28 @@ export default function ProblemsPage() {
</button>
</section>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
{error && <p className="mt-3 text-sm text-[color:var(--mc-red)]">{error}</p>}
<section className="mt-4 rounded-xl border bg-white">
<div className="divide-y md:hidden">
<section className="mt-4 rounded-xl border bg-[color:var(--surface)] shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
<div className="divide-y divide-black md:hidden">
{rows.map(({ problem, profile }) => {
const pid = resolvePid(problem, profile);
const tags = resolveTags(profile);
return (
<article key={problem.id} className="space-y-2 p-3">
<article key={problem.id} className="space-y-2 p-3 bg-[color:var(--surface)] text-zinc-100">
<div className="flex items-start justify-between gap-2">
<Link className="font-medium text-blue-700 hover:underline" href={`/problems/${problem.id}`}>
<Link className="font-bold text-[color:var(--mc-diamond)] hover:text-[color:var(--mc-gold)] hover:underline" href={`/problems/${problem.id}`}>
{pid} · {problem.title}
</Link>
<span className={`shrink-0 text-sm font-semibold ${difficultyClass(problem.difficulty)}`}>
{tx("难度", "Difficulty")} {problem.difficulty}
<span className={`shrink-0 text-sm font-bold ${difficultyClass(problem.difficulty)}`}>
{difficultyIcon(problem.difficulty)} T{problem.difficulty}
</span>
</div>
<p className="text-xs text-zinc-600">{tx("通过/提交", "Accepted/Submissions: ")}{resolvePassRate(profile)}</p>
<p className="text-xs text-zinc-500 break-all">{tx("来源:", "Source: ")}{problem.source || "-"}</p>
<p className="text-xs text-[color:var(--mc-stone)]">{tx("完成率", "Clear Rate: ")}{resolvePassRate(profile)}</p>
<div className="flex flex-wrap gap-1">
{tags.length === 0 && <span className="text-xs text-zinc-400">-</span>}
{tags.length === 0 && <span className="text-xs text-[color:var(--mc-stone-dark)]">-</span>}
{tags.map((tag) => (
<span key={tag} className="rounded bg-zinc-100 px-2 py-0.5 text-xs">
<span key={tag} className="border border-black bg-[color:var(--mc-stone-dark)] px-2 py-0.5 text-xs text-white">
{tag}
</span>
))}
@@ -374,23 +381,23 @@ export default function ProblemsPage() {
);
})}
{!loading && rows.length === 0 && (
<p className="px-3 py-6 text-center text-sm text-zinc-500">
<p className="px-3 py-6 text-center text-sm text-[color:var(--mc-stone)]">
{tx(
"当前筛选下暂无题目,请切换题单预设或先执行导入脚本。",
"No problems under current filters. Switch preset or run import first."
"没有找到任务。请尝试其他频道或刷新地图。",
"No quests found. Try different channel or reload map."
)}
</p>
)}
</div>
<div className="hidden overflow-x-auto md:block">
<table className="min-w-full text-sm">
<thead className="bg-zinc-100 text-left text-zinc-700">
<table className="min-w-full text-sm text-zinc-200">
<thead className="bg-[color:var(--mc-wood-dark)] text-left text-white border-b-2 border-black">
<tr>
<th className="px-3 py-2">{tx("号", "ID")}</th>
<th className="px-3 py-2">{tx("标题", "Title")}</th>
<th className="px-3 py-2">{tx("通过/提交", "Accepted/Submissions")}</th>
<th className="px-3 py-2">{tx("难度", "Difficulty")}</th>
<th className="px-3 py-2">{tx("号", "ID")}</th>
<th className="px-3 py-2">{tx("任务标题", "Quest Title")}</th>
<th className="px-3 py-2">{tx("完成率", "Clear Rate")}</th>
<th className="px-3 py-2">{tx("难度", "Tier")}</th>
<th className="px-3 py-2">{tx("标签", "Tags")}</th>
<th className="px-3 py-2">{tx("来源", "Source")}</th>
</tr>
@@ -400,37 +407,37 @@ export default function ProblemsPage() {
const pid = resolvePid(problem, profile);
const tags = resolveTags(profile);
return (
<tr key={problem.id} className="border-t hover:bg-zinc-50">
<td className="px-3 py-2 font-medium text-blue-700">{pid}</td>
<tr key={problem.id} className="border-b border-black hover:bg-[color:var(--surface-soft)] transition-colors">
<td className="px-3 py-2 font-bold text-[color:var(--mc-diamond)]">{pid}</td>
<td className="px-3 py-2">
<Link className="hover:underline" href={`/problems/${problem.id}`}>
<Link className="hover:underline hover:text-[color:var(--mc-gold)]" href={`/problems/${problem.id}`}>
{problem.title}
</Link>
</td>
<td className="px-3 py-2 text-zinc-600">{resolvePassRate(profile)}</td>
<td className={`px-3 py-2 font-semibold ${difficultyClass(problem.difficulty)}`}>
{problem.difficulty}
<td className="px-3 py-2 text-[color:var(--mc-stone)]">{resolvePassRate(profile)}</td>
<td className={`px-3 py-2 font-bold ${difficultyClass(problem.difficulty)}`}>
{difficultyIcon(problem.difficulty)} {problem.difficulty}
</td>
<td className="px-3 py-2">
<div className="flex flex-wrap gap-1">
{tags.length === 0 && <span className="text-zinc-400">-</span>}
{tags.length === 0 && <span className="text-[color:var(--mc-stone-dark)]">-</span>}
{tags.map((tag) => (
<span key={tag} className="rounded bg-zinc-100 px-2 py-0.5 text-xs">
<span key={tag} className="border border-black bg-[color:var(--mc-stone-dark)] px-2 py-0.5 text-xs text-white">
{tag}
</span>
))}
</div>
</td>
<td className="px-3 py-2 text-zinc-500">{problem.source || "-"}</td>
<td className="px-3 py-2 text-[color:var(--mc-stone)]">{problem.source || "-"}</td>
</tr>
);
})}
{!loading && rows.length === 0 && (
<tr>
<td className="px-3 py-6 text-center text-zinc-500" colSpan={6}>
<td className="px-3 py-6 text-center text-[color:var(--mc-stone)]" colSpan={6}>
{tx(
"当前筛选下暂无题目,请切换题单预设或先执行导入脚本。",
"No problems under current filters. Switch preset or run import first."
"没有找到任务。请尝试其他频道或刷新地图。",
"No quests found. Try different channel or reload map."
)}
</td>
</tr>
@@ -443,17 +450,17 @@ 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="rounded border px-3 py-1 disabled:opacity-50"
className="mc-btn"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={loading || page <= 1}
>
{tx("上一页", "Prev")}
</button>
<span>
<span className="text-[color:var(--mc-diamond)] font-bold">
{isZh ? `${page} / ${totalPages}` : `Page ${page} / ${totalPages}`}
</span>
<button
className="rounded border px-3 py-1 disabled:opacity-50"
className="mc-btn"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={loading || page >= totalPages}
>
@@ -461,10 +468,10 @@ export default function ProblemsPage() {
</button>
</div>
<div className="flex items-center gap-2 sm:justify-end">
<div className="flex items-center gap-2 sm:justify-end text-[color:var(--mc-stone)]">
<span>{tx("每页", "Per Page")}</span>
<select
className="rounded border px-2 py-1"
className="rounded border border-black bg-[color:var(--surface)] text-white px-2 py-1"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));

查看文件

@@ -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";
type Submission = {
id: number;
@@ -22,6 +23,8 @@ type ListResp = { items: Submission[]; page: number; page_size: number };
export default function SubmissionsPage() {
const { tx } = useI18nText();
const { theme } = useUiPreferences();
const isMc = theme === "minecraft";
const [userId, setUserId] = useState("");
const [problemId, setProblemId] = useState("");
const [contestId, setContestId] = useState("");
@@ -35,9 +38,28 @@ export default function SubmissionsPage() {
};
const ratingDeltaClass = (delta: number) => {
if (delta > 0) return "text-emerald-700";
if (delta < 0) return "text-red-700";
return "text-zinc-600";
if (delta > 0) return isMc ? "text-[color:var(--mc-green)]" : "text-emerald-700";
if (delta < 0) return isMc ? "text-[color:var(--mc-red)]" : "text-red-700";
return isMc ? "text-zinc-400" : "text-zinc-600";
};
/** 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;
};
const load = async () => {
@@ -63,103 +85,133 @@ export default function SubmissionsPage() {
}, []);
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">
{tx("提交记录", "Submissions")}
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<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")}
</span>
) : (
tx("提交记录", "Submissions")
)}
</h1>
<div className="mt-4 grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-4">
{/* Filters */}
<div className={`mt-4 grid gap-3 rounded-xl border p-4 md:grid-cols-4 ${isMc
? "bg-[color:var(--mc-stone-dark)] border-[3px] border-black shadow-[4px_4px_0_rgba(0,0,0,0.5)]"
: "bg-white"}`}>
<input
className="rounded border px-3 py-2"
placeholder="user_id"
className={`rounded border px-3 py-2 ${isMc
? "bg-black/40 border-zinc-600 text-zinc-200 placeholder:text-zinc-500"
: ""}`}
placeholder={isMc ? tx("冒险者 ID", "Adventurer ID") : "user_id"}
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<input
className="rounded border px-3 py-2"
placeholder="problem_id"
className={`rounded border px-3 py-2 ${isMc
? "bg-black/40 border-zinc-600 text-zinc-200 placeholder:text-zinc-500"
: ""}`}
placeholder={isMc ? tx("任务 ID", "Quest ID") : "problem_id"}
value={problemId}
onChange={(e) => setProblemId(e.target.value)}
/>
<input
className="rounded border px-3 py-2"
placeholder="contest_id"
className={`rounded border px-3 py-2 ${isMc
? "bg-black/40 border-zinc-600 text-zinc-200 placeholder:text-zinc-500"
: ""}`}
placeholder={isMc ? tx("突袭 ID", "Raid ID") : "contest_id"}
value={contestId}
onChange={(e) => setContestId(e.target.value)}
/>
<button
className="rounded bg-zinc-900 px-4 py-2 text-white disabled:opacity-50"
className={`px-4 py-2 disabled:opacity-50 ${isMc
? "mc-btn"
: "rounded bg-zinc-900 text-white"}`}
onClick={() => void load()}
disabled={loading}
>
{loading ? tx("加载中...", "Loading...") : tx("筛选", "Filter")}
{loading
? tx("搜索中...", "Searching...")
: isMc
? tx("🔍 搜索记录", "🔍 Search Logs")
: tx("筛选", "Filter")}
</button>
</div>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
<div className="mt-4 rounded-xl border bg-white">
{/* Mobile cards */}
<div className={`mt-4 rounded-xl border ${isMc
? "bg-[color:var(--mc-stone-dark)] border-[3px] border-black shadow-[4px_4px_0_rgba(0,0,0,0.5)]"
: "bg-white"}`}>
<div className="divide-y md:hidden">
{items.map((s) => (
<article key={s.id} className="space-y-2 p-3 text-sm">
<article key={s.id} className={`space-y-2 p-3 text-sm ${isMc ? "text-zinc-200 border-zinc-700" : ""}`}>
<div className="flex items-center justify-between gap-3">
<p className="font-medium">{tx("提交", "Submission")} #{s.id}</p>
<span className="text-xs text-zinc-500">{s.status}</span>
<p className={`font-medium ${isMc ? "text-[color:var(--mc-gold)]" : ""}`}>
{isMc ? tx("施法", "Cast") : tx("提交", "Submission")} #{s.id}
</p>
<span className={`text-xs ${isMc ? "" : "text-zinc-500"}`}>{statusLabel(s.status)}</span>
</div>
<p className="text-xs text-zinc-600">
{tx("用户", "User")} {s.user_id} · {tx("题目", "Problem")} {s.problem_id} · {tx("分数", "Score")} {s.score}
<p className={`text-xs ${isMc ? "text-zinc-400" : "text-zinc-600"}`}>
{isMc ? tx("冒险者", "Player") : tx("用户", "User")} {s.user_id} · {tx("任务", "Quest")} {s.problem_id} · {tx("分数", "Score")} {s.score}
</p>
<p className={`text-xs ${ratingDeltaClass(s.rating_delta)}`}>
{tx("Rating 变化", "Rating Delta")} {fmtRatingDelta(s.rating_delta)}
{isMc ? tx("绿宝石变化", "Emerald Δ") : tx("Rating 变化", "Rating Delta")} {fmtRatingDelta(s.rating_delta)}
</p>
<p className="text-xs text-zinc-600">{tx("耗时", "Time")} {s.time_ms} ms</p>
<Link className="text-blue-600 underline" href={`/submissions/${s.id}`}>
{tx("查看详情", "View Detail")}
<p className={`text-xs ${isMc ? "text-zinc-400" : "text-zinc-600"}`}>{tx("耗时", "Time")} {s.time_ms} ms</p>
<Link className={`underline ${isMc ? "text-[color:var(--mc-diamond)]" : "text-blue-600"}`} href={`/submissions/${s.id}`}>
{isMc ? tx("📜 查看详情", "📜 View Detail") : tx("查看详情", "View Detail")}
</Link>
</article>
))}
{!loading && items.length === 0 && (
<p className="px-3 py-5 text-center text-sm text-zinc-500">{tx("暂无提交记录", "No submissions yet")}</p>
<p className={`px-3 py-5 text-center text-sm ${isMc ? "text-zinc-500" : "text-zinc-500"}`}>
{isMc ? tx("暂无施法记录", "No spell casts yet") : tx("暂无提交记录", "No submissions yet")}
</p>
)}
</div>
{/* Desktop table */}
<div className="hidden overflow-x-auto md:block">
<table className="min-w-full text-sm">
<thead className="bg-zinc-100 text-left">
<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">{tx("用户", "User")}</th>
<th className="px-3 py-2">{tx("题目", "Problem")}</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">{tx("Rating 变化", "Rating Delta")}</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">{tx("详情", "Detail")}</th>
</tr>
</thead>
<tbody>
{items.map((s) => (
<tr key={s.id} className="border-t">
<td className="px-3 py-2">{s.id}</td>
<tr key={s.id} className={`border-t ${isMc ? "border-zinc-700" : ""}`}>
<td className={`px-3 py-2 ${isMc ? "text-[color:var(--mc-gold)]" : ""}`}>{s.id}</td>
<td className="px-3 py-2">{s.user_id}</td>
<td className="px-3 py-2">{s.problem_id}</td>
<td className="px-3 py-2">{s.status}</td>
<td className="px-3 py-2">{statusLabel(s.status)}</td>
<td className="px-3 py-2">{s.score}</td>
<td className={`px-3 py-2 ${ratingDeltaClass(s.rating_delta)}`}>
{fmtRatingDelta(s.rating_delta)}
</td>
<td className="px-3 py-2">{s.time_ms}</td>
<td className="px-3 py-2">
<Link className="text-blue-600 underline" href={`/submissions/${s.id}`}>
{tx("查看", "View")}
<Link className={`underline ${isMc ? "text-[color:var(--mc-diamond)]" : "text-blue-600"}`} href={`/submissions/${s.id}`}>
{isMc ? tx("📜 查看", "📜 View") : tx("查看", "View")}
</Link>
</td>
</tr>
))}
{!loading && items.length === 0 && (
<tr>
<td className="px-3 py-5 text-center text-zinc-500" colSpan={8}>
{tx("暂无提交记录", "No submissions yet")}
<td className={`px-3 py-5 text-center ${isMc ? "text-zinc-500" : "text-zinc-500"}`} colSpan={8}>
{isMc ? tx("暂无施法记录", "No spell casts yet") : tx("暂无提交记录", "No submissions yet")}
</td>
</tr>
)}

查看文件

@@ -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 { useUiPreferences } from "@/components/ui-preference-provider";
type WrongBookItem = {
user_id: number;
@@ -23,6 +24,8 @@ function fmtTs(v: number): string {
export default function WrongBookPage() {
const { tx } = useI18nText();
const { theme } = useUiPreferences();
const isMc = theme === "minecraft";
const [token, setToken] = useState("");
const [items, setItems] = useState<WrongBookItem[]>([]);
const [loading, setLoading] = useState(false);
@@ -73,21 +76,36 @@ export default function WrongBookPage() {
};
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">
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
{tx("错题本", "Wrong Book")}
<main className="mx-auto max-w-5xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
<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("诅咒卷轴", "Cursed Scrolls")}
</span>
) : (
tx("错题本", "Wrong Book")
)}
</h1>
<p className="mt-2 text-sm text-zinc-600">
{tx("未通过提交会自动进入错题本。", "Failed submissions are added to the wrong-book automatically.")}
<p className={`mt-2 text-sm ${isMc ? "text-zinc-400" : "text-zinc-600"}`}>
{isMc
? tx("失败的咒语会自动记录在诅咒卷轴中,复习并重新挑战!", "Failed spells are recorded in your Cursed Scrolls. Review and retry!")
: tx("未通过提交会自动进入错题本。", "Failed submissions are added to the wrong-book automatically.")}
</p>
<div className="mt-4">
<button
className="rounded bg-zinc-900 px-4 py-2 text-white disabled:opacity-50"
className={`px-4 py-2 disabled:opacity-50 ${isMc
? "mc-btn"
: "rounded bg-zinc-900 text-white"}`}
onClick={() => void load()}
disabled={loading}
>
{loading ? tx("刷新中...", "Refreshing...") : tx("刷新", "Refresh")}
{loading
? tx("搜索中...", "Searching...")
: isMc
? tx("🔍 重新搜索", "🔍 Search Again")
: tx("刷新", "Refresh")}
</button>
</div>
@@ -95,46 +113,63 @@ export default function WrongBookPage() {
<div className="mt-4 space-y-3">
{items.map((item) => (
<div key={item.problem_id} className="rounded-xl border bg-white p-4">
<div
key={item.problem_id}
className={`rounded-xl border p-4 ${isMc
? "bg-[color:var(--mc-stone-dark)] border-[3px] border-black shadow-[4px_4px_0_rgba(0,0,0,0.5)] text-white"
: "bg-white"}`}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<Link className="font-medium text-blue-700 hover:underline" href={`/problems/${item.problem_id}`}>
<Link
className={`font-medium hover:underline ${isMc ? "text-[color:var(--mc-gold)]" : "text-blue-700"}`}
href={`/problems/${item.problem_id}`}
>
#{item.problem_id} {item.problem_title}
</Link>
<div className="flex flex-wrap items-center gap-2">
<Link
className="rounded border px-3 py-1 text-sm hover:bg-zinc-100"
className={`rounded border px-3 py-1 text-sm ${isMc
? "mc-btn text-xs"
: "hover:bg-zinc-100"}`}
href={`/problems/${item.problem_id}`}
>
{tx("查看题目", "View Problem")}
{isMc ? tx("⚔️ 重新挑战", "⚔️ Retry Quest") : tx("查看任务", "View Quest")}
</Link>
{item.last_submission_id && (
<Link
className="rounded border px-3 py-1 text-sm hover:bg-zinc-100"
className={`rounded border px-3 py-1 text-sm ${isMc
? "mc-btn text-xs"
: "hover:bg-zinc-100"}`}
href={`/submissions/${item.last_submission_id}`}
>
{tx("查看最近提交", "View Latest Submission")}
{isMc ? tx("📜 查看战报", "📜 View Battle Log") : tx("查看最近提交", "View Latest Submission")}
</Link>
)}
</div>
</div>
<p className="mt-1 text-xs text-zinc-500">
{tx("最近提交:", "Latest Submission:")} {item.last_submission_id ?? "-"} ·{" "}
<p className={`mt-1 text-xs ${isMc ? "text-zinc-400" : "text-zinc-500"}`}>
{isMc ? tx("上次施法:", "Last Cast:") : tx("最近提交:", "Latest Submission:")} {item.last_submission_id ?? "-"} ·{" "}
{tx("更新时间:", "Updated:")} {fmtTs(item.updated_at)}
</p>
<div className="mt-2 flex flex-wrap justify-end gap-2">
<button
className="rounded border px-3 py-1 text-sm hover:bg-zinc-100"
className={`rounded border px-3 py-1 text-sm ${isMc
? "border-red-900 bg-red-900/40 text-red-300 hover:bg-red-900/60"
: "hover:bg-zinc-100"}`}
onClick={() => void removeItem(item.problem_id)}
>
{tx("移除", "Remove")}
{isMc ? tx("🗑️ 移除诅咒", "🗑️ Remove Curse") : tx("移除", "Remove")}
</button>
</div>
<textarea
className="mt-2 h-24 w-full rounded border p-2 text-sm"
className={`mt-2 h-24 w-full rounded border p-2 text-sm ${isMc
? "bg-black/40 border-zinc-600 text-zinc-200 placeholder:text-zinc-500"
: ""}`}
value={item.note}
placeholder={isMc ? tx("记录你的笔记...", "Write your notes...") : ""}
onChange={(e) => {
const next = e.target.value;
setItems((prev) =>
@@ -145,19 +180,28 @@ export default function WrongBookPage() {
}}
/>
<button
className="mt-2 rounded border px-3 py-1 text-sm hover:bg-zinc-100"
className={`mt-2 rounded border px-3 py-1 text-sm ${isMc
? "mc-btn text-xs"
: "hover:bg-zinc-100"}`}
onClick={() => void updateNote(item.problem_id, item.note)}
>
{tx("保存备注", "Save Note")}
{isMc ? tx("💾 保存笔记", "💾 Save Notes") : tx("保存备注", "Save Note")}
</button>
</div>
))}
{!loading && items.length === 0 && (
<div className="rounded-xl border bg-white p-6 text-center text-sm text-zinc-500">
{tx(
"暂无错题。提交未通过后会自动加入错题本,可点击“查看题目/查看最近提交”快速复盘。",
"No wrong-book entries yet. Failed submissions will be added automatically; use “View Problem/View Latest Submission” to review quickly."
)}
<div className={`rounded-xl border p-6 text-center text-sm ${isMc
? "bg-black/20 border-zinc-700 text-zinc-500"
: "bg-white text-zinc-500"}`}>
{isMc
? tx(
"🎉 诅咒卷轴为空!你的每一个咒语都精准命中了目标。",
"🎉 No cursed scrolls! Every spell you cast hit its mark."
)
: tx(
"暂无错题。提交未通过后会自动加入错题本,可点击「查看题目/查看最近提交」快速复盘。",
'No wrong-book entries yet. Failed submissions will be added automatically.'
)}
</div>
)}
</div>

查看文件

@@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { PixelAvatar } from "@/components/pixel-avatar";
import { useUiPreferences } from "@/components/ui-preference-provider";
import { XpBar } from "@/components/xp-bar";
import { apiFetch } from "@/lib/api";
import { clearToken, readToken } from "@/lib/auth";
import type { ThemeId } from "@/themes/types";
@@ -199,9 +200,8 @@ export function AppNav() {
<div key={group.key} className="relative">
<button
type="button"
className={`rounded-md border px-3 py-1 text-sm ${
active ? "border-zinc-900 bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
className={`rounded-md border px-3 py-1 text-sm ${active ? "border-zinc-900 bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
aria-expanded={opened}
onClick={() =>
setDesktopOpenGroup((prev) => (prev === group.key ? null : group.key))
@@ -211,9 +211,8 @@ export function AppNav() {
</button>
{opened && (
<div
className={`absolute left-0 top-full z-50 mt-2 rounded-md border bg-[color:var(--surface)] p-1 shadow-lg ${
group.key === "account" ? "min-w-[18rem]" : "min-w-[11rem]"
}`}
className={`absolute left-0 top-full z-50 mt-2 rounded-md border bg-[color:var(--surface)] p-1 shadow-lg ${group.key === "account" ? "min-w-[18rem]" : "min-w-[11rem]"
}`}
>
{group.links.map((item) => {
const linkActive = isActivePath(pathname, item.href);
@@ -221,9 +220,8 @@ export function AppNav() {
<button
key={item.href}
type="button"
className={`block w-full rounded px-3 py-1.5 text-left text-sm ${
linkActive ? "bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
className={`block w-full rounded px-3 py-1.5 text-left text-sm ${linkActive ? "bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
onClick={() => {
setDesktopOpenGroup(null);
router.push(item.href);
@@ -306,9 +304,8 @@ export function AppNav() {
<button
key={group.key}
type="button"
className={`rounded-md border px-3 py-1 text-sm ${
active ? "border-zinc-900 bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
className={`rounded-md border px-3 py-1 text-sm ${active ? "border-zinc-900 bg-zinc-900 text-white" : "hover:bg-zinc-100"
}`}
onClick={() => router.push(group.links[0]?.href ?? "/")}
>
{group.label}
@@ -391,12 +388,19 @@ export function AppNav() {
{hasToken ? t("nav.logged_in") : t("nav.logged_out")}
</span>
{hasToken && (
<PixelAvatar
seed={avatarSeed}
size={24}
className="border-zinc-700"
alt={meProfile?.username ? `${meProfile.username} avatar` : "avatar"}
/>
<>
{theme === "minecraft" && (
<div className="hidden md:block w-32 mr-2">
<XpBar level={5} currentXp={750} nextLevelXp={1000} />
</div>
)}
<PixelAvatar
seed={avatarSeed}
size={24}
className="border-zinc-700"
alt={meProfile?.username ? `${meProfile.username} avatar` : "avatar"}
/>
</>
)}
{hasToken && (
<button

查看文件

@@ -8,6 +8,7 @@ import remarkMath from "remark-math";
type Props = {
markdown: string;
className?: string;
};
function normalizeImageSrc(src: string): string {
@@ -18,9 +19,9 @@ function normalizeImageSrc(src: string): string {
return src;
}
export function MarkdownRenderer({ markdown }: Props) {
export function MarkdownRenderer({ markdown, className }: Props) {
return (
<article className="space-y-3 text-sm leading-7 text-zinc-800">
<article className={`space-y-3 text-sm leading-7 text-zinc-800 ${className ?? ""}`}>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex, rehypeHighlight]}

查看文件

@@ -12,18 +12,22 @@ function isActivePath(pathname: string, href: string): boolean {
export function MobileTabBar() {
const pathname = usePathname();
const { t } = useUiPreferences();
const { t, theme } = useUiPreferences();
const isMc = theme === "minecraft";
const tabs = [
{ label: t("mobile.tab.problems"), href: "/problems" },
{ label: t("mobile.tab.submissions"), href: "/submissions" },
{ label: t("mobile.tab.contests"), href: "/contests" },
{ label: t("mobile.tab.kb"), href: "/kb" },
{ label: t("mobile.tab.me"), href: "/me" },
{ label: t("mobile.tab.problems"), href: "/problems", icon: "📜" },
{ label: t("mobile.tab.submissions"), href: "/submissions", icon: "⏱️" },
{ label: t("mobile.tab.contests"), href: "/contests", icon: "⚔️" },
{ label: t("mobile.tab.kb"), href: "/kb", icon: "📘" },
{ label: t("mobile.tab.me"), href: "/me", icon: "👤" },
] as const;
return (
<nav className="print-hidden fixed inset-x-0 bottom-0 z-40 border-t bg-[color:var(--surface)]/95 pb-[calc(0.3rem+env(safe-area-inset-bottom))] pt-1 backdrop-blur supports-[backdrop-filter]:bg-[color:var(--surface)]/85 md:hidden">
<nav className={`print-hidden fixed inset-x-0 bottom-0 z-40 border-t pb-[calc(0.3rem+env(safe-area-inset-bottom))] pt-2 md:hidden ${isMc
? "bg-[color:var(--mc-stone-dark)] border-black border-t-[3px]"
: "bg-[color:var(--surface)]/95 backdrop-blur supports-[backdrop-filter]:bg-[color:var(--surface)]/85"
}`}>
<div className="mx-auto max-w-5xl px-2 max-[390px]:px-1.5">
<div className="grid grid-cols-5 gap-1 max-[390px]:gap-0.5">
{tabs.map((tab) => {
@@ -32,13 +36,17 @@ export function MobileTabBar() {
<Link
key={tab.href}
href={tab.href}
className={`rounded-md px-1 py-1.5 text-center text-xs max-[390px]:text-[11px] ${
active
? "bg-zinc-900 font-semibold text-white"
: "text-zinc-600 hover:bg-zinc-100"
}`}
className={`flex flex-col items-center justify-center rounded-none px-1 py-1 text-center text-[10px] sm:text-xs ${isMc
? active
? "bg-[color:var(--mc-diamond)] text-black font-bold border-2 border-black"
: "bg-[color:var(--mc-stone)] text-zinc-300 border-2 border-black/50 hover:bg-[color:var(--mc-stone-light)]"
: active
? "bg-zinc-900 font-semibold text-white rounded-md"
: "text-zinc-600 hover:bg-zinc-100 rounded-md"
}`}
>
{tab.label}
{isMc && <span className="text-sm mb-0.5">{tab.icon}</span>}
<span className="truncate w-full">{tab.label}</span>
</Link>
);
})}

查看文件

@@ -0,0 +1,49 @@
"use client";
import { useUiPreferences } from "@/components/ui-preference-provider";
type Props = {
level: number;
currentXp: number;
nextLevelXp: number;
className?: string;
};
export function XpBar({ level, currentXp, nextLevelXp, className = "" }: Props) {
const { theme } = useUiPreferences();
const isMc = theme === "minecraft";
const progress = Math.min(100, Math.max(0, (currentXp / nextLevelXp) * 100));
if (!isMc) {
return null;
}
return (
<div className={`flex flex-col items-center select-none ${className}`}>
<div className="relative w-full max-w-[400px]">
{/* Level Indicator */}
<div className="absolute -top-3 left-1/2 -translate-x-1/2 text-[#80ff20] drop-shadow-[2px_2px_0_#000] font-[PressStart2P] text-xs z-10">
{level}
</div>
{/* XP Bar Background */}
<div className="h-3 w-full bg-[#3a3a3a] border-2 border-black flex relative">
{/* XP Bar Progress */}
<div
className="h-full bg-[#80ff20] transition-all duration-500 ease-out"
style={{ width: `${progress}%`, boxShadow: "inset 0 2px 0 rgba(255,255,255,0.3), inset 0 -2px 0 rgba(0,0,0,0.2)" }}
/>
{/* Segmentation lines (every 10%) */}
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="absolute top-0 bottom-0 w-[2px] bg-black/20" style={{ left: `${(i + 1) * 10}%` }} />
))}
</div>
</div>
{/* XP Text */}
<div className="mt-1 text-[10px] text-[#80ff20] font-[VT323] drop-shadow-[1px_1px_0_#000]">
{currentXp} / {nextLevelXp}
</div>
</div>
);
}

查看文件

@@ -9,8 +9,8 @@ function uiText(zhText: string, enText: string): string {
}
type ApiEnvelope<T> =
| { ok: true; data?: T; [k: string]: unknown }
| { ok: false; error?: string; [k: string]: unknown };
| { ok: true; data?: T;[k: string]: unknown }
| { ok: false; error?: string;[k: string]: unknown };
export async function apiFetch<T>(
path: string,
@@ -52,11 +52,9 @@ export async function apiFetch<T>(
} catch (retryErr) {
throw new Error(
uiText(
`网络请求失败,请检查后端服务或代理连接(${
retryErr instanceof Error ? retryErr.message : String(retryErr)
`网络请求失败,请检查后端服务或代理连接(${retryErr instanceof Error ? retryErr.message : String(retryErr)
}`,
`Network request failed. Please check backend/proxy connectivity (${
retryErr instanceof Error ? retryErr.message : String(retryErr)
`Network request failed. Please check backend/proxy connectivity (${retryErr instanceof Error ? retryErr.message : String(retryErr)
}).`
)
);
@@ -92,3 +90,14 @@ export async function apiFetch<T>(
return payload as T;
}
export interface RatingHistoryItem {
type: string;
created_at: number;
change: number;
note: string;
}
export async function listRatingHistory(limit: number = 100): Promise<RatingHistoryItem[]> {
return apiFetch<RatingHistoryItem[]>(`/api/v1/me/rating-history?limit=${limit}`);
}

查看文件

@@ -1,5 +1,5 @@
import { enMessages } from "@/themes/default/messages/en";
import { zhMessages } from "@/themes/default/messages/zh";
import { enMessages } from "./messages/en";
import { zhMessages } from "./messages/zh";
import type { ThemeDefinition } from "@/themes/types";
export const minecraftTheme: ThemeDefinition = {
@@ -13,3 +13,4 @@ export const minecraftTheme: ThemeDefinition = {
zh: zhMessages,
},
};

查看文件

@@ -0,0 +1,53 @@
import type { ThemeMessages } from "@/themes/types";
export const enMessages: ThemeMessages = {
"app.title": "CSP Quest Chronicles",
"nav.menu": "Game Menu",
"nav.expand": "Open Inventory",
"nav.collapse": "Close",
"nav.secondary_menu": "Quick Slots",
"nav.logged_in": "Online",
"nav.logged_out": "Offline",
"nav.logout": "Disconnect",
"nav.group.learn": "Adventure",
"nav.group.contest": "Battle Area",
"nav.group.system": "Server Ops",
"nav.group.account": "Player",
"nav.link.home": "Spawn Point",
"nav.link.problems": "Quest Board",
"nav.link.submissions": "Adventure Log",
"nav.link.wrong_book": "Grimoire",
"nav.link.kb": "Enchanted Library",
"nav.link.run": "Craft Code",
"nav.link.contests": "Raids",
"nav.link.leaderboard": "Hall of Fame",
"nav.link.imports": "Import Maps",
"nav.link.backend_logs": "Server Logs",
"nav.link.admin_users": "XP Management",
"nav.link.admin_redeem": "Loot Config",
"nav.link.api_docs": "Redstone Logic",
"nav.link.auth": "Login to Server",
"nav.link.me": "Character Sheet",
"mobile.tab.problems": "Quests",
"mobile.tab.submissions": "History",
"mobile.tab.contests": "Raids",
"mobile.tab.kb": "Library",
"mobile.tab.me": "Char",
"prefs.theme": "Texture Pack",
"prefs.language": "Language",
"prefs.lang.en": "English",
"prefs.lang.zh": "Chinese",
"admin.entry.title": "OP Control Panel",
"admin.entry.desc": "Super User: admin / whoami139",
"admin.entry.login": "Enter Portal",
"admin.entry.user_rating": "Manage XP",
"admin.entry.redeem": "Manage Loot",
"admin.entry.logs": "Server Console",
"admin.entry.moved_to_platform": "Redirected to Server Ops module.",
};

查看文件

@@ -0,0 +1,53 @@
import type { ThemeMessages } from "@/themes/types";
export const zhMessages: ThemeMessages = {
"app.title": "CSP 冒险传奇",
"nav.menu": "游戏菜单",
"nav.expand": "打开背包",
"nav.collapse": "关闭",
"nav.secondary_menu": "快捷栏",
"nav.logged_in": "在线",
"nav.logged_out": "离线",
"nav.logout": "断开连接",
"nav.group.learn": "冒险模式",
"nav.group.contest": "竞技场",
"nav.group.system": "服务器指令",
"nav.group.account": "玩家档案",
"nav.link.home": "出生点",
"nav.link.problems": "任务布告栏",
"nav.link.submissions": "冒险日志",
"nav.link.wrong_book": "错题卷轴",
"nav.link.kb": "附魔指南",
"nav.link.run": "代码工作台",
"nav.link.contests": "团队副本",
"nav.link.leaderboard": "英雄榜",
"nav.link.imports": "地图导入",
"nav.link.backend_logs": "服务器日志",
"nav.link.admin_users": "XP管理",
"nav.link.admin_redeem": "战利品配置",
"nav.link.api_docs": "红石电路图",
"nav.link.auth": "登录服务器",
"nav.link.me": "角色面板",
"mobile.tab.problems": "任务",
"mobile.tab.submissions": "日志",
"mobile.tab.contests": "副本",
"mobile.tab.kb": "指南",
"mobile.tab.me": "角色",
"prefs.theme": "材质包",
"prefs.language": "语言",
"prefs.lang.en": "英语",
"prefs.lang.zh": "中文",
"admin.entry.title": "OP 控制台",
"admin.entry.desc": "管理员账号admin / whoami139",
"admin.entry.login": "进入传送门",
"admin.entry.user_rating": "XP 管理",
"admin.entry.redeem": "战利品管理",
"admin.entry.logs": "服务器日志",
"admin.entry.moved_to_platform": "已重新定向至服务器指令模块。",
};

查看文件

@@ -1,7 +1,6 @@
@font-face {
font-family: "PressStart2P";
src: url("https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2")
format("woff2");
src: url("https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2") format("woff2");
font-display: swap;
}
@@ -58,8 +57,18 @@
letter-spacing: 0.04em;
line-height: 1.5;
text-shadow: 2px 2px 0 #000000;
text-transform: uppercase;
}
:root[data-theme="minecraft"] .mc-text-shadow {
text-shadow: 2px 2px 0 #000000;
}
:root[data-theme="minecraft"] .mc-text-shadow-sm {
text-shadow: 1px 1px 0 #000000;
}
:root[data-theme="minecraft"] ::-webkit-scrollbar {
width: 12px;
height: 12px;
@@ -157,36 +166,62 @@
color: var(--mc-red) !important;
}
:root[data-theme="minecraft"] button {
:root[data-theme="minecraft"] button:not(.mc-reset),
:root[data-theme="minecraft"] .mc-btn {
background: linear-gradient(180deg, var(--mc-wood) 0%, var(--mc-wood-dark) 100%) !important;
border: 3px solid #000 !important;
border-bottom-width: 7px !important;
border-radius: 0 !important;
color: #fff !important;
font-family: "PressStart2P", "VT323", sans-serif !important;
font-size: 0.62rem !important;
font-size: 0.75rem !important;
/* Increased for better readability */
letter-spacing: 0.04em;
line-height: 1.4;
text-shadow: 1px 1px 0 #000;
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.48);
transition: transform 0.08s ease, filter 0.08s ease;
text-transform: uppercase;
padding: 0.5rem 1rem !important;
}
:root[data-theme="minecraft"] button:hover:not(:disabled) {
filter: brightness(1.07);
transform: translateY(-1px);
:root[data-theme="minecraft"] button:not(.mc-reset):hover:not(:disabled),
:root[data-theme="minecraft"] .mc-btn:hover:not(:disabled) {
filter: brightness(1.15);
transform: translateY(-2px);
}
:root[data-theme="minecraft"] button:active:not(:disabled) {
:root[data-theme="minecraft"] button:not(.mc-reset):active:not(:disabled),
:root[data-theme="minecraft"] .mc-btn:active:not(:disabled) {
border-bottom-width: 3px !important;
transform: translateY(3px);
transform: translateY(4px);
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.48);
}
:root[data-theme="minecraft"] button:disabled {
opacity: 0.68;
filter: saturate(0.28);
:root[data-theme="minecraft"] button:not(.mc-reset):disabled,
:root[data-theme="minecraft"] .mc-btn:disabled {
opacity: 0.6;
filter: grayscale(0.8);
cursor: not-allowed;
transform: none !important;
}
/* Variant: Primary/Diamond */
:root[data-theme="minecraft"] .mc-btn-primary {
background: linear-gradient(180deg, var(--mc-diamond) 0%, #008ba3 100%) !important;
}
/* Variant: Danger/Red */
:root[data-theme="minecraft"] .mc-btn-danger {
background: linear-gradient(180deg, var(--mc-red) 0%, #c62828 100%) !important;
}
/* Variant: Success/Emerald */
:root[data-theme="minecraft"] .mc-btn-success {
background: linear-gradient(180deg, var(--mc-grass-top) 0%, var(--mc-grass-dark) 100%) !important;
}
:root[data-theme="minecraft"] input,
:root[data-theme="minecraft"] textarea,
:root[data-theme="minecraft"] select {
@@ -290,3 +325,42 @@
:root[data-theme="minecraft"] .problem-markdown-compact th {
background: #3a3a3a !important;
}
/* ── Problem detail page markdown: dark text on light plank background ── */
:root[data-theme="minecraft"] .problem-markdown,
:root[data-theme="minecraft"] .problem-markdown-compact {
font-family: "MiSans", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
:root[data-theme="minecraft"] .problem-markdown,
:root[data-theme="minecraft"] .problem-markdown article {
color: #3e2723 !important;
}
:root[data-theme="minecraft"] .problem-markdown h1,
:root[data-theme="minecraft"] .problem-markdown h2,
:root[data-theme="minecraft"] .problem-markdown h3 {
color: #3e2723 !important;
}
:root[data-theme="minecraft"] .problem-markdown p,
:root[data-theme="minecraft"] .problem-markdown li,
:root[data-theme="minecraft"] .problem-markdown span,
:root[data-theme="minecraft"] .problem-markdown td,
:root[data-theme="minecraft"] .problem-markdown blockquote {
color: #4e342e !important;
}
:root[data-theme="minecraft"] .problem-markdown th {
color: #3e2723 !important;
background: #d7ccc8 !important;
}
:root[data-theme="minecraft"] .problem-markdown code:not([class*="hljs"]) {
color: #4e342e !important;
background: #d7ccc8 !important;
}
:root[data-theme="minecraft"] .problem-markdown a {
color: #1565c0 !important;
}

查看文件

@@ -1,7 +1,7 @@
export type ThemeId = "default" | "minecraft";
export type UiLanguage = "en" | "zh";
export const DEFAULT_THEME: ThemeId = "default";
export const DEFAULT_THEME: ThemeId = "minecraft";
export const DEFAULT_LANGUAGE: UiLanguage = "zh";
export type ThemeMessages = Record<string, string>;