mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-04 15:50:51 +00:00
feat(问题编辑): add problem-editor page
- 添加了编辑问题描述、解决方案、详细信息、代码模板和测试用例的组件 - 实现了问题编辑页面的基本布局和功能 - 增加了富文本预览和对比功能 - 支持多种编程语言的代码编辑- 提供了测试用例的添加和删除功能
This commit is contained in:
parent
4e6ab68566
commit
4141f0c017
32
src/app/(app)/problem-editor/[problemId]/page.tsx
Normal file
32
src/app/(app)/problem-editor/[problemId]/page.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout";
|
||||||
|
import { EditDescriptionPanel } from "@/components/creater/edit-description-panel";
|
||||||
|
import { EditSolutionPanel } from "@/components/creater/edit-solution-panel";
|
||||||
|
import { EditTestcasePanel } from "@/components/creater/edit-testcase-panel";
|
||||||
|
import { EditDetailPanel } from "@/components/creater/edit-detail-panel";
|
||||||
|
import { EditCodePanel } from "@/components/creater/edit-code-panel";
|
||||||
|
|
||||||
|
interface ProblemEditorPageProps {
|
||||||
|
params: Promise<{ problemId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProblemEditorPage({
|
||||||
|
params,
|
||||||
|
}: ProblemEditorPageProps) {
|
||||||
|
const { problemId } = await params;
|
||||||
|
|
||||||
|
const components: Record<string, React.ReactNode> = {
|
||||||
|
description: <EditDescriptionPanel problemId={problemId} />,
|
||||||
|
solution: <EditSolutionPanel problemId={problemId} />,
|
||||||
|
detail: <EditDetailPanel problemId={problemId} />,
|
||||||
|
code: <EditCodePanel problemId={problemId} />,
|
||||||
|
testcase: <EditTestcasePanel problemId={problemId} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full">
|
||||||
|
<ProblemFlexLayout components={components} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,109 +1,80 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useState } from "react";
|
||||||
import { getProblemData } from '@/app/actions/getProblem';
|
import { Label } from "@/components/ui/label";
|
||||||
import { updateProblemTemplate } from '@/components/creater/problem-maintain';
|
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 { CoreEditor } from "@/components/core-editor";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { CoreEditor } from '@/components/core-editor';
|
|
||||||
import { Language } from '@/generated/client';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
interface Template {
|
interface Template {
|
||||||
|
id: string;
|
||||||
language: string;
|
language: string;
|
||||||
content: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditCodePanelProps {
|
interface EditCodePanelProps {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditCodePanel({ problemId }: EditCodePanelProps) {
|
export const EditCodePanel = ({
|
||||||
const [codeTemplate, setCodeTemplate] = useState<Template>({
|
|
||||||
language: 'cpp',
|
|
||||||
content: `// 默认代码模板 for Problem ${problemId}`,
|
|
||||||
});
|
|
||||||
const [templates, setTemplates] = useState<Template[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchTemplates() {
|
|
||||||
try {
|
|
||||||
const problem = await getProblemData(problemId);
|
|
||||||
setTemplates(problem.templates);
|
|
||||||
const sel = problem.templates.find(t => t.language === 'cpp') || problem.templates[0];
|
|
||||||
if (sel) setCodeTemplate(sel);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载问题数据失败:', err);
|
|
||||||
toast.error('加载问题数据失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchTemplates();
|
|
||||||
}, [problemId]);
|
|
||||||
|
|
||||||
const handleLanguageChange = (language: string) => {
|
|
||||||
const sel = templates.find(t => t.language === language);
|
|
||||||
if (sel) setCodeTemplate(sel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const res = await updateProblemTemplate(
|
|
||||||
problemId,
|
problemId,
|
||||||
codeTemplate.language as Language,
|
}: EditCodePanelProps) => {
|
||||||
codeTemplate.content
|
const [language, setLanguage] = useState("typescript");
|
||||||
);
|
const [templates, setTemplates] = useState<Template[]>([
|
||||||
if (res.success) {
|
{
|
||||||
toast.success('保存成功');
|
id: "1",
|
||||||
} else {
|
language: "typescript",
|
||||||
toast.error('保存失败');
|
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}`
|
||||||
}
|
},
|
||||||
} catch (error) {
|
{
|
||||||
console.error('保存异常:', error);
|
id: "2",
|
||||||
toast.error('保存异常');
|
language: "python",
|
||||||
|
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 []"
|
||||||
}
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentTemplate = templates.find(t => t.language === language) || templates[0];
|
||||||
|
|
||||||
|
const handleCodeChange = (value: string | undefined) => {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
setTemplates(templates.map(t =>
|
||||||
|
t.language === language
|
||||||
|
? { ...t, code: value }
|
||||||
|
: t
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>代码模板</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="language-select">编程语言</Label>
|
<Label htmlFor="language">编程语言</Label>
|
||||||
<select
|
<Select value={language} onValueChange={setLanguage}>
|
||||||
id="language-select"
|
<SelectTrigger id="language">
|
||||||
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
|
<SelectValue placeholder="选择编程语言" />
|
||||||
value={codeTemplate.language}
|
</SelectTrigger>
|
||||||
onChange={e => handleLanguageChange(e.target.value)}
|
<SelectContent>
|
||||||
>
|
<SelectItem value="typescript">TypeScript</SelectItem>
|
||||||
{templates.map(t => (
|
<SelectItem value="javascript">JavaScript</SelectItem>
|
||||||
<option key={t.language} value={t.language}>
|
<SelectItem value="python">Python</SelectItem>
|
||||||
{t.language.toUpperCase()}
|
<SelectItem value="java">Java</SelectItem>
|
||||||
</option>
|
<SelectItem value="cpp">C++</SelectItem>
|
||||||
))}
|
</SelectContent>
|
||||||
</select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="code-editor">代码模板内容</Label>
|
|
||||||
<div className="border rounded-md h-[500px]">
|
<div className="border rounded-md h-[500px]">
|
||||||
|
{currentTemplate && (
|
||||||
<CoreEditor
|
<CoreEditor
|
||||||
language={codeTemplate.language}
|
language={language}
|
||||||
value={codeTemplate.content}
|
value={currentTemplate.code}
|
||||||
path={`/${problemId}.${codeTemplate.language}`}
|
path={`/${problemId}.${language}`}
|
||||||
onChange={value =>
|
onChange={handleCodeChange}
|
||||||
setCodeTemplate({ ...codeTemplate, content: value || '' })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleSave}>保存代码模板</Button>
|
<Button>保存代码模板</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
@ -1,173 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { useState } 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 { Textarea } from "@/components/ui/textarea";
|
||||||
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 MdxPreview from "@/components/mdx-preview";
|
import MdxPreview from "@/components/mdx-preview";
|
||||||
import { getProblemData } from "@/app/actions/getProblem";
|
|
||||||
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
|
||||||
import { Accordion } from "@/components/ui/accordion";
|
|
||||||
import { VideoEmbed } from "@/components/content/video-embed";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { updateProblemDescription, updateProblemTitle } from '@/components/creater/problem-maintain';
|
|
||||||
import { Locale } from "@/generated/client";
|
|
||||||
|
|
||||||
export default function EditDescriptionPanel({ problemId }: { problemId: string }) {
|
interface EditDescriptionPanelProps {
|
||||||
const [locales, setLocales] = useState<string[]>([]);
|
problemId: string;
|
||||||
const [currentLocale, setCurrentLocale] = useState<string>("");
|
}
|
||||||
const [customLocale, setCustomLocale] = useState("");
|
|
||||||
|
|
||||||
const [description, setDescription] = useState({ title: "", content: "" });
|
export const EditDescriptionPanel = ({
|
||||||
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
problemId,
|
||||||
|
}: EditDescriptionPanelProps) => {
|
||||||
useEffect(() => {
|
const [title, setTitle] = useState(`Problem ${problemId} Title`);
|
||||||
async function fetchLocales() {
|
const [content, setContent] = useState(`Problem ${problemId} Description Content...`);
|
||||||
try {
|
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit');
|
||||||
const langs = await getProblemLocales(problemId);
|
|
||||||
setLocales(langs);
|
|
||||||
if (langs.length > 0) setCurrentLocale(langs[0]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('获取语言列表失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchLocales();
|
|
||||||
}, [problemId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentLocale) return;
|
|
||||||
async function fetchProblem() {
|
|
||||||
try {
|
|
||||||
const data = await getProblemData(problemId, currentLocale);
|
|
||||||
setDescription({ title: data?.title || "", content: data?.description || "" });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('加载题目描述失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchProblem();
|
|
||||||
}, [problemId, currentLocale]);
|
|
||||||
|
|
||||||
const handleAddCustomLocale = () => {
|
|
||||||
if (customLocale && !locales.includes(customLocale)) {
|
|
||||||
const newLocales = [...locales, customLocale];
|
|
||||||
setLocales(newLocales);
|
|
||||||
setCurrentLocale(customLocale);
|
|
||||||
setCustomLocale("");
|
|
||||||
setDescription({ title: "", content: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
|
||||||
if (!currentLocale) {
|
|
||||||
toast.error('请选择语言');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const locale = currentLocale as Locale;
|
|
||||||
const resTitle = await updateProblemTitle(problemId, locale, description.title);
|
|
||||||
const resDesc = await updateProblemDescription(problemId, locale, description.content);
|
|
||||||
if (resTitle.success && resDesc.success) {
|
|
||||||
toast.success('保存成功');
|
|
||||||
} else {
|
|
||||||
toast.error('保存失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('保存异常');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<div className="space-y-6">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>题目描述</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{/* 语言切换 */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>选择语言</Label>
|
<Label htmlFor="title">题目标题</Label>
|
||||||
<div className="flex space-x-2">
|
|
||||||
<select
|
|
||||||
value={currentLocale}
|
|
||||||
onChange={(e) => setCurrentLocale(e.target.value)}
|
|
||||||
className="border rounded-md px-3 py-2"
|
|
||||||
>
|
|
||||||
{locales.map((locale) => (
|
|
||||||
<option key={locale} value={locale}>
|
|
||||||
{locale}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="添加新语言"
|
id="title"
|
||||||
value={customLocale}
|
value={title}
|
||||||
onChange={(e) => setCustomLocale(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
/>
|
|
||||||
<Button onClick={handleAddCustomLocale}>添加</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 标题输入 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description-title">标题</Label>
|
|
||||||
<Input
|
|
||||||
id="description-title"
|
|
||||||
value={description.title}
|
|
||||||
onChange={(e) => setDescription({ ...description, 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"}>
|
||||||
{(viewMode === "edit" || viewMode === "compare") && (
|
<Textarea
|
||||||
<div className="relative h-[600px]">
|
id="description"
|
||||||
<CoreEditor
|
value={content}
|
||||||
value={description.content}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onChange={(newVal) => setDescription({ ...description, content: newVal || "" })}
|
placeholder="输入题目详细描述..."
|
||||||
language="markdown"
|
className="min-h-[300px]"
|
||||||
className="absolute inset-0 rounded-md border border-input"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{viewMode !== "edit" && (
|
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||||
<div className="prose dark:prose-invert">
|
<MdxPreview source={content} />
|
||||||
<MdxPreview source={description.content} components={{ Accordion, VideoEmbed }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleSave}>保存更改</Button>
|
<Button>保存更改</Button>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
@ -1,157 +1,67 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import { useState } 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 { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { getProblemData } from "@/app/actions/getProblem";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { updateProblemDetail } from '@/components/creater/problem-maintain';
|
|
||||||
import { Difficulty } from "@/generated/client";
|
|
||||||
|
|
||||||
export default function EditDetailPanel({ problemId }: { problemId: string }) {
|
interface EditDetailPanelProps {
|
||||||
const [problemDetails, setProblemDetails] = useState({
|
problemId: string;
|
||||||
displayId: 1000,
|
}
|
||||||
difficulty: "EASY" as Difficulty,
|
|
||||||
timeLimit: 1000,
|
|
||||||
memoryLimit: 134217728,
|
|
||||||
isPublished: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const EditDetailPanel = ({
|
||||||
async function fetchData() {
|
problemId,
|
||||||
try {
|
}: EditDetailPanelProps) => {
|
||||||
const problemData = await getProblemData(problemId);
|
const [displayId, setDisplayId] = useState(problemId);
|
||||||
setProblemDetails({
|
const [difficulty, setDifficulty] = useState("medium");
|
||||||
displayId: problemData.displayId,
|
const [isPublished, setIsPublished] = useState(true);
|
||||||
difficulty: problemData.difficulty,
|
|
||||||
timeLimit: problemData.timeLimit,
|
|
||||||
memoryLimit: problemData.memoryLimit,
|
|
||||||
isPublished: problemData.isPublished,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取题目信息失败:", error);
|
|
||||||
toast.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 as Difficulty;
|
|
||||||
setProblemDetails({ ...problemDetails, difficulty: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
const res = await updateProblemDetail(problemId, {
|
|
||||||
displayId: problemDetails.displayId,
|
|
||||||
difficulty: problemDetails.difficulty,
|
|
||||||
timeLimit: problemDetails.timeLimit,
|
|
||||||
memoryLimit: problemDetails.memoryLimit,
|
|
||||||
isPublished: problemDetails.isPublished,
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
toast.success('保存成功');
|
|
||||||
} else {
|
|
||||||
toast.error('保存失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('保存异常:', err);
|
|
||||||
toast.error('保存异常');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>题目详情</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="display-id">显示ID</Label>
|
<Label htmlFor="display-id">题号</Label>
|
||||||
<Input
|
<Input
|
||||||
id="display-id"
|
id="display-id"
|
||||||
type="number"
|
value={displayId}
|
||||||
value={problemDetails.displayId}
|
onChange={(e) => setDisplayId(e.target.value)}
|
||||||
onChange={(e) => handleNumberInputChange(e, "displayId")}
|
placeholder="输入题号"
|
||||||
placeholder="输入显示ID"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="time-limit">时间限制 (ms)</Label>
|
<Label htmlFor="difficulty">难度</Label>
|
||||||
<Input
|
<Select value={difficulty} onValueChange={setDifficulty}>
|
||||||
id="time-limit"
|
<SelectTrigger id="difficulty">
|
||||||
type="number"
|
<SelectValue placeholder="选择难度" />
|
||||||
value={problemDetails.timeLimit}
|
</SelectTrigger>
|
||||||
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
|
<SelectContent>
|
||||||
placeholder="输入时间限制"
|
<SelectItem value="easy">简单</SelectItem>
|
||||||
/>
|
<SelectItem value="medium">中等</SelectItem>
|
||||||
</div>
|
<SelectItem value="hard">困难</SelectItem>
|
||||||
<div className="space-y-2">
|
</SelectContent>
|
||||||
<Label htmlFor="memory-limit">内存限制 (字节)</Label>
|
</Select>
|
||||||
<Input
|
|
||||||
id="memory-limit"
|
|
||||||
type="number"
|
|
||||||
value={problemDetails.memoryLimit}
|
|
||||||
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
|
|
||||||
placeholder="输入内存限制"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Label htmlFor="is-published" className="text-sm font-normal">
|
||||||
id="is-published"
|
|
||||||
type="checkbox"
|
|
||||||
checked={problemDetails.isPublished}
|
|
||||||
onChange={(e) =>
|
|
||||||
setProblemDetails({ ...problemDetails, isPublished: 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 focus:ring-2"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="is-published" className="text-sm font-medium">
|
|
||||||
是否发布
|
是否发布
|
||||||
</Label>
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="is-published"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isPublished}
|
||||||
|
onChange={(e) => setIsPublished(e.target.checked)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="button" onClick={handleSave}>
|
<div className="flex space-x-2">
|
||||||
保存更改
|
<Button>保存基本信息</Button>
|
||||||
|
<Button variant="outline" type="button">
|
||||||
|
删除题目
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
@ -1,156 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { useState } 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 { Textarea } from "@/components/ui/textarea";
|
||||||
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 MdxPreview from "@/components/mdx-preview";
|
import MdxPreview from "@/components/mdx-preview";
|
||||||
import { getProblemData } from "@/app/actions/getProblem";
|
|
||||||
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
|
||||||
import { Accordion } from "@/components/ui/accordion";
|
|
||||||
import { VideoEmbed } from "@/components/content/video-embed";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { updateProblemSolution } from '@/components/creater/problem-maintain';
|
|
||||||
import { Locale } from "@/generated/client";
|
|
||||||
|
|
||||||
export default function EditSolutionPanel({ problemId }: { problemId: string }) {
|
interface EditSolutionPanelProps {
|
||||||
const [locales, setLocales] = useState<string[]>([]);
|
problemId: string;
|
||||||
const [currentLocale, setCurrentLocale] = useState<string>("");
|
}
|
||||||
const [customLocale, setCustomLocale] = useState("");
|
|
||||||
|
|
||||||
const [solution, setSolution] = useState({ title: "", content: "" });
|
export const EditSolutionPanel = ({
|
||||||
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
problemId,
|
||||||
|
}: EditSolutionPanelProps) => {
|
||||||
useEffect(() => {
|
const [title, setTitle] = useState(`Solution for Problem ${problemId}`);
|
||||||
async function fetchLocales() {
|
const [content, setContent] = useState(`Solution content for Problem ${problemId}...`);
|
||||||
try {
|
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit');
|
||||||
const langs = await getProblemLocales(problemId);
|
|
||||||
setLocales(langs);
|
|
||||||
if (langs.length > 0) setCurrentLocale(langs[0]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('获取语言列表失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchLocales();
|
|
||||||
}, [problemId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentLocale) return;
|
|
||||||
async function fetchSolution() {
|
|
||||||
try {
|
|
||||||
const data = await getProblemData(problemId, currentLocale);
|
|
||||||
setSolution({ title: (data?.title || "") + " 解析", content: data?.solution || "" });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('加载题目解析失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchSolution();
|
|
||||||
}, [problemId, currentLocale]);
|
|
||||||
|
|
||||||
const handleAddCustomLocale = () => {
|
|
||||||
if (customLocale && !locales.includes(customLocale)) {
|
|
||||||
setLocales(prev => [...prev, customLocale]);
|
|
||||||
setCurrentLocale(customLocale);
|
|
||||||
setCustomLocale("");
|
|
||||||
setSolution({ title: "", content: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
|
||||||
if (!currentLocale) {
|
|
||||||
toast.error('请选择语言');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const locale = currentLocale as Locale;
|
|
||||||
const res = await updateProblemSolution(problemId, locale, solution.content);
|
|
||||||
if (res.success) {
|
|
||||||
toast.success('保存成功');
|
|
||||||
} else {
|
|
||||||
toast.error('保存失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('保存异常');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<div className="space-y-6">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>题目解析</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{/* 语言切换 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>选择语言</Label>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<select
|
|
||||||
value={currentLocale}
|
|
||||||
onChange={(e) => setCurrentLocale(e.target.value)}
|
|
||||||
className="border rounded-md px-3 py-2"
|
|
||||||
>
|
|
||||||
{locales.map((locale) => (
|
|
||||||
<option key={locale} value={locale}>
|
|
||||||
{locale}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<Input
|
|
||||||
placeholder="添加新语言"
|
|
||||||
value={customLocale}
|
|
||||||
onChange={(e) => setCustomLocale(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button type="button" onClick={handleAddCustomLocale}>添加</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 标题输入 (仅展示) */}
|
|
||||||
<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={solution.title}
|
value={title}
|
||||||
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="输入题解标题"
|
placeholder="输入题解标题"
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 编辑/预览切换 */}
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button type="button" variant={viewMode === "edit" ? "default" : "outline"} onClick={() => setViewMode("edit")}>编辑</Button>
|
<Button
|
||||||
<Button type="button" variant={viewMode === "preview" ? "default" : "outline"} onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")}>
|
type="button"
|
||||||
{viewMode === "preview" ? "取消" : "预览"}
|
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>
|
</Button>
|
||||||
<Button type="button" variant={viewMode === "compare" ? "default" : "outline"} onClick={() => setViewMode("compare")}>对比</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"}>
|
||||||
{(viewMode === "edit" || viewMode === "compare") && (
|
<Textarea
|
||||||
<div className="relative h-[600px]">
|
id="solution-content"
|
||||||
<CoreEditor
|
value={content}
|
||||||
value={solution.content}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onChange={(val) => setSolution({ ...solution, content: val || "" })}
|
placeholder="输入详细题解内容..."
|
||||||
language="markdown"
|
className="min-h-[300px]"
|
||||||
className="absolute inset-0 rounded-md border border-input"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{viewMode !== "edit" && (
|
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||||
<div className="prose dark:prose-invert">
|
<MdxPreview source={content} />
|
||||||
<MdxPreview source={solution.content} components={{ Accordion, VideoEmbed }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="button" onClick={handleSave}>保存更改</Button>
|
<Button>保存题解</Button>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
@ -1,224 +1,115 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import { useState } 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 { 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
import { toast } from "sonner";
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
interface Testcase {
|
interface TestCase {
|
||||||
id: string;
|
id: string;
|
||||||
|
input: string;
|
||||||
expectedOutput: string;
|
expectedOutput: string;
|
||||||
inputs: { name: string; value: string }[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditTestcasePanel({ problemId }: { problemId: string }) {
|
interface EditTestcasePanelProps {
|
||||||
const [testcases, setTestcases] = useState<Testcase[]>([]);
|
problemId: string;
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
|
||||||
|
|
||||||
// 加载测试用例
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetch() {
|
|
||||||
try {
|
|
||||||
const data = await getProblemData(problemId);
|
|
||||||
setTestcases(data.testcases || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("加载测试用例失败:", err);
|
|
||||||
toast.error("加载测试用例失败");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fetch();
|
|
||||||
}, [problemId]);
|
|
||||||
|
|
||||||
// 本地添加测试用例
|
export const EditTestcasePanel = ({
|
||||||
const handleAddTestcase = () =>
|
problemId,
|
||||||
setTestcases((prev) => [
|
}: EditTestcasePanelProps) => {
|
||||||
...prev,
|
const [testcases, setTestcases] = useState<TestCase[]>([
|
||||||
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: "", inputs: [{ name: "input1", value: "" }] },
|
{ id: "1", input: "input1", expectedOutput: "output1" },
|
||||||
|
{ id: "2", input: "input2", expectedOutput: "output2" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// AI 生成测试用例
|
const [newInput, setNewInput] = useState("");
|
||||||
const handleAITestcase = async () => {
|
const [newOutput, setNewOutput] = useState("");
|
||||||
setIsGenerating(true);
|
|
||||||
try {
|
const addTestCase = () => {
|
||||||
const ai = await generateAITestcase({ problemId });
|
if (!newInput || !newOutput) return;
|
||||||
setTestcases((prev) => [
|
|
||||||
...prev,
|
const newTestCase = {
|
||||||
{ id: `new-${Date.now()}-${Math.random()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs },
|
id: (testcases.length + 1).toString(),
|
||||||
]);
|
input: newInput,
|
||||||
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
expectedOutput: newOutput
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error("AI 生成测试用例失败");
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除测试用例(本地 + 服务器)
|
setTestcases([...testcases, newTestCase]);
|
||||||
const handleRemoveTestcase = async (idx: number) => {
|
setNewInput("");
|
||||||
const tc = testcases[idx];
|
setNewOutput("");
|
||||||
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));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改预期输出
|
const deleteTestCase = (id: string) => {
|
||||||
const handleExpectedOutputChange = (idx: number, val: string) =>
|
setTestcases(testcases.filter(tc => tc.id !== id));
|
||||||
setTestcases((prev) => {
|
|
||||||
const c = [...prev];
|
|
||||||
c[idx] = { ...c[idx], expectedOutput: val };
|
|
||||||
return c;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 修改输入参数
|
|
||||||
const handleInputChange = (
|
|
||||||
tIdx: number,
|
|
||||||
iIdx: number,
|
|
||||||
field: "name" | "value",
|
|
||||||
val: string
|
|
||||||
) =>
|
|
||||||
setTestcases((prev) => {
|
|
||||||
const c = [...prev];
|
|
||||||
const newInputs = [...c[tIdx].inputs];
|
|
||||||
newInputs[iIdx] = { ...newInputs[iIdx], [field]: val };
|
|
||||||
c[tIdx] = { ...c[tIdx], inputs: newInputs };
|
|
||||||
return c;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加输入参数
|
|
||||||
const handleAddInput = (tIdx: number) =>
|
|
||||||
setTestcases((prev) => {
|
|
||||||
const c = [...prev];
|
|
||||||
const inputs = [...c[tIdx].inputs, { name: `input${c[tIdx].inputs.length + 1}`, value: "" }];
|
|
||||||
c[tIdx] = { ...c[tIdx], inputs };
|
|
||||||
return c;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 删除输入参数
|
|
||||||
const handleRemoveInput = (tIdx: number, iIdx: number) =>
|
|
||||||
setTestcases((prev) => {
|
|
||||||
const c = [...prev];
|
|
||||||
const inputs = c[tIdx].inputs.filter((_, i) => i !== iIdx);
|
|
||||||
c[tIdx] = { ...c[tIdx], inputs };
|
|
||||||
return c;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存所有测试用例,并刷新最新数据
|
|
||||||
const handleSaveAll = async () => {
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < testcases.length; i++) {
|
|
||||||
const tc = testcases[i];
|
|
||||||
if (tc.expectedOutput.trim() === "" || tc.inputs.some(inp => !inp.name.trim() || !inp.value.trim())) {
|
|
||||||
toast.error(`第 ${i + 1} 个测试用例存在空的输入或输出,保存失败`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tc.id.startsWith("new-")) {
|
|
||||||
const res = await addProblemTestcase(problemId, tc.expectedOutput, tc.inputs);
|
|
||||||
if (res.success) {
|
|
||||||
toast.success(`新增测试用例 ${i + 1} 成功`);
|
|
||||||
} else {
|
|
||||||
toast.error(`新增测试用例 ${i + 1} 失败`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await updateProblemTestcase(problemId, tc.id, tc.expectedOutput, tc.inputs);
|
|
||||||
if (res.success) toast.success(`更新测试用例 ${i + 1} 成功`);
|
|
||||||
else toast.error(`更新测试用例 ${i + 1} 失败`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存完成后刷新最新测试用例
|
|
||||||
const data = await getProblemData(problemId);
|
|
||||||
setTestcases(data.testcases || []);
|
|
||||||
toast.success("测试用例保存并刷新成功");
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error("保存测试用例异常");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<div className="space-y-6">
|
||||||
<CardHeader>
|
<div className="flex space-x-2">
|
||||||
<CardTitle>测试用例</CardTitle>
|
<div className="flex-1 space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<Label htmlFor="testcase-input">输入</Label>
|
||||||
<Button onClick={handleAddTestcase}>添加测试用例</Button>
|
<Input
|
||||||
<Button onClick={handleAITestcase} disabled={isGenerating}>
|
id="testcase-input"
|
||||||
{isGenerating ? "生成中..." : "使用AI生成测试用例"}
|
value={newInput}
|
||||||
</Button>
|
onChange={(e) => setNewInput(e.target.value)}
|
||||||
<Button variant="secondary" onClick={handleSaveAll}>
|
placeholder="输入测试用例"
|
||||||
保存测试用例
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Label htmlFor="testcase-output">预期输出</Label>
|
||||||
|
<Input
|
||||||
|
id="testcase-output"
|
||||||
|
value={newOutput}
|
||||||
|
onChange={(e) => setNewOutput(e.target.value)}
|
||||||
|
placeholder="预期输出"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={addTestCase} className="self-end">
|
||||||
|
添加
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
<div className="border rounded-md">
|
||||||
{testcases.map((tc, idx) => (
|
<Table>
|
||||||
<div key={tc.id} className="border p-4 rounded space-y-4">
|
<TableHeader>
|
||||||
<div className="flex justify-between items-center">
|
<TableRow>
|
||||||
<h3 className="font-medium">测试用例 {idx + 1}</h3>
|
<TableHead>编号</TableHead>
|
||||||
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}>
|
<TableHead>输入</TableHead>
|
||||||
|
<TableHead>预期输出</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{testcases.map((tc) => (
|
||||||
|
<TableRow key={tc.id}>
|
||||||
|
<TableCell>{tc.id}</TableCell>
|
||||||
|
<TableCell>{tc.input}</TableCell>
|
||||||
|
<TableCell>{tc.expectedOutput}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteTestCase(tc.id)}
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</TableCell>
|
||||||
<div className="space-y-2">
|
</TableRow>
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button>保存测试用例</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
Loading…
Reference in New Issue
Block a user