feat(problem-editor): add feature to preload problem information if there already has had data in database

- 添加了数据预加载功能,通过 getProblemData API 获取题目信息
- 优化了各个编辑面板的实现,提高了代码复用性和可维护性- 新增了测试用例编辑功能,支持多输入参数的管理
- 改进了题解编辑面板,增加了预览和对比功能
- 统一了表单元素的样式和交互方式,提升了用户体验
This commit is contained in:
fly6516 2025-06-17 15:24:43 +08:00 committed by cfngc4594
parent f63d869403
commit 19824e0877
7 changed files with 590 additions and 389 deletions

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import { ProblemFlexLayout } from '@/features/problems/components/problem-flexlayout'; import { ProblemFlexLayout } from '@/features/problems/components/problem-flexlayout';
import { EditDescriptionPanel } from '@/components/creater/edit-description-panel'; import EditDescriptionPanel from '@/components/creater/edit-description-panel';
import { EditSolutionPanel } from '@/components/creater/edit-solution-panel'; import EditSolutionPanel from '@/components/creater/edit-solution-panel';
import { EditTestcasePanel } from '@/components/creater/edit-testcase-panel'; import EditTestcasePanel from '@/components/creater/edit-testcase-panel';
import { EditDetailPanel } from '@/components/creater/edit-detail-panel'; import EditDetailPanel from '@/components/creater/edit-detail-panel';
import { EditCodePanel } from '@/components/creater/edit-code-panel'; import EditCodePanel from '@/components/creater/edit-code-panel';
import { updateProblem } from '@/app/actions/updateProblem'; import { updateProblem } from '@/app/actions/updateProblem';
interface ProblemEditorPageProps { interface ProblemEditorPageProps {

View File

@ -2,25 +2,18 @@
'use server'; 'use server';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { Locale } from '@/generated/client';
import { serialize } from 'next-mdx-remote/serialize'; import { serialize } from 'next-mdx-remote/serialize';
export async function getProblemData(problemId: string, locale?: string) { export async function getProblemData(problemId: string) {
const selectedLocale = locale as Locale;
const problem = await prisma.problem.findUnique({ const problem = await prisma.problem.findUnique({
where: { id: problemId }, where: { id: problemId },
include: { include: {
localizations: true,
templates: true, templates: true,
testcases: { testcases: {
include: { inputs: true } include: { inputs: true }
}, }
localizations: { }
where: {
locale: selectedLocale,
},
},
},
}); });
if (!problem) { if (!problem) {
@ -32,7 +25,9 @@ export async function getProblemData(problemId: string, locale?: string) {
const rawDescription = getContent('DESCRIPTION'); const rawDescription = getContent('DESCRIPTION');
// MDX序列化给客户端渲染用
const mdxDescription = await serialize(rawDescription, { const mdxDescription = await serialize(rawDescription, {
// 可以根据需要添加MDX插件配置
parseFrontmatter: false, parseFrontmatter: false,
}); });
@ -45,19 +40,19 @@ export async function getProblemData(problemId: string, locale?: string) {
memoryLimit: problem.memoryLimit, memoryLimit: problem.memoryLimit,
title: getContent('TITLE'), title: getContent('TITLE'),
description: rawDescription, description: rawDescription,
mdxDescription, mdxDescription, // 新增序列化后的字段
solution: getContent('SOLUTION'), solution: getContent('SOLUTION'),
templates: problem.templates.map(t => ({ templates: problem.templates.map(t => ({
language: t.language, language: t.language,
content: t.content, content: t.content
})), })),
testcases: problem.testcases.map(tc => ({ testcases: problem.testcases.map(tc => ({
id: tc.id, id: tc.id,
expectedOutput: tc.expectedOutput, expectedOutput: tc.expectedOutput,
inputs: tc.inputs.map(input => ({ inputs: tc.inputs.map(input => ({
name: input.name, name: input.name,
value: input.value, value: input.value
})), }))
})), }))
}; };
} }

View File

@ -1,84 +1,121 @@
"use client"; 'use client';
import { useState } from "react"; import { useEffect, useState } from 'react';
import { Label } from "@/components/ui/label"; import { getProblemData } from '@/app/actions/getProblem';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from '@/components/ui/label';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CoreEditor } from "@/components/core-editor"; import { CoreEditor } from "@/components/core-editor";
interface Template { interface Template {
id: string;
language: string; language: string;
code: string; content: string;
} }
interface EditCodePanelProps { interface EditCodePanelProps {
problemId: string; problemId: string;
onUpdate?: (data: { onUpdate?: (data: Template) => Promise<{ success: boolean }>;
content: string;
language: 'c' | 'cpp'; // 移除可选标记
}) => void;
} }
export const EditCodePanel = ({ // 模拟保存函数
problemId, async function saveTemplate(data: Template): Promise<{ success: boolean }> {
}: EditCodePanelProps) => { try {
const [language, setLanguage] = useState("typescript"); console.log('保存模板数据:', data);
const [templates, setTemplates] = useState<Template[]>([ await new Promise(resolve => setTimeout(resolve, 500));
{ return { success: true };
id: "1", } catch {
language: "typescript", return { success: false };
code: `// TypeScript模板示例\nfunction twoSum(nums: number[], target: number): number[] {\n const map = new Map();\n for (let i = 0; i < nums.length; i++) {\n const complement = target - nums[i];\n if (map.has(complement)) {\n return [map.get(complement), i];\n }\n map.set(nums[i], i);\n }\n return [];\n}` }
}, }
{
id: "2", export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: EditCodePanelProps) {
language: "python", const [codeTemplate, setCodeTemplate] = useState<Template>({
code: "# Python模板示例\ndef two_sum(nums, target):\n num_dict = {}\n for i, num in enumerate(nums):\n complement = target - num\n if complement in num_dict:\n return [num_dict[complement], i]\n num_dict[num] = i\n return []" language: 'cpp',
content: `// 默认代码模板 for Problem ${problemId}`,
});
const [templates, setTemplates] = useState<Template[]>([]);
useEffect(() => {
async function fetch() {
try {
const problem = await getProblemData(problemId);
setTemplates(problem.templates);
const cppTemplate = problem.templates.find(t => t.language === 'cpp');
setCodeTemplate(cppTemplate || problem.templates[0]);
} catch (err) {
console.error('加载问题数据失败:', err);
}
} }
]); fetch();
}, [problemId]);
const currentTemplate = templates.find(t => t.language === language) || templates[0]; const handleLanguageChange = (language: string) => {
const selected = templates.find(t => t.language === language);
if (selected) {
setCodeTemplate(selected);
}
};
const handleCodeChange = (value: string | undefined) => { // 事件处理函数返回 Promise<void>,不要返回具体数据
if (!value) return; const handleSave = async (): Promise<void> => {
if (!onUpdate) {
setTemplates(templates.map(t => alert('保存函数未传入,无法保存');
t.language === language return;
? { ...t, code: value } }
: t try {
)); const result = await onUpdate(codeTemplate);
if (result.success) {
alert('保存成功');
} else {
alert('保存失败');
}
} catch (error) {
console.error(error);
alert('保存异常');
}
}; };
return ( return (
<div className="space-y-6"> <Card className="w-full">
<div className="space-y-2"> <CardHeader>
<Label htmlFor="language"></Label> <CardTitle></CardTitle>
<Select value={language} onValueChange={setLanguage}> </CardHeader>
<SelectTrigger id="language"> <CardContent>
<SelectValue placeholder="选择编程语言" /> <div className="space-y-6">
</SelectTrigger> <div className="space-y-2">
<SelectContent> <Label htmlFor="language-select"></Label>
<SelectItem value="typescript">TypeScript</SelectItem> <select
<SelectItem value="javascript">JavaScript</SelectItem> id="language-select"
<SelectItem value="python">Python</SelectItem> className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
<SelectItem value="java">Java</SelectItem> value={codeTemplate.language}
<SelectItem value="cpp">C++</SelectItem> onChange={(e) => handleLanguageChange(e.target.value)}
</SelectContent> >
</Select> {templates.map((t) => (
</div> <option key={t.language} value={t.language}>
{t.language.toUpperCase()}
</option>
))}
</select>
</div>
<div className="border rounded-md h-[500px]"> <div className="space-y-2">
{currentTemplate && ( <Label htmlFor="code-editor"></Label>
<CoreEditor <div className="border rounded-md h-[500px]">
language={language} <CoreEditor
value={currentTemplate.code} language={codeTemplate.language}
path={`/${problemId}.${language}`} value={codeTemplate.content}
onChange={handleCodeChange} path={`/${problemId}.${codeTemplate.language}`}
/> onChange={(value) =>
)} setCodeTemplate({ ...codeTemplate, content: value || '' })
</div> }
/>
</div>
</div>
<Button></Button> <Button onClick={handleSave}></Button>
</div> </div>
</CardContent>
</Card>
); );
}; }

View File

@ -1,85 +1,109 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import MdxPreview from "@/components/mdx-preview"; import MdxPreview from "@/components/mdx-preview";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {CoreEditor} from "@/components/core-editor"; import { CoreEditor } from '@/components/core-editor';
import { getProblemData } from '@/app/actions/getProblem';
import { Accordion } from "@/components/ui/accordion"; // ← 这里导入 Accordion
interface EditDescriptionPanelProps { export default function EditDescriptionPanel({
problemId,
}: {
problemId: string; problemId: string;
onUpdate?: (data: { content: string }) => void; }) {
} const [description, setDescription] = useState({
title: `Description for Problem ${problemId}`,
export const EditDescriptionPanel = ({ content: `Description content for Problem ${problemId}...`
problemId, });
}: EditDescriptionPanelProps) => {
const [title, setTitle] = useState(`Problem ${problemId} Title`);
const [content, setContent] = useState(`Problem ${problemId} Description Content...`);
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit'); const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit');
useEffect(() => {
async function fetchData() {
try {
const problemData = await getProblemData(problemId);
setDescription({
title: problemData.title,
content: problemData.description
});
} catch (error) {
console.error('获取题目信息失败:', error);
}
}
fetchData();
}, [problemId]);
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="title"></Label> <Label htmlFor="description-title"></Label>
<Input <Input
id="title" id="description-title"
value={title} value={description.title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setDescription({...description, title: e.target.value})}
placeholder="输入题目标题" placeholder="输入题目标题"
/> />
</div> </div>
<div className="flex space-x-2">
<Button
type="button"
variant={viewMode === 'edit' ? 'default' : 'outline'}
onClick={() => setViewMode('edit')}
>
</Button>
<Button
type="button"
variant={viewMode === 'preview' ? 'default' : 'outline'}
onClick={() => setViewMode(viewMode === 'preview' ? 'edit' : 'preview')}
>
{viewMode === 'preview' ? '取消' : '预览'}
</Button>
<Button
type="button"
variant={viewMode === 'compare' ? 'default' : 'outline'}
onClick={() => setViewMode('compare')}
>
</Button>
</div>
<div className={viewMode === 'compare' ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div className="flex space-x-2">
<div className={viewMode === 'edit' || viewMode === 'compare' ? "block" : "hidden"}> <Button
<div className="relative h-[600px]"> type="button"
<CoreEditor variant={viewMode === 'edit' ? 'default' : 'outline'}
value={content} onClick={() => setViewMode('edit')}
onChange={setContent} >
language="``"
className="absolute inset-0 rounded-md border border-input" </Button>
/> <Button
type="button"
variant={viewMode === 'preview' ? 'default' : 'outline'}
onClick={() => setViewMode(viewMode === 'preview' ? 'edit' : 'preview')}
>
{viewMode === 'preview' ? '取消' : '预览'}
</Button>
<Button
type="button"
variant={viewMode === 'compare' ? 'default' : 'outline'}
onClick={() => setViewMode('compare')}
>
</Button>
</div>
<div className={viewMode === 'compare' ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}>
<div className={viewMode === 'edit' || viewMode === 'compare' ? "block" : "hidden"}>
<div className="relative h-[600px]">
<CoreEditor
value={description.content}
onChange={(newContent) =>
setDescription({ ...description, content: newContent || '' })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
</div> </div>
{viewMode !== 'edit' && (
<div className="prose dark:prose-invert">
<MdxPreview
source={description.content}
components={{ Accordion }} // ← 这里传入 Accordion
/>
</div>
)}
</div> </div>
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}> <Button></Button>
<MdxPreview source={content} />
</div>
</div> </div>
</CardContent>
<Button></Button> </Card>
</div>
</CardContent>
</Card>
); );
}; }

