feat(creater): realise problem-editor interactive with database logic

- 在 edit-code-panel、edit-description-panel、edit-detail-panel、edit-solution-panel 和 edit-testcase-panel 组件中添加保存逻辑
- 实现与后端 API 的交互,包括保存代码模板、题目描述、详情、解析和测试用例
-优化错误处理和用户提示,使用 toast 组件显示操作结果
- 调整界面布局和交互细节,提升用户体验
This commit is contained in:
fly6516 2025-06-20 11:42:10 +08:00
parent 012ca82d76
commit 6e02c67013
7 changed files with 561 additions and 317 deletions

View File

@ -2,11 +2,11 @@
'use server';
import prisma from '@/lib/prisma';
import { Locale } from '@/generated/client'; // ✅ 导入 enum Locale
import { Locale } from '@/generated/client';
import { serialize } from 'next-mdx-remote/serialize';
export async function getProblemData(problemId: string, locale: string) {
const selectedLocale = locale as Locale; // ✅ 强制转换 string 为 Prisma enum
export async function getProblemData(problemId: string, locale?: string) {
const selectedLocale = locale as Locale;
const problem = await prisma.problem.findUnique({
where: { id: problemId },
@ -17,7 +17,7 @@ export async function getProblemData(problemId: string, locale: string) {
},
localizations: {
where: {
locale: selectedLocale, // ✅ 这里使用枚举
locale: selectedLocale,
},
},
},

View File

@ -1,11 +1,14 @@
'use client';
"use client"
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { getProblemData } from '@/app/actions/getProblem';
import { updateProblemTemplate } from '@/components/creater/problem-maintain';
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";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CoreEditor } from '@/components/core-editor';
import { Language } from '@/generated/client';
import { toast } from 'sonner';
interface Template {
language: string;
@ -14,65 +17,50 @@ interface Template {
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) {
export default function EditCodePanel({ problemId }: EditCodePanelProps) {
const [codeTemplate, setCodeTemplate] = useState<Template>({
language: 'cpp',
content: `// 默认代码模板 for Problem ${problemId}`,
});
const [templates, setTemplates] = useState<Template[]>([]);
useEffect(() => {
async function fetch() {
async function fetchTemplates() {
try {
const problem = await getProblemData(problemId);
setTemplates(problem.templates);
const cppTemplate = problem.templates.find(t => t.language === 'cpp');
setCodeTemplate(cppTemplate || problem.templates[0]);
const sel = problem.templates.find(t => t.language === 'cpp') || problem.templates[0];
if (sel) setCodeTemplate(sel);
} catch (err) {
console.error('加载问题数据失败:', err);
toast.error('加载问题数据失败');
}
}
fetch();
fetchTemplates();
}, [problemId]);
const handleLanguageChange = (language: string) => {
const selected = templates.find(t => t.language === language);
if (selected) {
setCodeTemplate(selected);
}
const sel = templates.find(t => t.language === language);
if (sel) setCodeTemplate(sel);
};
// 事件处理函数返回 Promise<void>,不要返回具体数据
const handleSave = async (): Promise<void> => {
if (!onUpdate) {
alert('保存函数未传入,无法保存');
return;
}
try {
const result = await onUpdate(codeTemplate);
if (result.success) {
alert('保存成功');
const res = await updateProblemTemplate(
problemId,
codeTemplate.language as Language,
codeTemplate.content
);
if (res.success) {
toast.success('保存成功');
} else {
alert('保存失败');
toast.error('保存失败');
}
} catch (error) {
console.error(error);
alert('保存异常');
console.error('保存异常:', error);
toast.error('保存异常');
}
};
@ -89,9 +77,9 @@ export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: Ed
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)}
onChange={e => handleLanguageChange(e.target.value)}
>
{templates.map((t) => (
{templates.map(t => (
<option key={t.language} value={t.language}>
{t.language.toUpperCase()}
</option>
@ -106,7 +94,7 @@ export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: Ed
language={codeTemplate.language}
value={codeTemplate.content}
path={`/${problemId}.${codeTemplate.language}`}
onChange={(value) =>
onChange={value =>
setCodeTemplate({ ...codeTemplate, content: value || '' })
}
/>

View File

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@ -11,6 +11,9 @@ 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";
import { toast } from "sonner";
import { updateProblemDescription, updateProblemTitle } from '@/components/creater/problem-maintain';
import { Locale } from "@/generated/client";
export default function EditDescriptionPanel({ problemId }: { problemId: string }) {
const [locales, setLocales] = useState<string[]>([]);
@ -20,33 +23,35 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
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]);
try {
const langs = await getProblemLocales(problemId);
setLocales(langs);
if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) {
console.error(err);
toast.error('获取语言列表失败');
}
}
fetchLocales();
}, [problemId]);
// 获取对应语言的题目数据
useEffect(() => {
if (!currentLocale) return;
async function fetchProblem() {
const data = await getProblemData(problemId, currentLocale);
setDescription({
title: data?.title || "",
content: data?.description || "",
});
try {
const data = await getProblemData(problemId, currentLocale);
setDescription({ title: data?.title || "", content: data?.description || "" });
} catch (err) {
console.error(err);
toast.error('加载题目描述失败');
}
}
fetchProblem();
}, [problemId, currentLocale]);
// 添加新语言(仅前端)
function handleAddCustomLocale() {
const handleAddCustomLocale = () => {
if (customLocale && !locales.includes(customLocale)) {
const newLocales = [...locales, customLocale];
setLocales(newLocales);
@ -54,7 +59,27 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
setCustomLocale("");
setDescription({ title: "", content: "" });
}
}
};
const handleSave = async (): Promise<void> => {
if (!currentLocale) {
toast.error('请选择语言');
return;
}
try {
const locale = currentLocale as Locale;
const resTitle = await updateProblemTitle(problemId, locale, description.title);
const resDesc = await updateProblemDescription(problemId, locale, description.content);
if (resTitle.success && resDesc.success) {
toast.success('保存成功');
} else {
toast.error('保存失败');
}
} catch (err) {
console.error(err);
toast.error('保存异常');
}
};
return (
<Card className="w-full">
@ -141,7 +166,7 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
)}
</div>
<Button></Button>
<Button onClick={handleSave}></Button>
</CardContent>
</Card>
);

View File

@ -1,19 +1,19 @@
"use client";
import { useState, useEffect } from "react";
import React, { 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";
import { toast } from "sonner";
import { updateProblemDetail } from '@/components/creater/problem-maintain';
import { Difficulty } from "@/generated/client";
export default function EditDetailPanel({
problemId,
}: {
problemId: string;
}) {
export default function EditDetailPanel({ problemId }: { problemId: string }) {
const [problemDetails, setProblemDetails] = useState({
displayId: 1000,
difficulty: "EASY" as "EASY" | "MEDIUM" | "HARD",
difficulty: "EASY" as Difficulty,
timeLimit: 1000,
memoryLimit: 134217728,
isPublished: false,
@ -32,6 +32,7 @@ export default function EditDetailPanel({
});
} catch (error) {
console.error("获取题目信息失败:", error);
toast.error('加载详情失败');
}
}
fetchData();
@ -51,9 +52,27 @@ export default function EditDetailPanel({
};
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value === "EASY" || value === "MEDIUM" || value === "HARD") {
setProblemDetails({ ...problemDetails, difficulty: value });
const value = e.target.value as Difficulty;
setProblemDetails({ ...problemDetails, difficulty: value });
};
const handleSave = async () => {
try {
const res = await updateProblemDetail(problemId, {
displayId: problemDetails.displayId,
difficulty: problemDetails.difficulty,
timeLimit: problemDetails.timeLimit,
memoryLimit: problemDetails.memoryLimit,
isPublished: problemDetails.isPublished,
});
if (res.success) {
toast.success('保存成功');
} else {
toast.error('保存失败');
}
} catch (err) {
console.error('保存异常:', err);
toast.error('保存异常');
}
};
@ -121,15 +140,16 @@ export default function EditDetailPanel({
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"
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2"
/>
<Label
htmlFor="is-published"
className="text-sm font-medium text-gray-700 dark:text-gray-300"
>
<Label htmlFor="is-published" className="text-sm font-medium">
</Label>
</div>
<Button type="button" onClick={handleSave}>
</Button>
</div>
</CardContent>
</Card>

View File

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@ -11,6 +11,9 @@ 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";
import { toast } from "sonner";
import { updateProblemSolution } from '@/components/creater/problem-maintain';
import { Locale } from "@/generated/client";
export default function EditSolutionPanel({ problemId }: { problemId: string }) {
const [locales, setLocales] = useState<string[]>([]);
@ -22,9 +25,14 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
useEffect(() => {
async function fetchLocales() {
const langs = await getProblemLocales(problemId);
setLocales(langs);
if (langs.length > 0) setCurrentLocale(langs[0]);
try {
const langs = await getProblemLocales(problemId);
setLocales(langs);
if (langs.length > 0) setCurrentLocale(langs[0]);
} catch (err) {
console.error(err);
toast.error('获取语言列表失败');
}
}
fetchLocales();
}, [problemId]);
@ -32,24 +40,44 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
useEffect(() => {
if (!currentLocale) return;
async function fetchSolution() {
const data = await getProblemData(problemId, currentLocale);
setSolution({
title: (data?.title || "") + " 解析",
content: data?.solution || "",
});
try {
const data = await getProblemData(problemId, currentLocale);
setSolution({ title: (data?.title || "") + " 解析", content: data?.solution || "" });
} catch (err) {
console.error(err);
toast.error('加载题目解析失败');
}
}
fetchSolution();
}, [problemId, currentLocale]);
function handleAddCustomLocale() {
const handleAddCustomLocale = () => {
if (customLocale && !locales.includes(customLocale)) {
const newLocales = [...locales, customLocale];
setLocales(newLocales);
setLocales(prev => [...prev, customLocale]);
setCurrentLocale(customLocale);
setCustomLocale("");
setSolution({ title: "", content: "" });
}
}
};
const handleSave = async (): Promise<void> => {
if (!currentLocale) {
toast.error('请选择语言');
return;
}
try {
const locale = currentLocale as Locale;
const res = await updateProblemSolution(problemId, locale, solution.content);
if (res.success) {
toast.success('保存成功');
} else {
toast.error('保存失败');
}
} catch (err) {
console.error(err);
toast.error('保存异常');
}
};
return (
<Card className="w-full">
@ -77,11 +105,11 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button onClick={handleAddCustomLocale}></Button>
<Button type="button" onClick={handleAddCustomLocale}></Button>
</div>
</div>
{/* 标题输入 */}
{/* 标题输入 (仅展示) */}
<div className="space-y-2">
<Label htmlFor="solution-title"></Label>
<Input
@ -89,32 +117,17 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
value={solution.title}
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
placeholder="输入题解标题"
disabled
/>
</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")}
>
<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>
<Button type="button" variant={viewMode === "compare" ? "default" : "outline"} onClick={() => setViewMode("compare")}></Button>
</div>
{/* 编辑/预览区域 */}
@ -136,7 +149,7 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
)}
</div>
<Button></Button>
<Button type="button" onClick={handleSave}></Button>
</CardContent>
</Card>
);

View File

@ -1,240 +1,208 @@
"use client";
import {useState, useEffect} from "react";
import {generateAITestcase} from "@/app/actions/ai-testcase";
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";
import React, { useState, useEffect } from "react";
import { generateAITestcase } from "@/app/actions/ai-testcase";
import { getProblemData } from "@/app/actions/getProblem";
import {
addProblemTestcase,
updateProblemTestcase,
deleteProblemTestcase,
} from "@/components/creater/problem-maintain";
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 { toast } from "sonner";
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: ""}],
},
]);
};
interface Testcase {
id: string;
expectedOutput: string;
inputs: { name: string; value: string }[];
}
export default function EditTestcasePanel({ problemId }: { problemId: string }) {
const [testcases, setTestcases] = useState<Testcase[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
// 1) Load existing testcases
useEffect(() => {
async function fetch() {
try {
const data = await getProblemData(problemId);
setTestcases(data.testcases || []);
} catch (err) {
console.error("加载测试用例失败:", err);
toast.error("加载测试用例失败");
}
}
fetch();
}, [problemId]);
// 2) Local add
const handleAddTestcase = () =>
setTestcases((prev) => [
...prev,
{ id: `new-${Date.now()}`, expectedOutput: "", inputs: [{ name: "input1", value: "" }] },
]);
// 3) AI generation
const handleAITestcase = async () => {
setIsGenerating(true);
try {
const AIOutputParsed = await generateAITestcase({problemId: problemId});
setTestcases([
...testcases,
{
id: `new-${Date.now()}`,
expectedOutput: AIOutputParsed.expectedOutput,
inputs: AIOutputParsed.inputs
}
])
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth',
});
} catch (error) {
console.error(error)
const ai = await generateAITestcase({ problemId });
setTestcases((prev) => [
...prev,
{ id: `new-${Date.now()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs },
]);
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
} catch (err) {
console.error(err);
toast.error("AI 生成测试用例失败");
} finally {
setIsGenerating(false);
}
}
const handleRemoveTestcase = (index: number) => {
const newTestcases = [...testcases];
newTestcases.splice(index, 1);
setTestcases(newTestcases);
};
// 4) Remove (local + remote if existing)
const handleRemoveTestcase = async (idx: number) => {
const tc = testcases[idx];
if (!tc.id.startsWith("new-")) {
try {
const res = await deleteProblemTestcase(problemId, tc.id);
if (res.success) toast.success("删除测试用例成功");
else toast.error("删除测试用例失败");
} catch (err) {
console.error(err);
toast.error("删除测试用例异常");
}
}
setTestcases((prev) => prev.filter((_, i) => i !== idx));
};
// 5) Field updates
const handleExpectedOutputChange = (idx: number, val: string) =>
setTestcases((prev) => {
const c = [...prev];
c[idx].expectedOutput = val;
return c;
});
const handleInputChange = (
testcaseIndex: number,
inputIndex: number,
tIdx: number,
iIdx: 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: "",
val: string
) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs[iIdx][field] = val;
return c;
});
setTestcases(newTestcases);
};
const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => {
const newTestcases = [...testcases];
newTestcases[testcaseIndex].inputs.splice(inputIndex, 1);
setTestcases(newTestcases);
const handleAddInput = (tIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs.push({ name: `input${c[tIdx].inputs.length + 1}`, value: "" });
return c;
});
const handleRemoveInput = (tIdx: number, iIdx: number) =>
setTestcases((prev) => {
const c = [...prev];
c[tIdx].inputs.splice(iIdx, 1);
return c;
});
// 6) Persist all changes
const handleSaveAll = async () => {
try {
for (const tc of testcases) {
if (tc.id.startsWith("new-")) {
const res = await addProblemTestcase(problemId, tc.expectedOutput, tc.inputs);
if (res.success) toast.success("新增测试用例成功");
else toast.error("新增测试用例失败");
} else {
const res = await updateProblemTestcase(
problemId,
tc.id,
tc.expectedOutput,
tc.inputs
);
if (res.success) toast.success("更新测试用例成功");
else toast.error("更新测试用例失败");
}
}
} catch (err) {
console.error(err);
toast.error("保存测试用例异常");
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<div className="flex items-center space-x-1"> {/* space-x-1 让按钮更接近 */}
<Button type="button" onClick={handleAddTestcase}>
<div className="flex items-center space-x-2">
<Button onClick={handleAddTestcase}></Button>
<Button onClick={handleAITestcase} disabled={isGenerating}>
{isGenerating ? "生成中..." : "使用AI生成测试用例"}
</Button>
<Button
type="button"
className="flex items-center gap-1"
onClick={handleAITestcase}
disabled={isGenerating}
style={{
opacity: isGenerating ? 0.7 : 1,
cursor: isGenerating ? 'not-allowed' : 'pointer'
}}
>
<svg
data-testid="geist-icon"
height="16"
strokeLinejoin="round"
style={{color: isGenerating ? '#888' : "currentColor"}}
viewBox="0 0 16 16"
width="16"
>
<path
d="M2.5 0.5V0H3.5V0.5C3.5 1.60457 4.39543 2.5 5.5 2.5H6V3V3.5H5.5C4.39543 3.5 3.5 4.39543 3.5 5.5V6H3H2.5V5.5C2.5 4.39543 1.60457 3.5 0.5 3.5H0V3V2.5H0.5C1.60457 2.5 2.5 1.60457 2.5 0.5Z"
fill="currentColor"></path>
<path
d="M14.5 4.5V5H13.5V4.5C13.5 3.94772 13.0523 3.5 12.5 3.5H12V3V2.5H12.5C13.0523 2.5 13.5 2.05228 13.5 1.5V1H14H14.5V1.5C14.5 2.05228 14.9477 2.5 15.5 2.5H16V3V3.5H15.5C14.9477 3.5 14.5 3.94772 14.5 4.5Z"
fill="currentColor"></path>
<path
d="M8.40706 4.92939L8.5 4H9.5L9.59294 4.92939C9.82973 7.29734 11.7027 9.17027 14.0706 9.40706L15 9.5V10.5L14.0706 10.5929C11.7027 10.8297 9.82973 12.7027 9.59294 15.0706L9.5 16H8.5L8.40706 15.0706C8.17027 12.7027 6.29734 10.8297 3.92939 10.5929L3 10.5V9.5L3.92939 9.40706C6.29734 9.17027 8.17027 7.29734 8.40706 4.92939Z"
fill="currentColor"></path>
</svg>
{isGenerating ? '生成中...' : '使用AI生成测试用例'}
<Button variant="secondary" onClick={handleSaveAll}>
</Button>
</div>
</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>
<CardContent className="space-y-6">
{testcases.map((tc, idx) => (
<div key={tc.id} className="border p-4 rounded space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium"> {idx + 1}</h3>
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}>
</Button>
</div>
))}
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={tc.expectedOutput}
onChange={(e) => handleExpectedOutputChange(idx, e.target.value)}
placeholder="输入预期输出"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label></Label>
<Button onClick={() => handleAddInput(idx)}></Button>
</div>
{tc.inputs.map((inp, iIdx) => (
<div key={iIdx} className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
value={inp.name}
onChange={(e) => handleInputChange(idx, iIdx, "name", e.target.value)}
placeholder="参数名称"
/>
</div>
<div>
<Label></Label>
<Input
value={inp.value}
onChange={(e) => handleInputChange(idx, iIdx, "value", e.target.value)}
placeholder="参数值"
/>
</div>
{iIdx > 0 && (
<Button variant="outline" onClick={() => handleRemoveInput(idx, iIdx)}>
</Button>
)}
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,230 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { Difficulty, Locale, ProblemContentType, Language } from "@/generated/client";
export async function updateProblemDetail(problemId: string, data: { displayId?: number; difficulty?: Difficulty; timeLimit?: number; memoryLimit?: number; isPublished?: boolean }) {
try {
const updatedProblem = await prisma.problem.update({
where: { id: problemId },
data: {
displayId: data.displayId,
difficulty: data.difficulty,
timeLimit: data.timeLimit,
memoryLimit: data.memoryLimit,
isPublished: data.isPublished
}
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, problem: updatedProblem };
} catch (error) {
console.error("Failed to update problem detail:", error);
return { success: false, error: "Failed to update problem detail" };
}
}
export async function updateProblemDescription(problemId: string, locale: Locale, content: string) {
try {
const updatedLocalization = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId: problemId,
locale: locale,
type: ProblemContentType.DESCRIPTION
}
},
create: {
problemId: problemId,
locale: locale,
type: ProblemContentType.DESCRIPTION,
content: content
},
update: {
content: content
}
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updatedLocalization };
} catch (error) {
console.error("Failed to update problem description:", error);
return { success: false, error: "Failed to update problem description" };
}
}
export async function updateProblemSolution(problemId: string, locale: Locale, content: string) {
try {
const updatedLocalization = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId: problemId,
locale: locale,
type: ProblemContentType.SOLUTION
}
},
create: {
problemId: problemId,
locale: locale,
type: ProblemContentType.SOLUTION,
content: content
},
update: {
content: content
}
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updatedLocalization };
} catch (error) {
console.error("Failed to update problem solution:", error);
return { success: false, error: "Failed to update problem solution" };
}
}
export async function updateProblemTemplate(problemId: string, language: Language, content: string) {
try {
const updatedTemplate = await prisma.template.upsert({
where: {
problemId_language: {
problemId: problemId,
language: language
}
},
create: {
problemId: problemId,
language: language,
content: content
},
update: {
content: content
}
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, template: updatedTemplate };
} catch (error) {
console.error("Failed to update problem template:", error);
return { success: false, error: "Failed to update problem template" };
}
}
export async function updateProblemTestcase(problemId: string, testcaseId: string, expectedOutput: string, inputs: { name: string; value: string }[]) {
try {
// Update testcase
const updatedTestcase = await prisma.testcase.update({
where: { id: testcaseId },
data: {
expectedOutput: expectedOutput
}
});
// Delete old inputs
await prisma.testcaseInput.deleteMany({
where: { testcaseId: testcaseId }
});
// Create new inputs
const createdInputs = await prisma.testcaseInput.createMany({
data: inputs.map((input, index) => ({
testcaseId: testcaseId,
index: index,
name: input.name,
value: input.value
}))
});
revalidatePath(`/problem-editor/${problemId}`);
return {
success: true,
testcase: updatedTestcase,
inputs: createdInputs
};
} catch (error) {
console.error("Failed to update problem testcase:", error);
return { success: false, error: "Failed to update problem testcase" };
}
}
export async function addProblemTestcase(problemId: string, expectedOutput: string, inputs: { name: string; value: string }[]) {
try {
// Create testcase
const newTestcase = await prisma.testcase.create({
data: {
problemId: problemId,
expectedOutput: expectedOutput
}
});
// Create inputs
const createdInputs = await prisma.testcaseInput.createMany({
data: inputs.map((input, index) => ({
testcaseId: newTestcase.id,
index: index,
name: input.name,
value: input.value
}))
});
revalidatePath(`/problem-editor/${problemId}`);
return {
success: true,
testcase: newTestcase,
inputs: createdInputs
};
} catch (error) {
console.error("Failed to add problem testcase:", error);
return { success: false, error: "Failed to add problem testcase" };
}
}
export async function deleteProblemTestcase(problemId: string, testcaseId: string) {
try {
const deletedTestcase = await prisma.testcase.delete({
where: { id: testcaseId }
});
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, testcase: deletedTestcase };
} catch (error) {
console.error("Failed to delete problem testcase:", error);
return { success: false, error: "Failed to delete problem testcase" };
}
}
/**
* TITLE
*/
export async function updateProblemTitle(
problemId: string,
locale: Locale,
title: string
) {
try {
const updated = await prisma.problemLocalization.upsert({
where: {
problemId_locale_type: {
problemId,
locale,
type: ProblemContentType.TITLE,
},
},
create: {
problemId,
locale,
type: ProblemContentType.TITLE,
content: title,
},
update: {
content: title,
},
});
// 重新缓存编辑页
revalidatePath(`/problem-editor/${problemId}`);
return { success: true, localization: updated };
} catch (error) {
console.error("更新题目标题失败:", error);
return { success: false, error: "更新题目标题失败" };
}
}