feat: expand platform management, admin controls, and learning workflows

这个提交包含在:
Codex CLI
2026-02-15 15:41:56 +08:00
父节点 ad29a9f62d
当前提交 f209ae82da
修改 75 个文件,包含 9663 行新增794 行删除

查看文件

@@ -2,6 +2,12 @@ export const API_BASE =
process.env.NEXT_PUBLIC_API_BASE ??
(process.env.NODE_ENV === "development" ? "http://localhost:8080" : "/admin139");
function uiText(zhText: string, enText: string): string {
if (typeof window === "undefined") return enText;
const lang = window.localStorage.getItem("csp.ui.language");
return lang === "zh" ? zhText : enText;
}
type ApiEnvelope<T> =
| { ok: true; data?: T; [k: string]: unknown }
| { ok: false; error?: string; [k: string]: unknown };
@@ -17,11 +23,45 @@ export async function apiFetch<T>(
headers.set("Content-Type", "application/json");
}
const resp = await fetch(`${API_BASE}${path}`, {
...init,
headers,
cache: "no-store",
});
const method = (init?.method ?? "GET").toUpperCase();
const retryable = method === "GET" || method === "HEAD";
let resp: Response;
try {
resp = await fetch(`${API_BASE}${path}`, {
...init,
headers,
cache: "no-store",
});
} catch (err) {
if (!retryable) {
throw new Error(
uiText(
`网络请求失败,请检查后端服务或代理连接(${err instanceof Error ? err.message : String(err)}`,
`Network request failed. Please check backend/proxy connectivity (${err instanceof Error ? err.message : String(err)}).`
)
);
}
await new Promise((resolve) => setTimeout(resolve, 400));
try {
resp = await fetch(`${API_BASE}${path}`, {
...init,
headers,
cache: "no-store",
});
} catch (retryErr) {
throw new Error(
uiText(
`网络请求失败,请检查后端服务或代理连接(${
retryErr instanceof Error ? retryErr.message : String(retryErr)
}`,
`Network request failed. Please check backend/proxy connectivity (${
retryErr instanceof Error ? retryErr.message : String(retryErr)
}).`
)
);
}
}
const text = await resp.text();
let payload: unknown = null;

查看文件