View File

@ -1,68 +1,137 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { getProblemData } from "@/app/actions/getProblem";
interface EditDetailPanelProps { export default function EditDetailPanel({
problemId,
}: {
problemId: string; problemId: string;
onUpdate?: (data: { content: string }) => void; }) {
} const [problemDetails, setProblemDetails] = useState({
displayId: 1000,
difficulty: "EASY" as "EASY" | "MEDIUM" | "HARD",
timeLimit: 1000,
memoryLimit: 134217728,
isPublished: false,
});
export const EditDetailPanel = ({ useEffect(() => {
problemId, async function fetchData() {
}: EditDetailPanelProps) => { try {
const [displayId, setDisplayId] = useState(problemId); const problemData = await getProblemData(problemId);
const [difficulty, setDifficulty] = useState("medium"); setProblemDetails({
const [isPublished, setIsPublished] = useState(true); displayId: problemData.displayId,
difficulty: problemData.difficulty,
timeLimit: problemData.timeLimit,
memoryLimit: problemData.memoryLimit,
isPublished: problemData.isPublished,
});
} catch (error) {
console.error("获取题目信息失败:", error);
}
}
fetchData();
}, [problemId]);
const handleNumberInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
field: keyof typeof problemDetails
) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value)) {
setProblemDetails({
...problemDetails,
[field]: value,
});
}
};
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value === "EASY" || value === "MEDIUM" || value === "HARD") {
setProblemDetails({ ...problemDetails, difficulty: value });
}
};
return ( return (
<div className="space-y-6"> <Card className="w-full">
<div className="space-y-2"> <CardHeader>
<Label htmlFor="display-id"></Label> <CardTitle></CardTitle>
<Input </CardHeader>
id="display-id" <CardContent>
value={displayId} <div className="space-y-6">
onChange={(e) => setDisplayId(e.target.value)} <div className="grid grid-cols-2 gap-4">
placeholder="输入题号" <div className="space-y-2">
/> <Label htmlFor="display-id">ID</Label>
</div> <Input
id="display-id"
type="number"
value={problemDetails.displayId}
onChange={(e) => handleNumberInputChange(e, "displayId")}
placeholder="输入显示ID"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty-select"></Label>
<select
id="difficulty-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={problemDetails.difficulty}
onChange={handleDifficultyChange}
>
<option value="EASY"></option>
<option value="MEDIUM"></option>
<option value="HARD"></option>
</select>
</div>
</div>
<div className="space-y-2"> <div className="grid grid-cols-2 gap-4">
<Label htmlFor="difficulty"></Label> <div className="space-y-2">
<Select value={difficulty} onValueChange={setDifficulty}> <Label htmlFor="time-limit"> (ms)</Label>
<SelectTrigger id="difficulty"> <Input
<SelectValue placeholder="选择难度" /> id="time-limit"
</SelectTrigger> type="number"
<SelectContent> value={problemDetails.timeLimit}
<SelectItem value="easy"></SelectItem> onChange={(e) => handleNumberInputChange(e, "timeLimit")}
<SelectItem value="medium"></SelectItem> placeholder="输入时间限制"
<SelectItem value="hard"></SelectItem> />
</SelectContent> </div>
</Select> <div className="space-y-2">
</div> <Label htmlFor="memory-limit"> ()</Label>
<Input
id="memory-limit"
type="number"
value={problemDetails.memoryLimit}
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
placeholder="输入内存限制"
/>
</div>
</div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label htmlFor="is-published" className="text-sm font-normal"> <input
id="is-published"
</Label> type="checkbox"
<Input checked={problemDetails.isPublished}
id="is-published" onChange={(e) =>
type="checkbox" setProblemDetails({ ...problemDetails, isPublished: e.target.checked })
checked={isPublished} }
onChange={(e) => setIsPublished(e.target.checked)} className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
className="h-4 w-4" />
/> <Label
</div> htmlFor="is-published"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
<div className="flex space-x-2"> >
<Button></Button>
<Button variant="outline" type="button"> </Label>
</div>
</Button> </div>
</div> </CardContent>
</div> </Card>
); );
}; }

