mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 01:10:53 +00:00
feat(问题编辑): add problem-editor page
- 添加了编辑问题描述、解决方案、详细信息、代码模板和测试用例的组件 - 实现了问题编辑页面的基本布局和功能 - 增加了富文本预览和对比功能 - 支持多种编程语言的代码编辑- 提供了测试用例的添加和删除功能
This commit is contained in:
parent
d6e611b9fd
commit
c74446d492
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>
|
||||
);
|
||||
}
|
80
src/components/creater/edit-code-panel.tsx
Normal file
80
src/components/creater/edit-code-panel.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
"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 { CoreEditor } from "@/components/core-editor";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
language: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface EditCodePanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
const handleCodeChange = (value: string | undefined) => {
|
||||
if (!value) return;
|
||||
|
||||
setTemplates(templates.map(t =>
|
||||
t.language === language
|
||||
? { ...t, code: value }
|
||||
: t
|
||||
));
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
75
src/components/creater/edit-description-panel.tsx
Normal file
75
src/components/creater/edit-description-panel.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
|
||||
interface EditDescriptionPanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
export const EditDescriptionPanel = ({
|
||||
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');
|
||||
|
||||
return (
|
||||
<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"}>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="输入题目详细描述..."
|
||||
className="min-h-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<MdxPreview source={content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button>保存更改</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
67
src/components/creater/edit-detail-panel.tsx
Normal file
67
src/components/creater/edit-detail-panel.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useState } 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";
|
||||
|
||||
interface EditDetailPanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
export const EditDetailPanel = ({
|
||||
problemId,
|
||||
}: EditDetailPanelProps) => {
|
||||
const [displayId, setDisplayId] = useState(problemId);
|
||||
const [difficulty, setDifficulty] = useState("medium");
|
||||
const [isPublished, setIsPublished] = useState(true);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
75
src/components/creater/edit-solution-panel.tsx
Normal file
75
src/components/creater/edit-solution-panel.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
|
||||
interface EditSolutionPanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
return (
|
||||
<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"}>
|
||||
<Textarea
|
||||
id="solution-content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="输入详细题解内容..."
|
||||
className="min-h-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'preview' || viewMode === 'compare' ? "block" : "hidden"}>
|
||||
<MdxPreview source={content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button>保存题解</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
115
src/components/creater/edit-testcase-panel.tsx
Normal file
115
src/components/creater/edit-testcase-panel.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useState } 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";
|
||||
|
||||
interface TestCase {
|
||||
id: string;
|
||||
input: string;
|
||||
expectedOutput: string;
|
||||
}
|
||||
|
||||
interface EditTestcasePanelProps {
|
||||
problemId: 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("");
|
||||
|
||||
const addTestCase = () => {
|
||||
if (!newInput || !newOutput) return;
|
||||
|
||||
const newTestCase = {
|
||||
id: (testcases.length + 1).toString(),
|
||||
input: newInput,
|
||||
expectedOutput: newOutput
|
||||
};
|
||||
|
||||
setTestcases([...testcases, newTestCase]);
|
||||
setNewInput("");
|
||||
setNewOutput("");
|
||||
};
|
||||
|
||||
const deleteTestCase = (id: string) => {
|
||||
setTestcases(testcases.filter(tc => tc.id !== id));
|
||||
};
|
||||
|
||||
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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Button>保存测试用例</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user