feat(problem-editor): realize backend for save problem edit

- 在编辑面板组件中添加 onUpdate属性,用于处理数据更新
- 新增 updateProblem 函数,实现问题数据的更新逻辑
- 在问题编辑页面中集成更新功能,实现即时保存
This commit is contained in:
fly6516 2025-06-17 01:35:04 +08:00 committed by cfngc4594
parent 20f4cc01da
commit f63d869403
7 changed files with 240 additions and 13 deletions

View File

@ -1,27 +1,116 @@
"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 { 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} />,
solution: <EditSolutionPanel problemId={problemId} />,
detail: <EditDetailPanel problemId={problemId} />,
code: <EditCodePanel problemId={problemId} />,
testcase: <EditTestcasePanel problemId={problemId} />,
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 (

View 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' };
}
}

View File

@ -14,6 +14,10 @@ interface Template {
interface EditCodePanelProps {
problemId: string;
onUpdate?: (data: {
content: string;
language: 'c' | 'cpp'; // 移除可选标记
}) => void;
}
export const EditCodePanel = ({

View File

@ -10,6 +10,7 @@ import {CoreEditor} from "@/components/core-editor";
interface EditDescriptionPanelProps {
problemId: string;
onUpdate?: (data: { content: string }) => void;
}
export const EditDescriptionPanel = ({
@ -65,7 +66,7 @@ export const EditDescriptionPanel = ({
<CoreEditor
value={content}
onChange={setContent}
language="markdown"
language="``"
className="absolute inset-0 rounded-md border border-input"
/>
</div>

View File

@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
interface EditDetailPanelProps {
problemId: string;
onUpdate?: (data: { content: string }) => void;
}
export const EditDetailPanel = ({

View File

@ -10,6 +10,7 @@ import {CoreEditor} from "@/components/core-editor";
interface EditSolutionPanelProps {
problemId: string;
onUpdate?: (data: { content: string }) => void;
}
export const EditSolutionPanel = ({
@ -65,7 +66,7 @@ export const EditSolutionPanel = ({
<CoreEditor
value={content}
onChange={setContent}
language="markdown"
language="``"
className="absolute inset-0 rounded-md border border-input"
/>
</div>

View File

@ -21,6 +21,10 @@ interface TestCase {
interface EditTestcasePanelProps {
problemId: string;
onUpdate?: (data: {
content: string;
inputs: Array<{ index: number; name: string; value: string }>
}) => void;
}
export const EditTestcasePanel = ({