View File

@ -1,85 +1,100 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import MdxPreview from "@/components/mdx-preview"; import MdxPreview from "@/components/mdx-preview";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {CoreEditor} from "@/components/core-editor"; import { CoreEditor } from "@/components/core-editor";
import { getProblemData } from "@/app/actions/getProblem";// 修改为你实际路径
interface EditSolutionPanelProps { export default function EditSolutionPanel({
problemId,
}: {
problemId: string; problemId: string;
onUpdate?: (data: { content: string }) => void; }) {
} const [solution, setSolution] = useState({
title: `Solution for Problem ${problemId}`,
content: `Solution content for Problem ${problemId}...`,
});
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
export const EditSolutionPanel = ({ useEffect(() => {
problemId, async function fetchSolution() {
}: EditSolutionPanelProps) => { try {
const [title, setTitle] = useState(`Solution for Problem ${problemId}`); const data = await getProblemData(problemId);
const [content, setContent] = useState(`Solution content for Problem ${problemId}...`); setSolution({
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit'); title: data.title ? data.title + " 解析" : `Solution for Problem ${problemId}`,
content: data.solution || "",
});
} catch (error) {
console.error("加载题解失败", error);
}
}
fetchSolution();
}, [problemId]);
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="solution-title"></Label> <Label htmlFor="solution-title"></Label>
<Input <Input
id="solution-title" id="solution-title"
value={title} value={solution.title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setSolution({ ...solution, title: e.target.value })}
placeholder="输入题解标题" placeholder="输入题解标题"
/> />
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <Button
type="button" type="button"
variant={viewMode === 'edit' ? 'default' : 'outline'} variant={viewMode === "edit" ? "default" : "outline"}
onClick={() => setViewMode('edit')} onClick={() => setViewMode("edit")}
> >
</Button> </Button>
<Button <Button
type="button" type="button"
variant={viewMode === 'preview' ? 'default' : 'outline'} variant={viewMode === "preview" ? "default" : "outline"}
onClick={() => setViewMode(viewMode === 'preview' ? 'edit' : 'preview')} onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")}
> >
{viewMode === 'preview' ? '取消' : '预览'} {viewMode === "preview" ? "取消" : "预览"}
</Button> </Button>
<Button <Button
type="button" type="button"
variant={viewMode === 'compare' ? 'default' : 'outline'} variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode('compare')} onClick={() => setViewMode("compare")}
> >
</Button> </Button>
</div> </div>
<div className={viewMode === 'compare' ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}> <div className={viewMode === "compare" ? "grid grid-cols-2 gap-6" : "flex flex-col gap-6"}>
<div className={viewMode === 'edit' || viewMode === 'compare' ? "block" : "hidden"}> <div className={viewMode === "edit" || viewMode === "compare" ? "block" : "hidden"}>
<div className="relative h-[600px]"> <div className="relative h-[600px]">
<CoreEditor <CoreEditor
value={content} value={solution.content}
onChange={setContent} onChange={(newContent) => setSolution({ ...solution, content: newContent })}
language="``" language="markdown"
className="absolute inset-0 rounded-md border border-input" className="absolute inset-0 rounded-md border border-input"
/> />
</div>
</div> </div>
</div>
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}> {viewMode !== "edit" && (
<MdxPreview source={content} /> <div className="prose dark:prose-invert">
<MdxPreview source={solution.content} />
</div>
)}
</div> </div>
</div> </div>
</CardContent>
<Button></Button> </Card>
</div>
</CardContent>
</Card>
); );
}; }

