judge4c/src/components/creater/edit-testcase-panel.tsx
fly6516 6e02c67013 feat(creater): realise problem-editor interactive with database logic
- 在 edit-code-panel、edit-description-panel、edit-detail-panel、edit-solution-panel 和 edit-testcase-panel 组件中添加保存逻辑
- 实现与后端 API 的交互,包括保存代码模板、题目描述、详情、解析和测试用例
-优化错误处理和用户提示,使用 toast 组件显示操作结果
- 调整界面布局和交互细节,提升用户体验
2025-06-20 17:37:00 +08:00

209 lines
8.1 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { generateAITestcase } from "@/app/actions/ai-testcase";
import { getProblemData } from "@/app/actions/getProblem";
import {
addProblemTestcase,
updateProblemTestcase,
deleteProblemTestcase,
} from "@/components/creater/problem-maintain";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
interface Testcase {
id: string;
expectedOutput: string;
inputs: { name: string; value: string }[];
}
export default function EditTestcasePanel({ problemId }: { problemId: string }) {
const [testcases, setTestcases] = useState<Testcase[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
// 1) Load existing testcases
useEffect(() => {
async function fetch() {
try {
const data = await getProblemData(problemId);
setTestcases(data.testcases || []);
} catch (err) {
console.error("加载测试用例失败:", err);
toast.error("加载测试用例失败");
}
}
fetch();
}, [problemId]);
// 2) Local add
const handleAddTestcase = () =>
setTestcases((prev) => [
...prev,
{ id: `new-${Date.now()}`, expectedOutput: "", inputs: [{ name: "input1", value: "" }] },
]);
// 3) AI generation
const handleAITestcase = async () => {
setIsGenerating(true);
try {
const ai = await generateAITestcase({ problemId });
setTestcases((prev) => [
...prev,
{ id: `new-${Date.now()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs },
]);
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
} catch (err) {
console.error(err);
toast.error("AI 生成测试用例失败");
} finally {
setIsGenerating(false);
}
};
// 4) Remove (local + remote if existing)
const handleRemoveTestcase = async (idx: number) => {
const tc = testcases[idx];
if (!tc.id.startsWith("new-")) {
try {
const res = await deleteProblemTestcase(problemId, tc.id);
if (res.success) toast.success("删除测试用例成功");
else toast.error("删除测试用例失败");
} catch (err) {
console.error(err);
toast.error("删除测试用例异常");
}
}
setTestcases((prev) => prev.filter((_, i) => i !== idx));
};
// 5) Field updates
const handleExpectedOutputChange = (idx: number, val: string) =>
setTestcases((prev) => {
const c = [...prev];
c[idx].expectedOutput = val;
return c;
});
const handleInputChange = (
tIdx: number,
iIdx: number,
field: "name" | "value",
val: string
) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs[iIdx][field] = val;
return c;
});
const handleAddInput = (tIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs.push({ name: `input${c[tIdx].inputs.length + 1}`, value: "" });
return c;
});
const handleRemoveInput = (tIdx: number, iIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs.splice(iIdx, 1);
return c;
});
// 6) Persist all changes
const handleSaveAll = async () => {
try {
for (const tc of testcases) {
if (tc.id.startsWith("new-")) {
const res = await addProblemTestcase(problemId, tc.expectedOutput, tc.inputs);
if (res.success) toast.success("新增测试用例成功");
else toast.error("新增测试用例失败");
} else {
const res = await updateProblemTestcase(
problemId,
tc.id,
tc.expectedOutput,
tc.inputs
);
if (res.success) toast.success("更新测试用例成功");
else toast.error("更新测试用例失败");
}
}
} catch (err) {
console.error(err);
toast.error("保存测试用例异常");
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<div className="flex items-center space-x-2">
<Button onClick={handleAddTestcase}></Button>
<Button onClick={handleAITestcase} disabled={isGenerating}>
{isGenerating ? "生成中..." : "使用AI生成测试用例"}
</Button>
<Button variant="secondary" onClick={handleSaveAll}>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
{testcases.map((tc, idx) => (
<div key={tc.id} className="border p-4 rounded space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium"> {idx + 1}</h3>
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}>
</Button>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={tc.expectedOutput}
onChange={(e) => handleExpectedOutputChange(idx, e.target.value)}
placeholder="输入预期输出"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label></Label>
<Button onClick={() => handleAddInput(idx)}></Button>
</div>
{tc.inputs.map((inp, iIdx) => (
<div key={iIdx} className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
value={inp.name}
onChange={(e) => handleInputChange(idx, iIdx, "name", e.target.value)}
placeholder="参数名称"
/>
</div>
<div>
<Label></Label>
<Input
value={inp.value}
onChange={(e) => handleInputChange(idx, iIdx, "value", e.target.value)}
placeholder="参数值"
/>
</div>
{iIdx > 0 && (
<Button variant="outline" onClick={() => handleRemoveInput(idx, iIdx)}>
</Button>
)}
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
);
}