feat: expand platform management, admin controls, and learning workflows
这个提交包含在:
@@ -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;
|
||||
|
||||
206
frontend/src/lib/cpp14-policy.ts
普通文件
206
frontend/src/lib/cpp14-policy.ts
普通文件
@@ -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 constexpr(C++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: "检测到 %I64d(Windows 特有)",
|
||||
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
普通文件
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 };
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户