diff --git a/src/app/(app)/problem-editor/[problemId]/page.tsx b/src/app/(app)/problem-editor/[problemId]/page.tsx new file mode 100644 index 0000000..3e2be4c --- /dev/null +++ b/src/app/(app)/problem-editor/[problemId]/page.tsx @@ -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 = { + description: { + await handleUpdate( + (descriptionData) => updateProblem({ + problemId, + displayId: 0, + description: descriptionData.content + }), + data + ); + }} + />, + solution: { + await handleUpdate( + (solutionData) => updateProblem({ + problemId, + displayId: 0, + solution: solutionData.content + }), + data + ); + }} + />, + detail: { + await handleUpdate( + (detailData) => updateProblem({ + problemId, + displayId: 0, + detail: detailData.content + }), + data + ); + }} + />, + code: { + await handleUpdate( + (codeData) => updateProblem({ + problemId, + displayId: 0, + templates: [{ + language: codeData.language || 'c', // 添加默认值 + content: codeData.content + }] + }), + data + ); + }} + />, + testcase: { + await handleUpdate( + (testcaseData) => updateProblem({ + problemId, + displayId: 0, + testcases: [{ + expectedOutput: testcaseData.content, + inputs: testcaseData.inputs || [] // 添加默认空数组 + }] + }), + data + ); + }} + /> + }; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/app/actions/getProblem.ts b/src/app/actions/getProblem.ts new file mode 100644 index 0000000..7fd776c --- /dev/null +++ b/src/app/actions/getProblem.ts @@ -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, + })), + })), + }; +} diff --git a/src/app/actions/getProblemLocales.ts b/src/app/actions/getProblemLocales.ts new file mode 100644 index 0000000..c2c2c26 --- /dev/null +++ b/src/app/actions/getProblemLocales.ts @@ -0,0 +1,14 @@ +// src/app/actions/getProblemLocales.ts +'use server'; + +import prisma from "@/lib/prisma"; + +export async function getProblemLocales(problemId: string): Promise { + const locales = await prisma.problemLocalization.findMany({ + where: { problemId }, + select: { locale: true }, + distinct: ['locale'], + }); + + return locales.map(l => l.locale); +} diff --git a/src/app/actions/updateProblem.ts b/src/app/actions/updateProblem.ts new file mode 100644 index 0000000..c06e67c --- /dev/null +++ b/src/app/actions/updateProblem.ts @@ -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; + +export async function updateProblem(data: z.infer) { + 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' }; + } +} \ No newline at end of file diff --git a/src/components/creater/edit-code-panel.tsx b/src/components/creater/edit-code-panel.tsx new file mode 100644 index 0000000..5479e92 --- /dev/null +++ b/src/components/creater/edit-code-panel.tsx @@ -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