feat: expand platform management, admin controls, and learning workflows
这个提交包含在:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useI18nText } from "@/lib/i18n";
|
||||
|
||||
type Row = {
|
||||
user_id: number;
|
||||
@@ -12,6 +13,7 @@ type Row = {
|
||||
};
|
||||
|
||||
export default function LeaderboardPage() {
|
||||
const { tx } = useI18nText();
|
||||
const [items, setItems] = useState<Row[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -33,34 +35,65 @@ export default function LeaderboardPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-6 py-8">
|
||||
<h1 className="text-2xl font-semibold">全站排行榜</h1>
|
||||
{loading && <p className="mt-3 text-sm text-zinc-500">加载中...</p>}
|
||||
<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")}
|
||||
</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>}
|
||||
|
||||
<div className="mt-4 overflow-x-auto rounded-xl border bg-white">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-zinc-100 text-left">
|
||||
<tr>
|
||||
<th className="px-3 py-2">排名</th>
|
||||
<th className="px-3 py-2">用户</th>
|
||||
<th className="px-3 py-2">Rating</th>
|
||||
<th className="px-3 py-2">注册时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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">
|
||||
{new Date(row.created_at * 1000).toLocaleString()}
|
||||
</td>
|
||||
<div className="mt-4 rounded-xl border bg-white">
|
||||
<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>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{tx("注册时间:", "Registered: ")}
|
||||
{new Date(row.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
{!loading && items.length === 0 && (
|
||||
<p className="px-3 py-5 text-center text-sm text-zinc-500">
|
||||
{tx("暂无排行数据", "No ranking data 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">
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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">
|
||||
{new Date(row.created_at * 1000).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!loading && items.length === 0 && (
|
||||
<tr>
|
||||
<td className="px-3 py-5 text-center text-zinc-500" colSpan={4}>
|
||||
{tx("暂无排行数据", "No ranking data yet")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
在新工单中引用
屏蔽一个用户