mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 09:20:53 +00:00
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:
parent
fc204b8aa3
commit
f858ccb066
@ -2,11 +2,11 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { Locale } from '@/generated/client'; // ✅ 导入 enum Locale
|
import { Locale } from '@/generated/client';
|
||||||
import { serialize } from 'next-mdx-remote/serialize';
|
import { serialize } from 'next-mdx-remote/serialize';
|
||||||
|
|
||||||
export async function getProblemData(problemId: string, locale: string) {
|
export async function getProblemData(problemId: string, locale?: string) {
|
||||||
const selectedLocale = locale as Locale; // ✅ 强制转换 string 为 Prisma enum
|
const selectedLocale = locale as Locale;
|
||||||
|
|
||||||
const problem = await prisma.problem.findUnique({
|
const problem = await prisma.problem.findUnique({
|
||||||
where: { id: problemId },
|
where: { id: problemId },
|
||||||
@ -17,7 +17,7 @@ export async function getProblemData(problemId: string, locale: string) {
|
|||||||
},
|
},
|
||||||
localizations: {
|
localizations: {
|
||||||
where: {
|
where: {
|
||||||
locale: selectedLocale, // ✅ 这里使用枚举
|
locale: selectedLocale,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 { getProblemData } from '@/app/actions/getProblem';
|
||||||
|
import { updateProblemTemplate } from '@/components/creater/problem-maintain';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { CoreEditor } from "@/components/core-editor";
|
import { CoreEditor } from '@/components/core-editor';
|
||||||
|
import { Language } from '@/generated/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface Template {
|
interface Template {
|
||||||
language: string;
|
language: string;
|
||||||
@ -14,65 +17,50 @@ interface Template {
|
|||||||
|
|
||||||
interface EditCodePanelProps {
|
interface EditCodePanelProps {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
onUpdate?: (data: Template) => Promise<{ success: boolean }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟保存函数
|
export default function EditCodePanel({ problemId }: EditCodePanelProps) {
|
||||||
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>({
|
const [codeTemplate, setCodeTemplate] = useState<Template>({
|
||||||
language: 'cpp',
|
language: 'cpp',
|
||||||
content: `// 默认代码模板 for Problem ${problemId}`,
|
content: `// 默认代码模板 for Problem ${problemId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [templates, setTemplates] = useState<Template[]>([]);
|
const [templates, setTemplates] = useState<Template[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetch() {
|
async function fetchTemplates() {
|
||||||
try {
|
try {
|
||||||
const problem = await getProblemData(problemId);
|
const problem = await getProblemData(problemId);
|
||||||
setTemplates(problem.templates);
|
setTemplates(problem.templates);
|
||||||
const cppTemplate = problem.templates.find(t => t.language === 'cpp');
|
const sel = problem.templates.find(t => t.language === 'cpp') || problem.templates[0];
|
||||||
setCodeTemplate(cppTemplate || problem.templates[0]);
|
if (sel) setCodeTemplate(sel);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载问题数据失败:', err);
|
console.error('加载问题数据失败:', err);
|
||||||
|
toast.error('加载问题数据失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetch();
|
fetchTemplates();
|
||||||
}, [problemId]);
|
}, [problemId]);
|
||||||
|
|
||||||
const handleLanguageChange = (language: string) => {
|
const handleLanguageChange = (language: string) => {
|
||||||
const selected = templates.find(t => t.language === language);
|
const sel = templates.find(t => t.language === language);
|
||||||
if (selected) {
|
if (sel) setCodeTemplate(sel);
|
||||||
setCodeTemplate(selected);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 事件处理函数返回 Promise<void>,不要返回具体数据
|
|
||||||
const handleSave = async (): Promise<void> => {
|
const handleSave = async (): Promise<void> => {
|
||||||
if (!onUpdate) {
|
|
||||||
alert('保存函数未传入,无法保存');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const result = await onUpdate(codeTemplate);
|
const res = await updateProblemTemplate(
|
||||||
if (result.success) {
|
problemId,
|
||||||
alert('保存成功');
|
codeTemplate.language as Language,
|
||||||
|
codeTemplate.content
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('保存成功');
|
||||||
} else {
|
} else {
|
||||||
alert('保存失败');
|
toast.error('保存失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('保存异常:', error);
|
||||||
alert('保存异常');
|
toast.error('保存异常');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,9 +77,9 @@ export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: Ed
|
|||||||
id="language-select"
|
id="language-select"
|
||||||
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
|
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
|
||||||
value={codeTemplate.language}
|
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}>
|
<option key={t.language} value={t.language}>
|
||||||
{t.language.toUpperCase()}
|
{t.language.toUpperCase()}
|
||||||
</option>
|
</option>
|
||||||
@ -106,7 +94,7 @@ export default function EditCodePanel({ problemId, onUpdate = saveTemplate }: Ed
|
|||||||
language={codeTemplate.language}
|
language={codeTemplate.language}
|
||||||
value={codeTemplate.content}
|
value={codeTemplate.content}
|
||||||
path={`/${problemId}.${codeTemplate.language}`}
|
path={`/${problemId}.${codeTemplate.language}`}
|
||||||
onChange={(value) =>
|
onChange={value =>
|
||||||
setCodeTemplate({ ...codeTemplate, content: value || '' })
|
setCodeTemplate({ ...codeTemplate, content: value || '' })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -11,6 +11,9 @@ import { getProblemData } from "@/app/actions/getProblem";
|
|||||||
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
||||||
import { Accordion } from "@/components/ui/accordion";
|
import { Accordion } from "@/components/ui/accordion";
|
||||||
import { VideoEmbed } from "@/components/content/video-embed";
|
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 }) {
|
export default function EditDescriptionPanel({ problemId }: { problemId: string }) {
|
||||||
const [locales, setLocales] = useState<string[]>([]);
|
const [locales, setLocales] = useState<string[]>([]);
|
||||||
@ -20,33 +23,35 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
|
|||||||
const [description, setDescription] = useState({ title: "", content: "" });
|
const [description, setDescription] = useState({ title: "", content: "" });
|
||||||
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit");
|
||||||
|
|
||||||
// 获取语言列表
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLocales() {
|
async function fetchLocales() {
|
||||||
|
try {
|
||||||
const langs = await getProblemLocales(problemId);
|
const langs = await getProblemLocales(problemId);
|
||||||
setLocales(langs);
|
setLocales(langs);
|
||||||
if (langs.length > 0) {
|
if (langs.length > 0) setCurrentLocale(langs[0]);
|
||||||
setCurrentLocale(langs[0]);
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('获取语言列表失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchLocales();
|
fetchLocales();
|
||||||
}, [problemId]);
|
}, [problemId]);
|
||||||
|
|
||||||
// 获取对应语言的题目数据
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentLocale) return;
|
if (!currentLocale) return;
|
||||||
async function fetchProblem() {
|
async function fetchProblem() {
|
||||||
|
try {
|
||||||
const data = await getProblemData(problemId, currentLocale);
|
const data = await getProblemData(problemId, currentLocale);
|
||||||
setDescription({
|
setDescription({ title: data?.title || "", content: data?.description || "" });
|
||||||
title: data?.title || "",
|
} catch (err) {
|
||||||
content: data?.description || "",
|
console.error(err);
|
||||||
});
|
toast.error('加载题目描述失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchProblem();
|
fetchProblem();
|
||||||
}, [problemId, currentLocale]);
|
}, [problemId, currentLocale]);
|
||||||
|
|
||||||
// 添加新语言(仅前端)
|
const handleAddCustomLocale = () => {
|
||||||
function handleAddCustomLocale() {
|
|
||||||
if (customLocale && !locales.includes(customLocale)) {
|
if (customLocale && !locales.includes(customLocale)) {
|
||||||
const newLocales = [...locales, customLocale];
|
const newLocales = [...locales, customLocale];
|
||||||
setLocales(newLocales);
|
setLocales(newLocales);
|
||||||
@ -54,7 +59,27 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
|
|||||||
setCustomLocale("");
|
setCustomLocale("");
|
||||||
setDescription({ title: "", content: "" });
|
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 (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@ -141,7 +166,7 @@ export default function EditDescriptionPanel({ problemId }: { problemId: string
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button>保存更改</Button>
|
<Button onClick={handleSave}>保存更改</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { getProblemData } from "@/app/actions/getProblem";
|
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({
|
export default function EditDetailPanel({ problemId }: { problemId: string }) {
|
||||||
problemId,
|
|
||||||
}: {
|
|
||||||
problemId: string;
|
|
||||||
}) {
|
|
||||||
const [problemDetails, setProblemDetails] = useState({
|
const [problemDetails, setProblemDetails] = useState({
|
||||||
displayId: 1000,
|
displayId: 1000,
|
||||||
difficulty: "EASY" as "EASY" | "MEDIUM" | "HARD",
|
difficulty: "EASY" as Difficulty,
|
||||||
timeLimit: 1000,
|
timeLimit: 1000,
|
||||||
memoryLimit: 134217728,
|
memoryLimit: 134217728,
|
||||||
isPublished: false,
|
isPublished: false,
|
||||||
@ -32,6 +32,7 @@ export default function EditDetailPanel({
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取题目信息失败:", error);
|
console.error("获取题目信息失败:", error);
|
||||||
|
toast.error('加载详情失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -51,9 +52,27 @@ export default function EditDetailPanel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value as Difficulty;
|
||||||
if (value === "EASY" || value === "MEDIUM" || value === "HARD") {
|
|
||||||
setProblemDetails({ ...problemDetails, difficulty: value });
|
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) =>
|
onChange={(e) =>
|
||||||
setProblemDetails({ ...problemDetails, isPublished: e.target.checked })
|
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
|
<Label htmlFor="is-published" className="text-sm font-medium">
|
||||||
htmlFor="is-published"
|
|
||||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
是否发布
|
是否发布
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" onClick={handleSave}>
|
||||||
|
保存更改
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -11,6 +11,9 @@ import { getProblemData } from "@/app/actions/getProblem";
|
|||||||
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
import { getProblemLocales } from "@/app/actions/getProblemLocales";
|
||||||
import { Accordion } from "@/components/ui/accordion";
|
import { Accordion } from "@/components/ui/accordion";
|
||||||
import { VideoEmbed } from "@/components/content/video-embed";
|
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 }) {
|
export default function EditSolutionPanel({ problemId }: { problemId: string }) {
|
||||||
const [locales, setLocales] = useState<string[]>([]);
|
const [locales, setLocales] = useState<string[]>([]);
|
||||||
@ -22,9 +25,14 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLocales() {
|
async function fetchLocales() {
|
||||||
|
try {
|
||||||
const langs = await getProblemLocales(problemId);
|
const langs = await getProblemLocales(problemId);
|
||||||
setLocales(langs);
|
setLocales(langs);
|
||||||
if (langs.length > 0) setCurrentLocale(langs[0]);
|
if (langs.length > 0) setCurrentLocale(langs[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('获取语言列表失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchLocales();
|
fetchLocales();
|
||||||
}, [problemId]);
|
}, [problemId]);
|
||||||
@ -32,24 +40,44 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentLocale) return;
|
if (!currentLocale) return;
|
||||||
async function fetchSolution() {
|
async function fetchSolution() {
|
||||||
|
try {
|
||||||
const data = await getProblemData(problemId, currentLocale);
|
const data = await getProblemData(problemId, currentLocale);
|
||||||
setSolution({
|
setSolution({ title: (data?.title || "") + " 解析", content: data?.solution || "" });
|
||||||
title: (data?.title || "") + " 解析",
|
} catch (err) {
|
||||||
content: data?.solution || "",
|
console.error(err);
|
||||||
});
|
toast.error('加载题目解析失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchSolution();
|
fetchSolution();
|
||||||
}, [problemId, currentLocale]);
|
}, [problemId, currentLocale]);
|
||||||
|
|
||||||
function handleAddCustomLocale() {
|
const handleAddCustomLocale = () => {
|
||||||
if (customLocale && !locales.includes(customLocale)) {
|
if (customLocale && !locales.includes(customLocale)) {
|
||||||
const newLocales = [...locales, customLocale];
|
setLocales(prev => [...prev, customLocale]);
|
||||||
setLocales(newLocales);
|
|
||||||
setCurrentLocale(customLocale);
|
setCurrentLocale(customLocale);
|
||||||
setCustomLocale("");
|
setCustomLocale("");
|
||||||
setSolution({ title: "", content: "" });
|
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 (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@ -77,11 +105,11 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
|
|||||||
value={customLocale}
|
value={customLocale}
|
||||||
onChange={(e) => setCustomLocale(e.target.value)}
|
onChange={(e) => setCustomLocale(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleAddCustomLocale}>添加</Button>
|
<Button type="button" onClick={handleAddCustomLocale}>添加</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 标题输入 */}
|
{/* 标题输入 (仅展示) */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="solution-title">题解标题</Label>
|
<Label htmlFor="solution-title">题解标题</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -89,32 +117,17 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
|
|||||||
value={solution.title}
|
value={solution.title}
|
||||||
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
|
onChange={(e) => setSolution({ ...solution, title: e.target.value })}
|
||||||
placeholder="输入题解标题"
|
placeholder="输入题解标题"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 编辑/预览切换 */}
|
{/* 编辑/预览切换 */}
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button
|
<Button type="button" variant={viewMode === "edit" ? "default" : "outline"} onClick={() => setViewMode("edit")}>编辑</Button>
|
||||||
type="button"
|
<Button type="button" variant={viewMode === "preview" ? "default" : "outline"} onClick={() => setViewMode(viewMode === "preview" ? "edit" : "preview")}>
|
||||||
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" ? "取消" : "预览"}
|
{viewMode === "preview" ? "取消" : "预览"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" variant={viewMode === "compare" ? "default" : "outline"} onClick={() => setViewMode("compare")}>对比</Button>
|
||||||
type="button"
|
|
||||||
variant={viewMode === "compare" ? "default" : "outline"}
|
|
||||||
onClick={() => setViewMode("compare")}
|
|
||||||
>
|
|
||||||
对比
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 编辑/预览区域 */}
|
{/* 编辑/预览区域 */}
|
||||||
@ -136,7 +149,7 @@ export default function EditSolutionPanel({ problemId }: { problemId: string })
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button>保存更改</Button>
|
<Button type="button" onClick={handleSave}>保存更改</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -1,229 +1,199 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useState, useEffect} from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {generateAITestcase} from "@/app/actions/ai-testcase";
|
import { generateAITestcase } from "@/app/actions/ai-testcase";
|
||||||
import {Label} from "@/components/ui/label";
|
import { getProblemData } from "@/app/actions/getProblem";
|
||||||
import {Input} from "@/components/ui/input";
|
import {
|
||||||
import {Button} from "@/components/ui/button";
|
addProblemTestcase,
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
updateProblemTestcase,
|
||||||
import {getProblemData} from "@/app/actions/getProblem";
|
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({
|
interface Testcase {
|
||||||
problemId,
|
|
||||||
}: {
|
|
||||||
problemId: string;
|
|
||||||
}) {
|
|
||||||
const [testcases, setTestcases] = useState<
|
|
||||||
Array<{
|
|
||||||
id: string;
|
id: string;
|
||||||
expectedOutput: string;
|
expectedOutput: string;
|
||||||
inputs: Array<{
|
inputs: { name: string; value: string }[];
|
||||||
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: ""}],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export default function EditTestcasePanel({ problemId }: { problemId: string }) {
|
||||||
|
const [testcases, setTestcases] = useState<Testcase[]>([]);
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
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 () => {
|
const handleAITestcase = async () => {
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
const AIOutputParsed = await generateAITestcase({problemId: problemId});
|
const ai = await generateAITestcase({ problemId });
|
||||||
setTestcases([
|
setTestcases((prev) => [
|
||||||
...testcases,
|
...prev,
|
||||||
{
|
{ id: `new-${Date.now()}`, expectedOutput: ai.expectedOutput, inputs: ai.inputs },
|
||||||
id: `new-${Date.now()}`,
|
]);
|
||||||
expectedOutput: AIOutputParsed.expectedOutput,
|
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
||||||
inputs: AIOutputParsed.inputs
|
} catch (err) {
|
||||||
}
|
console.error(err);
|
||||||
])
|
toast.error("AI 生成测试用例失败");
|
||||||
window.scrollTo({
|
|
||||||
top: document.body.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
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 = (
|
const handleInputChange = (
|
||||||
testcaseIndex: number,
|
tIdx: number,
|
||||||
inputIndex: number,
|
iIdx: number,
|
||||||
field: "name" | "value",
|
field: "name" | "value",
|
||||||
value: string
|
val: string
|
||||||
) => {
|
) =>
|
||||||
const newTestcases = [...testcases];
|
setTestcases((prev) => {
|
||||||
newTestcases[testcaseIndex].inputs[inputIndex][field] = value;
|
const c = [...prev];
|
||||||
setTestcases(newTestcases);
|
c[tIdx].inputs[iIdx][field] = val;
|
||||||
};
|
return c;
|
||||||
|
|
||||||
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 handleAddInput = (tIdx: number) =>
|
||||||
const newTestcases = [...testcases];
|
setTestcases((prev) => {
|
||||||
newTestcases[testcaseIndex].inputs.splice(inputIndex, 1);
|
const c = [...prev];
|
||||||
setTestcases(newTestcases);
|
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 (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>测试用例</CardTitle>
|
<CardTitle>测试用例</CardTitle>
|
||||||
<div className="flex items-center space-x-1"> {/* space-x-1 让按钮更接近 */}
|
<div className="flex items-center space-x-2">
|
||||||
<Button type="button" onClick={handleAddTestcase}>
|
<Button onClick={handleAddTestcase}>添加测试用例</Button>
|
||||||
添加测试用例
|
<Button onClick={handleAITestcase} disabled={isGenerating}>
|
||||||
|
{isGenerating ? "生成中..." : "使用AI生成测试用例"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="secondary" onClick={handleSaveAll}>
|
||||||
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-6">
|
||||||
<div className="space-y-6">
|
{testcases.map((tc, idx) => (
|
||||||
{testcases.map((testcase, index) => (
|
<div key={tc.id} className="border p-4 rounded space-y-4">
|
||||||
<div key={testcase.id} className="border p-4 rounded-md space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-lg font-medium">测试用例 {index + 1}</h3>
|
<h3 className="font-medium">测试用例 {idx + 1}</h3>
|
||||||
<Button
|
<Button variant="destructive" onClick={() => handleRemoveTestcase(idx)}>
|
||||||
type="button"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => handleRemoveTestcase(index)}
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`expected-output-${index}`}>预期输出</Label>
|
<Label>预期输出</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`expected-output-${index}`}
|
value={tc.expectedOutput}
|
||||||
value={testcase.expectedOutput}
|
onChange={(e) => handleExpectedOutputChange(idx, e.target.value)}
|
||||||
onChange={(e) => handleExpectedOutputChange(index, e.target.value)}
|
|
||||||
placeholder="输入预期输出"
|
placeholder="输入预期输出"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Label>输入参数</Label>
|
<Label>输入参数</Label>
|
||||||
<Button type="button" onClick={() => handleAddInput(index)}>
|
<Button onClick={() => handleAddInput(idx)}>添加输入</Button>
|
||||||
添加输入
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{tc.inputs.map((inp, iIdx) => (
|
||||||
{testcase.inputs.map((input, inputIndex) => (
|
<div key={iIdx} className="grid grid-cols-2 gap-4">
|
||||||
<div key={input.name} className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div className="space-y-2">
|
<Label>名称</Label>
|
||||||
<Label htmlFor={`input-name-${index}-${inputIndex}`}>
|
|
||||||
参数名称
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id={`input-name-${index}-${inputIndex}`}
|
value={inp.name}
|
||||||
value={input.name}
|
onChange={(e) => handleInputChange(idx, iIdx, "name", e.target.value)}
|
||||||
onChange={(e) =>
|
placeholder="参数名称"
|
||||||
handleInputChange(index, inputIndex, "name", e.target.value)
|
|
||||||
}
|
|
||||||
placeholder="输入参数名称"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
<Label htmlFor={`input-value-${index}-${inputIndex}`}>
|
<Label>值</Label>
|
||||||
参数值
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id={`input-value-${index}-${inputIndex}`}
|
value={inp.value}
|
||||||
value={input.value}
|
onChange={(e) => handleInputChange(idx, iIdx, "value", e.target.value)}
|
||||||
onChange={(e) =>
|
placeholder="参数值"
|
||||||
handleInputChange(index, inputIndex, "value", e.target.value)
|
|
||||||
}
|
|
||||||
placeholder="输入参数值"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{inputIndex > 0 && (
|
{iIdx > 0 && (
|
||||||
<Button
|
<Button variant="outline" onClick={() => handleRemoveInput(idx, iIdx)}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleRemoveInput(index, inputIndex)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
删除输入
|
删除输入
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -232,9 +202,7 @@ export default function EditTestcasePanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user