mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 09:20:53 +00:00
Merge pull request #27 from massbug/refactor/problem_creater
Refactor/problem creater
This commit is contained in:
commit
3ac6337f96
121
src/app/(app)/problem-editor/[problemId]/page.tsx
Normal file
121
src/app/(app)/problem-editor/[problemId]/page.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"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 { updateProblem } from '@/app/actions/updateProblem';
|
||||||
|
|
||||||
|
interface ProblemEditorPageProps {
|
||||||
|
params: Promise<{ problemId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateData {
|
||||||
|
content: string;
|
||||||
|
language?: 'c' | 'cpp';
|
||||||
|
inputs?: Array<{ index: number; name: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = async (
|
||||||
|
updateFn: (data: UpdateData) => Promise<{ success: boolean }>,
|
||||||
|
data: UpdateData
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const result = await updateFn(data);
|
||||||
|
if (!result.success) {
|
||||||
|
// 这里可以添加更具体的错误处理
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新失败:', error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function ProblemEditorPage({
|
||||||
|
params,
|
||||||
|
}: ProblemEditorPageProps) {
|
||||||
|
const { problemId } = await params;
|
||||||
|
|
||||||
|
const components: Record<string, React.ReactNode> = {
|
||||||
|
description: <EditDescriptionPanel
|
||||||
|
problemId={problemId}
|
||||||
|
onUpdate={async (data) => {
|
||||||
|
await handleUpdate(
|
||||||
|
(descriptionData) => updateProblem({
|
||||||
|
problemId,
|
||||||
|
displayId: 0,
|
||||||
|
description: descriptionData.content
|
||||||
|
}),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
solution: <EditSolutionPanel
|
||||||
|
problemId={problemId}
|
||||||
|
onUpdate={async (data) => {
|
||||||
|
await handleUpdate(
|
||||||
|
(solutionData) => updateProblem({
|
||||||
|
problemId,
|
||||||
|
displayId: 0,
|
||||||
|
solution: solutionData.content
|
||||||
|
}),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
detail: <EditDetailPanel
|
||||||
|
problemId={problemId}
|
||||||
|
onUpdate={async (data) => {
|
||||||
|
await handleUpdate(
|
||||||
|
(detailData) => updateProblem({
|
||||||
|
problemId,
|
||||||
|
displayId: 0,
|
||||||
|
detail: detailData.content
|
||||||
|
}),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
code: <EditCodePanel
|
||||||
|
problemId={problemId}
|
||||||
|
onUpdate={async (data) => {
|
||||||
|
await handleUpdate(
|
||||||
|
(codeData) => updateProblem({
|
||||||
|
problemId,
|
||||||
|
displayId: 0,
|
||||||
|
templates: [{
|
||||||
|
language: codeData.language || 'c', // 添加默认值
|
||||||
|
content: codeData.content
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
testcase: <EditTestcasePanel
|
||||||
|
problemId={problemId}
|
||||||
|
onUpdate={async (data) => {
|
||||||
|
await handleUpdate(
|
||||||
|
(testcaseData) => updateProblem({
|
||||||
|
problemId,
|
||||||
|
displayId: 0,
|
||||||
|
testcases: [{
|
||||||
|
expectedOutput: testcaseData.content,
|
||||||
|
inputs: testcaseData.inputs || [] // 添加默认空数组
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full">
|
||||||
|
<ProblemFlexLayout components={components} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
63
src/app/actions/getProblem.ts
Normal file
63
src/app/actions/getProblem.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// app/actions/get-problem-data.ts
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import { Locale } from '@/generated/client'; // ✅ 导入 enum Locale
|
||||||
|
import { serialize } from 'next-mdx-remote/serialize';
|
||||||
|
|
||||||
|
export async function getProblemData(problemId: string, locale: string) {
|
||||||
|
const selectedLocale = locale as Locale; // ✅ 强制转换 string 为 Prisma enum
|
||||||
|
|
||||||
|
const problem = await prisma.problem.findUnique({
|
||||||
|
where: { id: problemId },
|
||||||
|
include: {
|
||||||
|
templates: true,
|
||||||
|
testcases: {
|
||||||
|
include: { inputs: true }
|
||||||
|
},
|
||||||
|
localizations: {
|
||||||
|
where: {
|
||||||
|
locale: selectedLocale, // ✅ 这里使用枚举
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!problem) {
|
||||||
|
throw new Error('Problem not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContent = (type: string) =>
|
||||||
|
problem.localizations.find(loc => loc.type === type)?.content || '';
|
||||||
|
|
||||||
|
const rawDescription = getContent('DESCRIPTION');
|
||||||
|
|
||||||
|
const mdxDescription = await serialize(rawDescription, {
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
14
src/app/actions/getProblemLocales.ts
Normal file
14
src/app/actions/getProblemLocales.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// src/app/actions/getProblemLocales.ts
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function getProblemLocales(problemId: string): Promise<string[]> {
|
||||||
|
const locales = await prisma.problemLocalization.findMany({
|
||||||
|
where: { problemId },
|
||||||
|
select: { locale: true },
|
||||||
|
distinct: ['locale'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return locales.map(l => l.locale);
|
||||||
|
}
|
127
src/app/actions/updateProblem.ts
Normal file
127
src/app/actions/updateProblem.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ProblemUpdateSchema = z.object({
|
||||||
|
problemId: z.string(),
|
||||||
|
displayId: z.number().optional(), // 改回可选字段
|
||||||
|
difficulty: z.enum(['EASY', 'MEDIUM', 'HARD']).optional(),
|
||||||
|
isPublished: z.boolean().optional(),
|
||||||
|
timeLimit: z.number().optional(),
|
||||||
|
memoryLimit: z.number().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
solution: z.string().optional(),
|
||||||
|
detail: z.string().optional(),
|
||||||
|
templates: z.array(z.object({
|
||||||
|
language: z.enum(['c', 'cpp']),
|
||||||
|
content: z.string()
|
||||||
|
})).optional(),
|
||||||
|
testcases: z.array(z.object({
|
||||||
|
expectedOutput: z.string(),
|
||||||
|
inputs: z.array(z.object({
|
||||||
|
index: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
}))
|
||||||
|
})).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateProblemData = z.infer<typeof ProblemUpdateSchema>;
|
||||||
|
|
||||||
|
export async function updateProblem(data: z.infer<typeof ProblemUpdateSchema>) {
|
||||||
|
try {
|
||||||
|
const validatedData = ProblemUpdateSchema.parse(data);
|
||||||
|
|
||||||
|
// 使用upsert代替update实现存在时更新,不存在时创建
|
||||||
|
const problem = await prisma.problem.upsert({
|
||||||
|
where: { id: validatedData.problemId },
|
||||||
|
create: {
|
||||||
|
id: validatedData.problemId, // 需要显式指定ID
|
||||||
|
displayId: validatedData.displayId || 0,
|
||||||
|
difficulty: validatedData.difficulty || 'EASY',
|
||||||
|
isPublished: validatedData.isPublished || false,
|
||||||
|
timeLimit: validatedData.timeLimit || 1000,
|
||||||
|
memoryLimit: validatedData.memoryLimit || 134217728,
|
||||||
|
// 初始化关联数据
|
||||||
|
localizations: validatedData.description ? {
|
||||||
|
create: [{
|
||||||
|
locale: 'en',
|
||||||
|
type: 'DESCRIPTION',
|
||||||
|
content: validatedData.description
|
||||||
|
}]
|
||||||
|
} : undefined,
|
||||||
|
templates: validatedData.templates ? {
|
||||||
|
create: validatedData.templates.map(t => ({
|
||||||
|
language: t.language,
|
||||||
|
content: t.content
|
||||||
|
}))
|
||||||
|
} : undefined,
|
||||||
|
testcases: validatedData.testcases ? {
|
||||||
|
create: validatedData.testcases.map(t => ({
|
||||||
|
expectedOutput: t.expectedOutput,
|
||||||
|
inputs: {
|
||||||
|
create: t.inputs.map(i => ({
|
||||||
|
index: i.index,
|
||||||
|
name: i.name,
|
||||||
|
value: i.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} : undefined
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
displayId: validatedData.displayId,
|
||||||
|
difficulty: validatedData.difficulty,
|
||||||
|
isPublished: validatedData.isPublished,
|
||||||
|
timeLimit: validatedData.timeLimit,
|
||||||
|
memoryLimit: validatedData.memoryLimit,
|
||||||
|
// 更新关联数据
|
||||||
|
localizations: validatedData.description ? {
|
||||||
|
upsert: {
|
||||||
|
where: { problemId_locale_type: {
|
||||||
|
problemId: validatedData.problemId,
|
||||||
|
locale: 'en',
|
||||||
|
type: 'DESCRIPTION'
|
||||||
|
}},
|
||||||
|
create: {
|
||||||
|
locale: 'en',
|
||||||
|
type: 'DESCRIPTION',
|
||||||
|
content: validatedData.description
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
content: validatedData.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : undefined,
|
||||||
|
templates: validatedData.templates ? {
|
||||||
|
deleteMany: {},
|
||||||
|
create: validatedData.templates.map(t => ({
|
||||||
|
language: t.language,
|
||||||
|
content: t.content
|
||||||
|
}))
|
||||||
|
} : undefined,
|
||||||
|
testcases: validatedData.testcases ? {
|
||||||
|
deleteMany: {},
|
||||||
|
create: validatedData.testcases.map(t => ({
|
||||||
|
expectedOutput: t.expectedOutput,
|
||||||
|
inputs: {
|
||||||
|
create: t.inputs.map(i => ({
|
||||||
|
index: i.index,
|
||||||
|
name: i.name,
|
||||||
|
value: i.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath(`/problem-editor/${validatedData.problemId}`);
|
||||||
|
return { success: true, problem };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update problem:', error);
|
||||||
|
return { success: false, error: 'Failed to update problem' };
|
||||||
|
}
|
||||||
|
}
|
121
src/components/creater/edit-code-panel.tsx
Normal file
121
src/components/creater/edit-code-panel.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditCodePanelProps {
|
||||||
|
problemId: string;
|
||||||
|
onUpdate?: (data: Template) => Promise<{ success: boolean }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟保存函数
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
148
src/components/creater/edit-description-panel.tsx
Normal file
148
src/components/creater/edit-description-panel.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { CoreEditor } from "@/components/core-editor";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default function EditDescriptionPanel({ problemId }: { problemId: string }) {
|
||||||
|
const [locales, setLocales] = useState<string[]>([]);
|
||||||
|
const [currentLocale, setCurrentLocale] = useState<string>("");
|
||||||
|
const [customLocale, setCustomLocale] = useState("");
|
||||||
|
|
||||||
|
const [description, setDescription] = useState({ title: "", content: "" });
|
||||||
|
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
||||||
|
|
||||||
|
// 获取语言列表
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchLocales() {
|
||||||
|
const langs = await getProblemLocales(problemId);
|
||||||
|
setLocales(langs);
|
||||||
|
if (langs.length > 0) {
|
||||||
|
setCurrentLocale(langs[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchLocales();
|
||||||
|
}, [problemId]);
|
||||||
|
|
||||||
|
// 获取对应语言的题目数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentLocale) return;
|
||||||
|
async function fetchProblem() {
|
||||||
|
const data = await getProblemData(problemId, currentLocale);
|
||||||
|
setDescription({
|
||||||
|
title: data?.title || "",
|
||||||
|
content: data?.description || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fetchProblem();
|
||||||
|
}, [problemId, currentLocale]);
|
||||||
|
|
||||||
|
// 添加新语言(仅前端)
|
||||||
|
function handleAddCustomLocale() {
|
||||||
|
if (customLocale && !locales.includes(customLocale)) {
|
||||||
|
const newLocales = [...locales, customLocale];
|
||||||
|
setLocales(newLocales);
|
||||||
|
setCurrentLocale(customLocale);
|
||||||
|
setCustomLocale("");
|
||||||
|
setDescription({ title: "", content: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<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 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="输入题目标题"
|
||||||
|
/>
|
||||||
|
</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"}>
|
||||||
|
{(viewMode === "edit" || viewMode === "compare") && (
|
||||||
|
<div className="relative h-[600px]">
|
||||||
|
<CoreEditor
|
||||||
|
value={description.content}
|
||||||
|
onChange={(newVal) => setDescription({ ...description, content: newVal || "" })}
|
||||||
|
language="markdown"
|
||||||
|
className="absolute inset-0 rounded-md border border-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewMode !== "edit" && (
|
||||||
|
<div className="prose dark:prose-invert">
|
||||||
|
<MdxPreview source={description.content} components={{ Accordion, VideoEmbed }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button>保存更改</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
137
src/components/creater/edit-detail-panel.tsx
Normal file
137
src/components/creater/edit-detail-panel.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getProblemData } from "@/app/actions/getProblem";
|
||||||
|
|
||||||
|
export default function EditDetailPanel({
|
||||||
|
problemId,
|
||||||
|
}: {
|
||||||
|
problemId: string;
|
||||||
|
}) {
|
||||||
|
const [problemDetails, setProblemDetails] = useState({
|
||||||
|
displayId: 1000,
|
||||||
|
difficulty: "EASY" as "EASY" | "MEDIUM" | "HARD",
|
||||||
|
timeLimit: 1000,
|
||||||
|
memoryLimit: 134217728,
|
||||||
|
isPublished: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
143
src/components/creater/edit-solution-panel.tsx
Normal file
143
src/components/creater/edit-solution-panel.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { CoreEditor } from "@/components/core-editor";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default function EditSolutionPanel({ problemId }: { problemId: string }) {
|
||||||
|
const [locales, setLocales] = useState<string[]>([]);
|
||||||
|
const [currentLocale, setCurrentLocale] = useState<string>("");
|
||||||
|
const [customLocale, setCustomLocale] = useState("");
|
||||||
|
|
||||||
|
const [solution, setSolution] = useState({ title: "", content: "" });
|
||||||
|
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchLocales() {
|
||||||
|
const langs = await getProblemLocales(problemId);
|
||||||
|
setLocales(langs);
|
||||||
|
if (langs.length > 0) setCurrentLocale(langs[0]);
|
||||||
|
}
|
||||||
|
fetchLocales();
|
||||||
|
}, [problemId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentLocale) return;
|
||||||
|
async function fetchSolution() {
|
||||||
|
const data = await getProblemData(problemId, currentLocale);
|
||||||
|
setSolution({
|
||||||
|
title: (data?.title || "") + " 解析",
|
||||||
|
content: data?.solution || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fetchSolution();
|
||||||
|
}, [problemId, currentLocale]);
|
||||||
|
|
||||||
|
function handleAddCustomLocale() {
|
||||||
|
if (customLocale && !locales.includes(customLocale)) {
|
||||||
|
const newLocales = [...locales, customLocale];
|
||||||
|
setLocales(newLocales);
|
||||||
|
setCurrentLocale(customLocale);
|
||||||
|
setCustomLocale("");
|
||||||
|
setSolution({ title: "", content: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<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 onClick={handleAddCustomLocale}>添加</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题输入 */}
|
||||||
|
<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"}>
|
||||||
|
{(viewMode === "edit" || viewMode === "compare") && (
|
||||||
|
<div className="relative h-[600px]">
|
||||||
|
<CoreEditor
|
||||||
|
value={solution.content}
|
||||||
|
onChange={(val) => setSolution({ ...solution, content: val || "" })}
|
||||||
|
language="markdown"
|
||||||
|
className="absolute inset-0 rounded-md border border-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewMode !== "edit" && (
|
||||||
|
<div className="prose dark:prose-invert">
|
||||||
|
<MdxPreview source={solution.content} components={{ Accordion, VideoEmbed }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button>保存更改</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
180
src/components/creater/edit-testcase-panel.tsx
Normal file
180
src/components/creater/edit-testcase-panel.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getProblemData } from "@/app/actions/getProblem";
|
||||||
|
|
||||||
|
export default function EditTestcasePanel({
|
||||||
|
problemId,
|
||||||
|
}: {
|
||||||
|
problemId: string;
|
||||||
|
}) {
|
||||||
|
const [testcases, setTestcases] = useState<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
expectedOutput: string;
|
||||||
|
inputs: Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
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 handleAddTestcase = () => {
|
||||||
|
setTestcases([
|
||||||
|
...testcases,
|
||||||
|
{
|
||||||
|
id: `new-${Date.now()}`,
|
||||||
|
expectedOutput: "",
|
||||||
|
inputs: [{ name: "input1", value: "" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user