feat: 完成源晶权限与经验系统并优化 me/admin 交互
这个提交包含在:
@@ -3,10 +3,12 @@
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { HintTip } from "@/components/hint-tip";
|
||||
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";
|
||||
import { formatUnixDateTime } from "@/lib/time";
|
||||
import { Activity, HardDrive, Play, RefreshCw, Server } from "lucide-react";
|
||||
|
||||
type ImportJob = {
|
||||
id: number;
|
||||
@@ -55,8 +57,7 @@ type MeProfile = {
|
||||
};
|
||||
|
||||
function fmtTs(v: number | null | undefined): string {
|
||||
if (!v) return "-";
|
||||
return new Date(v * 1000).toLocaleString();
|
||||
return formatUnixDateTime(v);
|
||||
}
|
||||
|
||||
type ImportJobOptions = {
|
||||
@@ -78,6 +79,23 @@ function parseOptions(raw: string): ImportJobOptions | null {
|
||||
}
|
||||
}
|
||||
|
||||
function statusToneClass(raw: string): string {
|
||||
const value = raw.toLowerCase();
|
||||
if (value.includes("queue") || value === "queued" || value === "pending") {
|
||||
return "mc-status-warning text-amber-700";
|
||||
}
|
||||
if (value.includes("run") || value === "running" || value === "processing") {
|
||||
return "mc-status-running text-blue-700";
|
||||
}
|
||||
if (value.includes("success") || value === "done" || value === "completed") {
|
||||
return "mc-status-success text-emerald-700";
|
||||
}
|
||||
if (value.includes("fail") || value === "failed" || value === "error") {
|
||||
return "mc-status-danger text-red-700";
|
||||
}
|
||||
return "mc-status-muted text-zinc-700";
|
||||
}
|
||||
|
||||
export default function ImportsPage() {
|
||||
const { tx } = useI18nText();
|
||||
const [token, setToken] = useState("");
|
||||
@@ -254,15 +272,26 @@ 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 flex items-center gap-2">
|
||||
<HardDrive size={24} />
|
||||
{tx("题库导入/出题任务", "Import / Generation Jobs")}
|
||||
</h1>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<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>
|
||||
<HintTip title={tx("管理说明", "Management Guide")}>
|
||||
{tx(
|
||||
"该页面仅管理员可用。支持 Luogu 导入与本地 PDF + RAG 出题两种模式,建议先小规模验证再扩大批量。",
|
||||
"Admin-only page. Supports Luogu import and Local PDF + RAG generation. Start with a small batch before scaling up."
|
||||
)}
|
||||
</HintTip>
|
||||
</div>
|
||||
|
||||
<section className="mt-4 rounded-xl border bg-white p-4">
|
||||
<h2 className="text-base font-medium">{tx("平台管理快捷入口(原 /admin139)", "Platform Shortcuts (moved from /admin139)")}</h2>
|
||||
<p className="mt-1 text-xs text-zinc-600">
|
||||
{tx("默认管理员账号:admin / whoami139", "Default admin account: admin / whoami139")}
|
||||
{tx(
|
||||
"管理员凭据已配置,请使用授权账号登录。",
|
||||
"Admin credentials are configured separately. Sign in with an authorized account."
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<Link className="rounded border bg-zinc-50 px-3 py-2 text-sm hover:bg-zinc-100" href="/auth">
|
||||
@@ -369,22 +398,24 @@ export default function ImportsPage() {
|
||||
{running ? tx("运行中", "Running") : tx("空闲", "Idle")}
|
||||
</span>
|
||||
</div>
|
||||
{runMode === "luogu" && (
|
||||
<p className="mt-2 text-xs text-zinc-500">
|
||||
{tx(
|
||||
"抓取洛谷 CSP-J/CSP-S/NOIP 标签题;容器重启后可自动触发(可通过环境变量关闭)。",
|
||||
"Fetch Luogu problems tagged CSP-J/CSP-S/NOIP. It can auto-start after container restart (configurable via env)."
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{runMode === "local_pdf_rag" && (
|
||||
<p className="mt-2 text-xs text-zinc-500">
|
||||
{tx(
|
||||
"从本地 PDF 提取文本做 RAG,调用 LLM 生成 CSP-J/S 题目,按现有题库难度分布补齐到目标题量并自动去重跳过。",
|
||||
"Extract text from local PDFs for RAG, then call LLM to generate CSP-J/S problems with dedupe and target distribution."
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 text-xs text-zinc-500 inline-flex items-center gap-2">
|
||||
<span>
|
||||
{runMode === "luogu"
|
||||
? tx("当前模式:Luogu 标签导入。", "Mode: Luogu tag import.")
|
||||
: tx("当前模式:本地 PDF + RAG 出题。", "Mode: Local PDF + RAG generation.")}
|
||||
</span>
|
||||
<HintTip title={tx("模式说明", "Mode Notes")} align="left">
|
||||
{runMode === "luogu"
|
||||
? tx(
|
||||
"抓取洛谷 CSP-J/CSP-S/NOIP 标签题。可按需选择启动前清空历史题库。",
|
||||
"Fetch Luogu problems tagged CSP-J/CSP-S/NOIP. You can choose to clear historical problem sets before start."
|
||||
)
|
||||
: tx(
|
||||
"从本地 PDF 提取文本做 RAG,再调用 LLM 生成题目。系统会按目标规模与去重策略补齐题库。",
|
||||
"Extract text from local PDFs for RAG and generate problems with LLM. The system fills the set based on target size and dedupe strategy."
|
||||
)}
|
||||
</HintTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
@@ -395,7 +426,8 @@ export default function ImportsPage() {
|
||||
{job && (
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
<p>
|
||||
{tx("任务", "Job")} #{job.id} · {tx("状态", "Status")} <b>{job.status}</b> · {tx("触发方式", "Trigger")} {job.trigger}
|
||||
{tx("任务", "Job")} #{job.id} · {tx("状态", "Status")}{" "}
|
||||
<b className={statusToneClass(job.status)}>{job.status}</b> · {tx("触发方式", "Trigger")} {job.trigger}
|
||||
</p>
|
||||
<p className="text-zinc-600">
|
||||
{tx("模式", "Mode")} {jobOpts?.mode || jobOpts?.source || "luogu"} · {tx("线程", "Workers")} {jobOpts?.workers ?? "-"}
|
||||
@@ -452,7 +484,7 @@ export default function ImportsPage() {
|
||||
<p className="font-medium">
|
||||
{tx("明细", "Detail")} #{item.id}
|
||||
</p>
|
||||
<span>{item.status}</span>
|
||||
<span className={statusToneClass(item.status)}>{item.status}</span>
|
||||
</div>
|
||||
<p className="break-all text-zinc-600">{tx("路径:", "Path: ")}{item.source_path}</p>
|
||||
<p className="text-zinc-600">{tx("标题:", "Title: ")}{item.title || "-"}</p>
|
||||
@@ -487,7 +519,7 @@ export default function ImportsPage() {
|
||||
{item.source_path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2">{item.status}</td>
|
||||
<td className={`px-2 py-2 ${statusToneClass(item.status)}`}>{item.status}</td>
|
||||
<td className="max-w-[220px] px-2 py-2">
|
||||
<div className="truncate" title={item.title}>
|
||||
{item.title || "-"}
|
||||
|
||||
在新工单中引用
屏蔽一个用户