From 7d4e4ae2e4c0b55aea58eba6d5d1ab2d9b3fd7ef Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Thu, 19 Jun 2025 17:54:35 +0800 Subject: [PATCH] feat(admin): Implement admin problem editing and protected routing --- .../(app)/problem-editor/[problemId]/page.tsx | 121 ---------- src/app/(app)/problems/[problemId]/layout.tsx | 11 +- src/app/(app)/problems/[problemId]/page.tsx | 34 +-- .../problems/[problemId]/edit/layout.tsx | 19 ++ .../admin/problems/[problemId]/edit/page.tsx | 13 ++ src/app/(protected)/layout.tsx | 11 + src/components/content/pre-detail.tsx | 6 +- .../ui/components/problem-edit-flexlayout.tsx | 26 +++ .../ui/layouts/admin-protected-layout.tsx | 30 +++ .../admin/ui/layouts/problem-edit-layout.tsx | 29 +++ .../admin/ui/views/problem-edit-view.tsx | 26 +++ .../bot/components/view-bot-button.tsx | 2 +- .../problems/components/flexlayout.tsx | 111 +++++++++ .../problems/components/judge-button.tsx | 2 +- .../components/problem-flexlayout.tsx | 88 +------- .../problems/detail/components/header.tsx | 2 +- .../components/view-solution-button.tsx | 2 +- .../problems/submission/components/row.tsx | 2 +- .../problems/ui/views/problem-view.tsx | 31 +++ src/stores/flexlayout.ts | 211 ++++++++++++++++++ src/stores/problem-flexlayout.ts | 115 ---------- 21 files changed, 535 insertions(+), 357 deletions(-) delete mode 100644 src/app/(app)/problem-editor/[problemId]/page.tsx create mode 100644 src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx create mode 100644 src/app/(protected)/admin/problems/[problemId]/edit/page.tsx create mode 100644 src/app/(protected)/layout.tsx create mode 100644 src/features/admin/ui/components/problem-edit-flexlayout.tsx create mode 100644 src/features/admin/ui/layouts/admin-protected-layout.tsx create mode 100644 src/features/admin/ui/layouts/problem-edit-layout.tsx create mode 100644 src/features/admin/ui/views/problem-edit-view.tsx create mode 100644 src/features/problems/components/flexlayout.tsx create mode 100644 src/features/problems/ui/views/problem-view.tsx create mode 100644 src/stores/flexlayout.ts delete mode 100644 src/stores/problem-flexlayout.ts diff --git a/src/app/(app)/problem-editor/[problemId]/page.tsx b/src/app/(app)/problem-editor/[problemId]/page.tsx deleted file mode 100644 index 3e2be4c..0000000 --- a/src/app/(app)/problem-editor/[problemId]/page.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"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/(app)/problems/[problemId]/layout.tsx b/src/app/(app)/problems/[problemId]/layout.tsx index def7321..fe22fac 100644 --- a/src/app/(app)/problems/[problemId]/layout.tsx +++ b/src/app/(app)/problems/[problemId]/layout.tsx @@ -1,15 +1,12 @@ import { notFound } from "next/navigation"; import { ProblemHeader } from "@/features/problems/components/header"; -interface ProblemLayoutProps { +interface LayoutProps { children: React.ReactNode; params: Promise<{ problemId: string }>; } -export default async function ProblemLayout({ - children, - params, -}: ProblemLayoutProps) { +const Layout = async ({ children, params }: LayoutProps) => { const { problemId } = await params; if (!problemId) { @@ -24,4 +21,6 @@ export default async function ProblemLayout({ ); -} +}; + +export default Layout; diff --git a/src/app/(app)/problems/[problemId]/page.tsx b/src/app/(app)/problems/[problemId]/page.tsx index a0bb4e9..8ae3f1e 100644 --- a/src/app/(app)/problems/[problemId]/page.tsx +++ b/src/app/(app)/problems/[problemId]/page.tsx @@ -1,39 +1,17 @@ -import { TestcasePanel } from "@/features/problems/testcase/panel"; -import { BotPanel } from "@/features/problems/bot/components/panel"; -import { CodePanel } from "@/features/problems/code/components/panel"; -import { DetailPanel } from "@/features/problems/detail/components/panel"; -import { SolutionPanel } from "@/features/problems/solution/components/panel"; -import { SubmissionPanel } from "@/features/problems/submission/components/panel"; -import { DescriptionPanel } from "@/features/problems/description/components/panel"; -import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout"; +import { ProblemView } from "@/features/problems/ui/views/problem-view"; -interface ProblemPageProps { +interface PageProps { params: Promise<{ problemId: string }>; searchParams: Promise<{ submissionId: string | undefined; }>; } -export default async function ProblemPage({ - params, - searchParams, -}: ProblemPageProps) { +const Page = async ({ params, searchParams }: PageProps) => { const { problemId } = await params; const { submissionId } = await searchParams; - const components: Record = { - description: , - solution: , - submission: , - detail: , - code: , - testcase: , - bot: , - }; + return ; +}; - return ( -
- -
- ); -} +export default Page; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx new file mode 100644 index 0000000..fb46c63 --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; +import { ProblemEditLayout } from "@/features/admin/ui/layouts/problem-edit-layout"; + +interface LayoutProps { + children: React.ReactNode; + params: Promise<{ problemId: string }>; +} + +const Layout = async ({ children, params }: LayoutProps) => { + const { problemId } = await params; + + if (!problemId) { + return notFound(); + } + + return {children}; +}; + +export default Layout; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx new file mode 100644 index 0000000..8f8302f --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx @@ -0,0 +1,13 @@ +import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view"; + +interface PageProps { + params: Promise<{ problemId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { problemId } = await params; + + return ; +}; + +export default Page; diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx new file mode 100644 index 0000000..8a017f9 --- /dev/null +++ b/src/app/(protected)/layout.tsx @@ -0,0 +1,11 @@ +import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout"; + +interface LayoutProps { + children: React.ReactNode; +} + +const Layout = ({ children }: LayoutProps) => { + return {children}; +}; + +export default Layout; diff --git a/src/components/content/pre-detail.tsx b/src/components/content/pre-detail.tsx index 2293ce7..c535fa8 100644 --- a/src/components/content/pre-detail.tsx +++ b/src/components/content/pre-detail.tsx @@ -1,13 +1,13 @@ "use client"; import { cn } from "@/lib/utils"; +import { Actions } from "flexlayout-react"; import { Button } from "@/components/ui/button"; import { ReactNode, useRef, useState } from "react"; import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { useProblemEditorStore } from "@/stores/problem-editor"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; -import { Actions } from "flexlayout-react"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; interface PreDetailProps { children?: ReactNode; diff --git a/src/features/admin/ui/components/problem-edit-flexlayout.tsx b/src/features/admin/ui/components/problem-edit-flexlayout.tsx new file mode 100644 index 0000000..90a45d9 --- /dev/null +++ b/src/features/admin/ui/components/problem-edit-flexlayout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useProblemEditFlexLayoutStore } from "@/stores/flexlayout"; +import { FlexLayout } from "@/features/problems/components/flexlayout"; + +interface ProblemEditFlexLayoutProps { + components: Record; +} + +export const ProblemEditFlexLayout = ({ + components, +}: ProblemEditFlexLayoutProps) => { + const { hasHydrated, model, jsonModel, setModel, setJsonModel } = + useProblemEditFlexLayoutStore(); + + return ( + + ); +}; diff --git a/src/features/admin/ui/layouts/admin-protected-layout.tsx b/src/features/admin/ui/layouts/admin-protected-layout.tsx new file mode 100644 index 0000000..5af0b59 --- /dev/null +++ b/src/features/admin/ui/layouts/admin-protected-layout.tsx @@ -0,0 +1,30 @@ +import prisma from "@/lib/prisma"; +import { auth, signIn } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +interface AdminProtectedLayoutProps { + children: React.ReactNode; +} + +export const AdminProtectedLayout = async ({ + children, +}: AdminProtectedLayoutProps) => { + const session = await auth(); + const userId = session?.user?.id; + if (!userId) { + await signIn(); + } + + const user = await prisma.user.findUnique({ + select: { + role: true, + }, + where: { + id: userId, + }, + }); + + if (user?.role !== "ADMIN") redirect("/unauthorized"); + + return <>{children}; +}; diff --git a/src/features/admin/ui/layouts/problem-edit-layout.tsx b/src/features/admin/ui/layouts/problem-edit-layout.tsx new file mode 100644 index 0000000..a1857cb --- /dev/null +++ b/src/features/admin/ui/layouts/problem-edit-layout.tsx @@ -0,0 +1,29 @@ +import { Suspense } from "react"; +import { BackButton } from "@/components/back-button"; +import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar"; + +interface ProblemEditLayoutProps { + children: React.ReactNode; +} + +export const ProblemEditLayout = ({ children }: ProblemEditLayoutProps) => { + return ( +
+
+
+
+ +
+
+ }> + + +
+
+
+
+ {children} +
+
+ ); +}; diff --git a/src/features/admin/ui/views/problem-edit-view.tsx b/src/features/admin/ui/views/problem-edit-view.tsx new file mode 100644 index 0000000..add476f --- /dev/null +++ b/src/features/admin/ui/views/problem-edit-view.tsx @@ -0,0 +1,26 @@ +import EditCodePanel from "@/components/creater/edit-code-panel"; +import EditDetailPanel from "@/components/creater/edit-detail-panel"; +import EditSolutionPanel from "@/components/creater/edit-solution-panel"; +import EditTestcasePanel from "@/components/creater/edit-testcase-panel"; +import EditDescriptionPanel from "@/components/creater/edit-description-panel"; +import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout"; + +interface ProblemEditViewProps { + problemId: string; +} + +export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => { + const components: Record = { + description: , + solution: , + detail: , + code: , + testcase: , + }; + + return ( +
+ +
+ ); +}; diff --git a/src/features/problems/bot/components/view-bot-button.tsx b/src/features/problems/bot/components/view-bot-button.tsx index e2abb06..e4ae598 100644 --- a/src/features/problems/bot/components/view-bot-button.tsx +++ b/src/features/problems/bot/components/view-bot-button.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Toggle } from "@/components/ui/toggle"; import { Actions, DockLocation } from "flexlayout-react"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; export const ViewBotButton = () => { const t = useTranslations(); diff --git a/src/features/problems/components/flexlayout.tsx b/src/features/problems/components/flexlayout.tsx new file mode 100644 index 0000000..2996a6a --- /dev/null +++ b/src/features/problems/components/flexlayout.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { + BotIcon, + CircleCheckBigIcon, + FileTextIcon, + FlaskConicalIcon, + SquareCheckIcon, + SquarePenIcon, +} from "lucide-react"; +import { + IJsonModel, + ITabRenderValues, + Layout, + Model, + TabNode, +} from "flexlayout-react"; +import "@/styles/flexlayout.css"; +import { useTranslations } from "next-intl"; +import { useCallback, useEffect } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface FlexLayoutProps { + components: Record; + hasHydrated: boolean; + model: Model | null; + jsonModel: IJsonModel; + setModel: (model: Model) => void; + setJsonModel: (jsonModel: IJsonModel) => void; +} + +export const FlexLayout = ({ + components, + hasHydrated, + model, + jsonModel, + setModel, + setJsonModel, +}: FlexLayoutProps) => { + const t = useTranslations("ProblemPage"); + + useEffect(() => { + if (hasHydrated && !model) { + const model = Model.fromJson(jsonModel); + setModel(model); + } + }, [hasHydrated, jsonModel, model, setModel]); + + const onModelChange = useCallback( + (model: Model) => { + const jsonModel = model.toJson(); + setJsonModel(jsonModel); + }, + [setJsonModel] + ); + + const factory = useCallback( + (node: TabNode) => { + const component = node.getComponent(); + return component ? components[component] : null; + }, + [components] + ); + + const onRenderTab = useCallback( + (node: TabNode, renderValues: ITabRenderValues) => { + const Icon = getIconForTab(node.getId()); + renderValues.leading = Icon ? ( +