From 9b7d7a8cfd2edc61fab06de5913539ad7dbf739a Mon Sep 17 00:00:00 2001 From: fly6516 Date: Tue, 17 Jun 2025 16:18:59 +0800 Subject: [PATCH] feat(creater): add feature to display multi-language description and solution in problem-creater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在编辑题目描述和解析面板中添加语言切换功能 - 实现获取题目支持的语言列表和对应语言的题目数据 - 增加添加新语言的功能(仅前端) -优化题目描述和解析的编辑、预览和对比功能 - 在预览中添加 Accordion 和 VideoEmbed 组件支持 --- src/app/actions/getProblem.ts | 27 ++- .../creater/edit-description-panel.tsx | 189 +++++++++++------- .../creater/edit-solution-panel.tsx | 171 ++++++++++------ 3 files changed, 237 insertions(+), 150 deletions(-) diff --git a/src/app/actions/getProblem.ts b/src/app/actions/getProblem.ts index 271e1fe..7fd776c 100644 --- a/src/app/actions/getProblem.ts +++ b/src/app/actions/getProblem.ts @@ -2,18 +2,25 @@ '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) { +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: { - localizations: true, templates: true, testcases: { include: { inputs: true } - } - } + }, + localizations: { + where: { + locale: selectedLocale, // ✅ 这里使用枚举 + }, + }, + }, }); if (!problem) { @@ -25,9 +32,7 @@ export async function getProblemData(problemId: string) { const rawDescription = getContent('DESCRIPTION'); - // MDX序列化,给客户端渲染用 const mdxDescription = await serialize(rawDescription, { - // 可以根据需要添加MDX插件配置 parseFrontmatter: false, }); @@ -40,19 +45,19 @@ export async function getProblemData(problemId: string) { memoryLimit: problem.memoryLimit, title: getContent('TITLE'), description: rawDescription, - mdxDescription, // 新增序列化后的字段 + mdxDescription, solution: getContent('SOLUTION'), templates: problem.templates.map(t => ({ language: t.language, - content: t.content + 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 - })) - })) + value: input.value, + })), + })), }; } diff --git a/src/components/creater/edit-description-panel.tsx b/src/components/creater/edit-description-panel.tsx index e5f3bd5..24667e3 100644 --- a/src/components/creater/edit-description-panel.tsx +++ b/src/components/creater/edit-description-panel.tsx @@ -1,108 +1,147 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import MdxPreview from "@/components/mdx-preview"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { CoreEditor } from '@/components/core-editor'; -import { getProblemData } from '@/app/actions/getProblem'; -import { Accordion } from "@/components/ui/accordion"; // ← 这里导入 Accordion +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 [description, setDescription] = useState({ - title: `Description for Problem ${problemId}`, - content: `Description content for Problem ${problemId}...` - }); - const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'compare'>('edit'); +export default function EditDescriptionPanel({ problemId }: { problemId: string }) { + const [locales, setLocales] = useState([]); + const [currentLocale, setCurrentLocale] = useState(""); + const [customLocale, setCustomLocale] = useState(""); + const [description, setDescription] = useState({ title: "", content: "" }); + const [viewMode, setViewMode] = useState<"edit" | "preview" | "compare">("edit"); + + // 获取语言列表 useEffect(() => { - async function fetchData() { - try { - const problemData = await getProblemData(problemId); - setDescription({ - title: problemData.title, - content: problemData.description - }); - } catch (error) { - console.error('获取题目信息失败:', error); + async function fetchLocales() { + const langs = await getProblemLocales(problemId); + setLocales(langs); + if (langs.length > 0) { + setCurrentLocale(langs[0]); } } - - fetchData(); + 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 ( 题目描述 - -
-
- - setDescription({...description, title: e.target.value})} - placeholder="输入题目标题" - /> -
- + + {/* 语言切换 */} +
+
-
+
-
-
+ {/* 标题输入 */} +
+ + setDescription({ ...description, title: e.target.value })} + placeholder="输入题目标题" + /> +
+ + {/* 编辑/预览切换 */} +
+ + + +
+ + {/* 编辑/预览区域 */} +
+ {(viewMode === "edit" || viewMode === "compare") && (
- setDescription({ ...description, content: newContent || '' }) - } + onChange={(newVal) => setDescription({ ...description, content: newVal || "" })} language="markdown" className="absolute inset-0 rounded-md border border-input" />
-
- {viewMode !== 'edit' && ( -
- -
- )} -
- - + )} + {viewMode !== "edit" && ( +
+ +
+ )}
+ +
); diff --git a/src/components/creater/edit-solution-panel.tsx b/src/components/creater/edit-solution-panel.tsx index 3f16b86..3cc02ea 100644 --- a/src/components/creater/edit-solution-panel.tsx +++ b/src/components/creater/edit-solution-panel.tsx @@ -1,99 +1,142 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import MdxPreview from "@/components/mdx-preview"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CoreEditor } from "@/components/core-editor"; -import { getProblemData } from "@/app/actions/getProblem";// 修改为你实际路径 +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 [solution, setSolution] = useState({ - title: `Solution for Problem ${problemId}`, - content: `Solution content for Problem ${problemId}...`, - }); +export default function EditSolutionPanel({ problemId }: { problemId: string }) { + const [locales, setLocales] = useState([]); + const [currentLocale, setCurrentLocale] = useState(""); + 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() { - try { - const data = await getProblemData(problemId); - setSolution({ - title: data.title ? data.title + " 解析" : `Solution for Problem ${problemId}`, - content: data.solution || "", - }); - } catch (error) { - console.error("加载题解失败", error); - } + const data = await getProblemData(problemId, currentLocale); + setSolution({ + title: (data?.title || "") + " 解析", + content: data?.solution || "", + }); } fetchSolution(); - }, [problemId]); + }, [problemId, currentLocale]); + + function handleAddCustomLocale() { + if (customLocale && !locales.includes(customLocale)) { + const newLocales = [...locales, customLocale]; + setLocales(newLocales); + setCurrentLocale(customLocale); + setCustomLocale(""); + setSolution({ title: "", content: "" }); + } + } return ( 题目解析 - -
-
- - setSolution({ ...solution, title: e.target.value })} - placeholder="输入题解标题" - /> -
+ + {/* 语言切换 */} +
+
-
+
-
-
+ {/* 标题输入 */} +
+ + setSolution({ ...solution, title: e.target.value })} + placeholder="输入题解标题" + /> +
+ + {/* 编辑/预览切换 */} +
+ + + +
+ + {/* 编辑/预览区域 */} +
+ {(viewMode === "edit" || viewMode === "compare") && (
setSolution({ ...solution, content: newContent })} + onChange={(val) => setSolution({ ...solution, content: val || "" })} language="markdown" className="absolute inset-0 rounded-md border border-input" />
-
- - {viewMode !== "edit" && ( -
- -
- )} -
+ )} + {viewMode !== "edit" && ( +
+ +
+ )}
+ +
);