View File

@ -1,119 +1,180 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Table, import { getProblemData } from "@/app/actions/getProblem";
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
interface TestCase { export default function EditTestcasePanel({
id: string; problemId,
input: string; }: {
expectedOutput: string;
}
interface EditTestcasePanelProps {
problemId: string; problemId: string;
onUpdate?: (data: { }) {
content: string; const [testcases, setTestcases] = useState<
inputs: Array<{ index: number; name: string; value: string }> Array<{
}) => void; id: string;
} expectedOutput: string;
inputs: Array<{
name: string;
value: string;
}>;
}>
>([]);
export const EditTestcasePanel = ({ useEffect(() => {
problemId, async function fetchData() {
}: EditTestcasePanelProps) => { try {
const [testcases, setTestcases] = useState<TestCase[]>([ const problemData = await getProblemData(problemId);
{ id: "1", input: "input1", expectedOutput: "output1" }, if (problemData && problemData.testcases) {
{ id: "2", input: "input2", expectedOutput: "output2" } setTestcases(problemData.testcases);
]); } else {
setTestcases([]);
}
} catch (error) {
console.error("加载测试用例失败:", error);
setTestcases([]);
}
}
fetchData();
}, [problemId]);
const [newInput, setNewInput] = useState(""); const handleAddTestcase = () => {
const [newOutput, setNewOutput] = useState(""); setTestcases([
...testcases,
const addTestCase = () => { {
if (!newInput || !newOutput) return; id: `new-${Date.now()}`,
expectedOutput: "",
const newTestCase = { inputs: [{ name: "input1", value: "" }],
id: (testcases.length + 1).toString(), },
input: newInput, ]);
expectedOutput: newOutput
};
setTestcases([...testcases, newTestCase]);
setNewInput("");
setNewOutput("");
}; };
const deleteTestCase = (id: string) => { const handleRemoveTestcase = (index: number) => {
setTestcases(testcases.filter(tc => tc.id !== id)); const newTestcases = [...testcases];
newTestcases.splice(index, 1);
setTestcases(newTestcases);
};
const handleInputChange = (
testcaseIndex: number,
inputIndex: number,
field: "name" | "value",
value: string
) => {
const newTestcases = [...testcases];
newTestcases[testcaseIndex].inputs[inputIndex][field] = value;
setTestcases(newTestcases);
};
const handleExpectedOutputChange = (testcaseIndex: number, value: string) => {
const newTestcases = [...testcases];
newTestcases[testcaseIndex].expectedOutput = value;
setTestcases(newTestcases);
};
const handleAddInput = (testcaseIndex: number) => {
const newTestcases = [...testcases];
newTestcases[testcaseIndex].inputs.push({
name: `input${newTestcases[testcaseIndex].inputs.length + 1}`,
value: "",
});
setTestcases(newTestcases);
};
const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => {
const newTestcases = [...testcases];
newTestcases[testcaseIndex].inputs.splice(inputIndex, 1);
setTestcases(newTestcases);
}; };
return ( return (
<div className="space-y-6"> <Card className="w-full">
<div className="flex space-x-2"> <CardHeader>
<div className="flex-1 space-y-2"> <CardTitle></CardTitle>
<Label htmlFor="testcase-input"></Label> <Button type="button" onClick={handleAddTestcase}>
<Input
id="testcase-input" </Button>
value={newInput} </CardHeader>
onChange={(e) => setNewInput(e.target.value)} <CardContent>
placeholder="输入测试用例" <div className="space-y-6">
/> {testcases.map((testcase, index) => (
</div> <div key={testcase.id} className="border p-4 rounded-md space-y-4">
<div className="flex-1 space-y-2"> <div className="flex justify-between items-center">
<Label htmlFor="testcase-output"></Label> <h3 className="text-lg font-medium"> {index + 1}</h3>
<Input <Button
id="testcase-output" type="button"
value={newOutput} variant="destructive"
onChange={(e) => setNewOutput(e.target.value)} onClick={() => handleRemoveTestcase(index)}
placeholder="预期输出" >
/>
</div> </Button>
<Button onClick={addTestCase} className="self-end"> </div>
</Button>
</div>
<div className="border rounded-md"> <div className="space-y-2">
<Table> <Label htmlFor={`expected-output-${index}`}></Label>
<TableHeader> <Input
<TableRow> id={`expected-output-${index}`}
<TableHead></TableHead> value={testcase.expectedOutput}
<TableHead></TableHead> onChange={(e) => handleExpectedOutputChange(index, e.target.value)}
<TableHead></TableHead> placeholder="输入预期输出"
<TableHead></TableHead> />
</TableRow> </div>
</TableHeader>
<TableBody> <div className="space-y-2">
{testcases.map((tc) => ( <div className="flex justify-between items-center">
<TableRow key={tc.id}> <Label></Label>
<TableCell>{tc.id}</TableCell> <Button type="button" onClick={() => handleAddInput(index)}>
<TableCell>{tc.input}</TableCell>
<TableCell>{tc.expectedOutput}</TableCell> </Button>
<TableCell> </div>
<Button
variant="destructive" {testcase.inputs.map((input, inputIndex) => (
size="sm" <div key={input.name} className="grid grid-cols-2 gap-4">
onClick={() => deleteTestCase(tc.id)} <div className="space-y-2">
> <Label htmlFor={`input-name-${index}-${inputIndex}`}>
</Button> </Label>
</TableCell> <Input
</TableRow> id={`input-name-${index}-${inputIndex}`}
value={input.name}
onChange={(e) =>
handleInputChange(index, inputIndex, "name", e.target.value)
}
placeholder="输入参数名称"
/>
</div>
<div className="space-y-2">
<Label htmlFor={`input-value-${index}-${inputIndex}`}>
</Label>
<Input
id={`input-value-${index}-${inputIndex}`}
value={input.value}
onChange={(e) =>
handleInputChange(index, inputIndex, "value", e.target.value)
}
placeholder="输入参数值"
/>
</div>
{inputIndex > 0 && (
<Button
type="button"
variant="outline"
onClick={() => handleRemoveInput(index, inputIndex)}
className="w-full"
>
</Button>
)}
</div>
))}
</div>
</div>
))} ))}
</TableBody> </div>
</Table> </CardContent>
</div> </Card>
<Button></Button>
</div>
); );
}; }