mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-03 23:30:50 +00:00
feat(problem-editor): add feature to preload problem information if there already has had data in database
- 添加了数据预加载功能,通过 getProblemData API 获取题目信息 - 优化了各个编辑面板的实现,提高了代码复用性和可维护性- 新增了测试用例编辑功能,支持多输入参数的管理 - 改进了题解编辑面板,增加了预览和对比功能 - 统一了表单元素的样式和交互方式,提升了用户体验
This commit is contained in:
parent
956a37d825
commit
95a1817419
@ -1,11 +1,11 @@
|
||||
"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';
|
||||
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';
|
||||
import { updateProblem } from '@/app/actions/updateProblem';
|
||||
|
||||
interface ProblemEditorPageProps {
|
||||
|
58
src/app/actions/getProblem.ts
Normal file
58
src/app/actions/getProblem.ts
Normal file
@ -0,0 +1,58 @@
|
||||
// app/actions/get-problem-data.ts
|
||||
'use server';
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { serialize } from 'next-mdx-remote/serialize';
|
||||
|
||||
export async function getProblemData(problemId: string) {
|
||||
const problem = await prisma.problem.findUnique({
|
||||
where: { id: problemId },
|
||||
include: {
|
||||
localizations: true,
|
||||
templates: true,
|
||||
testcases: {
|
||||
include: { inputs: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!problem) {
|
||||
throw new Error('Problem not found');
|
||||
}
|
||||
|
||||
const getContent = (type: string) =>
|
||||
problem.localizations.find(loc => loc.type === type)?.content || '';
|
||||
|
||||
const rawDescription = getContent('DESCRIPTION');
|
||||
|
||||
// MDX序列化,给客户端渲染用
|
||||
const mdxDescription = await serialize(rawDescription, {
|
||||
// 可以根据需要添加MDX插件配置
|
||||
parseFrontmatter: false,
|
||||
});
|
||||
|
||||
return {
|
||||
id: problem.id,
|
||||
displayId: problem.displayId,
|
||||
difficulty: problem.difficulty,
|
||||
isPublished: problem.isPublished,
|
||||
timeLimit: problem.timeLimit,
|
||||
memoryLimit: problem.memoryLimit,
|
||||
title: getContent('TITLE'),
|
||||
description: rawDescription,
|
||||
mdxDescription, // 新增序列化后的字段
|
||||
solution: getContent('SOLUTION'),
|
||||
templates: problem.templates.map(t => ({
|
||||
language: t.language,
|
||||
content: t.content
|
||||
})),
|
||||
testcases: problem.testcases.map(tc => ({
|
||||
id: tc.id,
|
||||
expectedOutput: tc.expectedOutput,
|
||||
inputs: tc.inputs.map(input => ({
|
||||
name: input.name,
|
||||
value: input.value
|
||||
}))
|
||||
}))
|
||||
};
|
||||
}
|
@ -1,84 +1,121 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getProblemData } from '@/app/actions/getProblem';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { CoreEditor } from "@/components/core-editor";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
language: string;
|
||||
code: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface EditCodePanelProps {
|
||||
problemId: string;
|
||||
onUpdate?: (data: {
|
||||
content: string;
|
||||
language: 'c' | 'cpp'; // 移除可选标记
|
||||
}) => void;
|
||||
onUpdate?: (data: Template) => Promise<{ success: boolean }>;
|
||||
}
|
||||
|
||||
export const EditCodePanel = ({
|
||||
problemId,
|
||||
}: EditCodePanelProps) => {
|
||||
const [language, setLanguage] = useState("typescript");
|
||||
const [templates, setTemplates] = useState<Template[]>([
|
||||
{
|
||||
id: "1",
|
||||
language: "typescript",
|
||||
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",
|
||||
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];
|
||||
// 模拟保存函数
|
||||
async function saveTemplate(data: Template): Promise<{ success: boolean }> {
|
||||
try {
|
||||
console.log('保存模板数据:', data);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
const handleCodeChange = (value: string | undefined) => {
|
||||
if (!value) return;
|
||||
|
||||
setTemplates(templates.map(t =>
|
||||
t.language === language
|
||||
? { ...t, code: value }
|
||||
: t
|
||||
));
|
||||
export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: EditCodePanelProps) {
|
||||
const [codeTemplate, setCodeTemplate] = useState<Template>({
|
||||
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 handleLanguageChange = (language: string) => {
|
||||
const selected = templates.find(t => t.language === language);
|
||||
if (selected) {
|
||||
setCodeTemplate(selected);
|
||||
}
|
||||
};
|
||||
|
||||
// 事件处理函数返回 Promise<void>,不要返回具体数据
|
||||
const handleSave = async (): Promise<void> => {
|
||||
if (!onUpdate) {
|
||||
alert('保存函数未传入,无法保存');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await onUpdate(codeTemplate);
|
||||
if (result.success) {
|
||||
alert('保存成功');
|
||||
} else {
|
||||
alert('保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('保存异常');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">编程语言</Label>
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue placeholder="选择编程语言" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="typescript">TypeScript</SelectItem>
|
||||
<SelectItem value="javascript">JavaScript</SelectItem>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="java">Java</SelectItem>
|
||||
<SelectItem value="cpp">C++</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md h-[500px]">
|
||||
{currentTemplate && (
|
||||
<CoreEditor
|
||||
language={language}
|
||||
value={currentTemplate.code}
|
||||
path={`/${problemId}.${language}`}
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button>保存代码模板</Button>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>代码模板</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language-select">编程语言</Label>
|
||||
<select
|
||||
id="language-select"
|
||||
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
|
||||
value={codeTemplate.language}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
>
|
||||
{templates.map((t) => (
|
||||
<option key={t.language} value={t.language}>
|
||||
{t.language.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code-editor">代码模板内容</Label>
|
||||
<div className="border rounded-md h-[500px]">
|
||||
<CoreEditor
|
||||
language={codeTemplate.language}
|
||||
value={codeTemplate.content}
|
||||
path={`/${problemId}.${codeTemplate.language}`}
|
||||
onChange={(value) =>
|
||||
setCodeTemplate({ ...codeTemplate, content: value || '' })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave}>保存代码模板</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,85 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
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;
|
||||
onUpdate?: (data: { content: string }) => void;
|
||||
}
|
||||
|
||||
export const EditDescriptionPanel = ({
|
||||
problemId,
|
||||
}: EditDescriptionPanelProps) => {
|
||||
const [title, setTitle] = useState(`Problem ${problemId} Title`);
|
||||
const [content, setContent] = useState(`Problem ${problemId} Description Content...`);
|
||||
}) {
|
||||
const [description, setDescription] = useState({
|
||||
title: `Description for Problem ${problemId}`,
|
||||
content: `Description content for Problem ${problemId}...`
|
||||
});
|
||||
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 (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>题目描述</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">题目标题</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="输入题目标题"
|
||||
/>
|
||||
</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={viewMode === 'edit' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<div className="relative h-[600px]">
|
||||
<CoreEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
language="``"
|
||||
className="absolute inset-0 rounded-md border border-input"
|
||||
/>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>题目描述</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<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="输入题目标题"
|
||||
/>
|
||||
</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={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>
|
||||
{viewMode !== 'edit' && (
|
||||
<div className="prose dark:prose-invert">
|
||||
<MdxPreview
|
||||
source={description.content}
|
||||
components={{ Accordion }} // ← 这里传入 Accordion
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<MdxPreview source={content} />
|
||||
</div>
|
||||
|
||||
<Button>保存更改</Button>
|
||||
</div>
|
||||
|
||||
<Button>保存更改</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,68 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getProblemData } from "@/app/actions/getProblem";
|
||||
|
||||
interface EditDetailPanelProps {
|
||||
export default function EditDetailPanel({
|
||||
problemId,
|
||||
}: {
|
||||
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 = ({
|
||||
problemId,
|
||||
}: EditDetailPanelProps) => {
|
||||
const [displayId, setDisplayId] = useState(problemId);
|
||||
const [difficulty, setDifficulty] = useState("medium");
|
||||
const [isPublished, setIsPublished] = useState(true);
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const problemData = await getProblemData(problemId);
|
||||
setProblemDetails({
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display-id">题号</Label>
|
||||
<Input
|
||||
id="display-id"
|
||||
value={displayId}
|
||||
onChange={(e) => setDisplayId(e.target.value)}
|
||||
placeholder="输入题号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="difficulty">难度</Label>
|
||||
<Select value={difficulty} onValueChange={setDifficulty}>
|
||||
<SelectTrigger id="difficulty">
|
||||
<SelectValue placeholder="选择难度" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="easy">简单</SelectItem>
|
||||
<SelectItem value="medium">中等</SelectItem>
|
||||
<SelectItem value="hard">困难</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="is-published" className="text-sm font-normal">
|
||||
是否发布
|
||||
</Label>
|
||||
<Input
|
||||
id="is-published"
|
||||
type="checkbox"
|
||||
checked={isPublished}
|
||||
onChange={(e) => setIsPublished(e.target.checked)}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button>保存基本信息</Button>
|
||||
<Button variant="outline" type="button">
|
||||
删除题目
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>题目详情</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display-id">显示ID</Label>
|
||||
<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="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="time-limit">时间限制 (ms)</Label>
|
||||
<Input
|
||||
id="time-limit"
|
||||
type="number"
|
||||
value={problemDetails.timeLimit}
|
||||
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
|
||||
placeholder="输入时间限制"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<input
|
||||
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 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="is-published"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
是否发布
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,85 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
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;
|
||||
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 = ({
|
||||
problemId,
|
||||
}: EditSolutionPanelProps) => {
|
||||
const [title, setTitle] = useState(`Solution for Problem ${problemId}`);
|
||||
const [content, setContent] = useState(`Solution content for Problem ${problemId}...`);
|
||||
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit');
|
||||
useEffect(() => {
|
||||
async function fetchSolution() {
|
||||
try {
|
||||
const data = await getProblemData(problemId);
|
||||
setSolution({
|
||||
title: data.title ? data.title + " 解析" : `Solution for Problem ${problemId}`,
|
||||
content: data.solution || "",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载题解失败", error);
|
||||
}
|
||||
}
|
||||
fetchSolution();
|
||||
}, [problemId]);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>题目解析</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="solution-title">题解标题</Label>
|
||||
<Input
|
||||
id="solution-title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="输入题解标题"
|
||||
/>
|
||||
</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={viewMode === 'edit' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<div className="relative h-[600px]">
|
||||
<CoreEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
language="``"
|
||||
className="absolute inset-0 rounded-md border border-input"
|
||||
/>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>题目解析</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="solution-title">题解标题</Label>
|
||||
<Input
|
||||
id="solution-title"
|
||||
value={solution.title}
|
||||
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
|
||||
placeholder="输入题解标题"
|
||||
/>
|
||||
</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={viewMode === "edit" || viewMode === "compare" ? "block" : "hidden"}>
|
||||
<div className="relative h-[600px]">
|
||||
<CoreEditor
|
||||
value={solution.content}
|
||||
onChange={(newContent) => setSolution({ ...solution, content: newContent })}
|
||||
language="markdown"
|
||||
className="absolute inset-0 rounded-md border border-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<MdxPreview source={content} />
|
||||
|
||||
{viewMode !== "edit" && (
|
||||
<div className="prose dark:prose-invert">
|
||||
<MdxPreview source={solution.content} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button>保存题解</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,119 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getProblemData } from "@/app/actions/getProblem";
|
||||
|
||||
interface TestCase {
|
||||
id: string;
|
||||
input: string;
|
||||
expectedOutput: string;
|
||||
}
|
||||
|
||||
interface EditTestcasePanelProps {
|
||||
export default function EditTestcasePanel({
|
||||
problemId,
|
||||
}: {
|
||||
problemId: string;
|
||||
onUpdate?: (data: {
|
||||
content: string;
|
||||
inputs: Array<{ index: number; name: string; value: string }>
|
||||
}) => void;
|
||||
}
|
||||
}) {
|
||||
const [testcases, setTestcases] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
expectedOutput: string;
|
||||
inputs: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
}>;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
export const EditTestcasePanel = ({
|
||||
problemId,
|
||||
}: EditTestcasePanelProps) => {
|
||||
const [testcases, setTestcases] = useState<TestCase[]>([
|
||||
{ id: "1", input: "input1", expectedOutput: "output1" },
|
||||
{ id: "2", input: "input2", expectedOutput: "output2" }
|
||||
]);
|
||||
|
||||
const [newInput, setNewInput] = useState("");
|
||||
const [newOutput, setNewOutput] = useState("");
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const problemData = await getProblemData(problemId);
|
||||
if (problemData && problemData.testcases) {
|
||||
setTestcases(problemData.testcases);
|
||||
} else {
|
||||
setTestcases([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载测试用例失败:", error);
|
||||
setTestcases([]);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [problemId]);
|
||||
|
||||
const addTestCase = () => {
|
||||
if (!newInput || !newOutput) return;
|
||||
|
||||
const newTestCase = {
|
||||
id: (testcases.length + 1).toString(),
|
||||
input: newInput,
|
||||
expectedOutput: newOutput
|
||||
};
|
||||
|
||||
setTestcases([...testcases, newTestCase]);
|
||||
setNewInput("");
|
||||
setNewOutput("");
|
||||
const handleAddTestcase = () => {
|
||||
setTestcases([
|
||||
...testcases,
|
||||
{
|
||||
id: `new-${Date.now()}`,
|
||||
expectedOutput: "",
|
||||
inputs: [{ name: "input1", value: "" }],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteTestCase = (id: string) => {
|
||||
setTestcases(testcases.filter(tc => tc.id !== id));
|
||||
|
||||
const handleRemoveTestcase = (index: number) => {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label htmlFor="testcase-input">输入</Label>
|
||||
<Input
|
||||
id="testcase-input"
|
||||
value={newInput}
|
||||
onChange={(e) => setNewInput(e.target.value)}
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>编号</TableHead>
|
||||
<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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>测试用例</CardTitle>
|
||||
<Button type="button" onClick={handleAddTestcase}>
|
||||
添加测试用例
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{testcases.map((testcase, index) => (
|
||||
<div key={testcase.id} className="border p-4 rounded-md space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium">测试用例 {index + 1}</h3>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => handleRemoveTestcase(index)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`expected-output-${index}`}>预期输出</Label>
|
||||
<Input
|
||||
id={`expected-output-${index}`}
|
||||
value={testcase.expectedOutput}
|
||||
onChange={(e) => handleExpectedOutputChange(index, e.target.value)}
|
||||
placeholder="输入预期输出"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>输入参数</Label>
|
||||
<Button type="button" onClick={() => handleAddInput(index)}>
|
||||
添加输入
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{testcase.inputs.map((input, inputIndex) => (
|
||||
<div key={input.name} className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`input-name-${index}-${inputIndex}`}>
|
||||
参数名称
|
||||
</Label>
|
||||
<Input
|
||||
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>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Button>保存测试用例</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user