@@ -0,0 +1,206 @@
export type Cpp14PolicySeverity = "error" | "warning" | "hint";
export type Cpp14PolicyIssue = {
id: string;
severity: Cpp14PolicySeverity;
message: string;
detail: string;
line: number;
column: number;
endLine: number;
endColumn: number;
};
type RegexRule = {
id: string;
severity: Cpp14PolicySeverity;
pattern: RegExp;
message: string;
detail: string;
};
const REGEX_RULES: RegexRule[] = [
{
id: "cpp17-header",
severity: "error",
pattern: /#\s*include\s*<\s*(optional|variant|any|string_view|filesystem|charconv|execution)\s*>/g,
message: "检测到 C++17+ 头文件",
detail: "福建 CSP-J/S 环境通常以 C++14 为准,建议改为 C++14 可用写法。",
},
{
id: "if-constexpr",
severity: "error",
pattern: /\bif\s+constexpr\b/g,
message: "检测到 if constexprC++17",
detail: "请改用普通条件分支或模板特化方案。",
},
{
id: "structured-binding",
severity: "error",
pattern: /\b(?:const\s+)?auto(?:\s*&|\s*&&)?\s*\[[^\]\n]+\]\s*=/g,
message: "检测到结构化绑定C++17",
detail: "可改为 pair.first/second 或自定义结构体字段。",
},
{
id: "cpp17-stdlib",
severity: "error",
pattern: /\bstd::(optional|variant|any|string_view|filesystem|byte|clamp|gcd|lcm)\b/g,
message: "检测到 C++17+ 标准库符号",
detail: "请替换为 C++14 可用实现,避免提交到老版本 GCC 报 CE。",
},
{
id: "void-main",
severity: "error",
pattern: /\bvoid\s+main\s*\(/g,
message: "main 函数返回类型不规范",
detail: "请使用 int main(),并在末尾 return 0;",
},
{
id: "windows-i64d",
severity: "warning",
pattern: /%I64d/g,
message: "检测到 %I64dWindows 特有)",
detail: "Linux 评测机请使用 %lld 读写 long long。",
},
{
id: "windows-int64",
severity: "warning",
pattern: /\b__int64\b/g,
message: "检测到 __int64非标准",
detail: "建议改为标准类型 long long。",
},
{
id: "bits-header",
severity: "warning",
pattern: /#\s*include\s*<\s*bits\/stdc\+\+\.h\s*>/g,
message: "检测到 <bits/stdc++.h>",
detail: "福建实战建议优先使用标准头文件,提升环境兼容性。",
},
];
export const CSP_CPP14_TIPS: string[] = [
"编译标准固定为 C++14建议按 -std=gnu++14 习惯编码),不要使用 C++17+ 特性。",
"main 必须是 int main(),结尾写 return 0;。",
"long long 的 scanf/printf 请使用 %lld,不要使用 %I64d。",
"命名/提交包按考场须知执行:题目目录与源码名使用英文小写。",
"福建二轮常见要求是文件读写freopen,赛前请按官方样例再核对一次。",
];
function buildLineStarts(text: string): number[] {
const starts = [0];
for (let i = 0; i < text.length; i += 1) {
if (text[i] === "\n") starts.push(i + 1);
}
return starts;
}
function offsetToPosition(lineStarts: number[], offset: number): { line: number; column: number } {
let lo = 0;
let hi = lineStarts.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (lineStarts[mid] <= offset) lo = mid + 1;
else hi = mid - 1;
}
const lineIdx = Math.max(0, hi);
return {
line: lineIdx + 1,
column: offset - lineStarts[lineIdx] + 1,
};
}
function pushIssue(
issues: Cpp14PolicyIssue[],
id: string,
severity: Cpp14PolicySeverity,
message: string,
detail: string,
line: number,
column: number,
endLine: number,
endColumn: number
) {
issues.push({ id, severity, message, detail, line, column, endLine, endColumn });
}
export function analyzeCpp14Policy(code: string): Cpp14PolicyIssue[] {
const text = (code ?? "").replace(/\r\n?/g, "\n");
if (!text.trim()) return [];
const issues: Cpp14PolicyIssue[] = [];
const lineStarts = buildLineStarts(text);
for (const rule of REGEX_RULES) {
const matcher = new RegExp(rule.pattern.source, rule.pattern.flags);
let match = matcher.exec(text);
while (match) {
const start = match.index;
const end = start + Math.max(1, match[0].length);
const p1 = offsetToPosition(lineStarts, start);
const p2 = offsetToPosition(lineStarts, end);
pushIssue(
issues,
rule.id,
rule.severity,
rule.message,
rule.detail,
p1.line,
p1.column,
p2.line,
p2.column
);
if (matcher.lastIndex === match.index) matcher.lastIndex += 1;
match = matcher.exec(text);
}
}
if (/\bint\s+main\s*\(/.test(text) && !/\breturn\s+0\s*;/.test(text)) {
const idx = text.search(/\bint\s+main\s*\(/);
const pos = offsetToPosition(lineStarts, Math.max(0, idx));
pushIssue(
issues,
"main-return-zero",
"warning",
"建议在 main 末尾显式 return 0;",
"部分考场与评测环境会严格检查主函数返回行为。",
pos.line,
pos.column,
pos.line,
pos.column + 3
);
}
if (/\blong\s+long\b/.test(text) && /\b(?:scanf|printf)\s*\(/.test(text) && !/%lld/.test(text)) {
const idx = text.search(/\b(?:scanf|printf)\s*\(/);
const pos = offsetToPosition(lineStarts, Math.max(0, idx));
pushIssue(
issues,
"ll-format",
"warning",
"检测到 long long + scanf/printf,建议确认格式符为 %lld",
"Linux 评测环境不支持 %I64d。",
pos.line,
pos.column,
pos.line,
pos.column + 6
);
}
if (!/\bfreopen\s*\(/.test(text) && /\bint\s+main\s*\(/.test(text)) {
const idx = text.search(/\bint\s+main\s*\(/);
const pos = offsetToPosition(lineStarts, Math.max(0, idx));
pushIssue(
issues,
"freopen-tip",
"hint",
"未检测到 freopen福建二轮常见文件读写要求",
"若考场题面要求 *.in/*.out,请按官方文件名补上 freopen。",
pos.line,
pos.column,
pos.line,
pos.column + 3
);
}
return issues;
}

15
frontend/src/lib/i18n.ts 普通文件
查看文件

@@ -0,0 +1,15 @@
"use client";
import { useUiPreferences } from "@/components/ui-preference-provider";
export function readUiLanguage(): "en" | "zh" {
if (typeof window === "undefined") return "en";
return window.localStorage.getItem("csp.ui.language") === "zh" ? "zh" : "en";
}
export function useI18nText() {
const { language } = useUiPreferences();
const isZh = language === "zh";
const tx = (zhText: string, enText: string) => (isZh ? zhText : enText);
return { language, isZh, tx };
}