feat(panel-layout): add scrollable content support with isScroll prop

- Add ScrollArea and ScrollBar components from ui/scroll-area
- Introduce optional isScroll prop (defaults to true) to control scrolling
- Maintain backward compatibility with existing usage
This commit is contained in:
cfngc4594 2025-06-21 12:14:02 +08:00
parent b52d96b645
commit 573007398e
14 changed files with 422 additions and 561 deletions

View File

@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CoreEditor } from "@/components/core-editor"; import { CoreEditor } from "@/components/core-editor";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { updateProblemTemplate } from "@/components/creater/problem-maintain"; import { updateProblemTemplate } from "@/components/creater/problem-maintain";
@ -70,50 +69,47 @@ export default function EditCodePanel({ problemId }: EditCodePanelProps) {
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <Card className="w-full rounded-none border-none bg-background">
<Card className="w-full rounded-none border-none bg-background"> <CardHeader className="px-6 py-4">
<CardHeader className="px-6 py-4"> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <span></span>
<span></span> <Button onClick={handleSave}></Button>
<Button onClick={handleSave}></Button> </div>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="language-select"></Label>
<select
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)}
>
{templates.map((t) => (
<option key={t.language} value={t.language}>
{t.language.toUpperCase()}
</option>
))}
</select>
</div> </div>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="language-select"></Label>
<select
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)}
>
{templates.map((t) => (
<option key={t.language} value={t.language}>
{t.language.toUpperCase()}
</option>
))}
</select>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="code-editor"></Label> <Label htmlFor="code-editor"></Label>
<div className="border rounded-md h-[500px]"> <div className="border rounded-md h-[500px]">
<CoreEditor <CoreEditor
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 || "" })
} }
/> />
</div>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </CardContent>
<ScrollBar orientation="horizontal" /> </Card>
</ScrollArea>
</PanelLayout> </PanelLayout>
); );
} }

View File

@ -16,7 +16,6 @@ import { CoreEditor } from "@/components/core-editor";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { VideoEmbed } from "@/components/content/video-embed"; import { VideoEmbed } from "@/components/content/video-embed";
import { getProblemLocales } from "@/app/actions/getProblemLocales"; import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -105,114 +104,111 @@ export default function EditDescriptionPanel({
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <Card className="w-full rounded-none border-none bg-background">
<Card className="w-full rounded-none border-none bg-background"> <CardHeader>
<CardHeader> <CardTitle></CardTitle>
<CardTitle></CardTitle> </CardHeader>
</CardHeader> <CardContent className="space-y-6">
<CardContent className="space-y-6"> {/* 语言切换 */}
{/* 语言切换 */} <div className="space-y-2">
<div className="space-y-2"> <Label></Label>
<Label></Label> <div className="flex space-x-2">
<div className="flex space-x-2"> <select
<select value={currentLocale}
value={currentLocale} onChange={(e) => setCurrentLocale(e.target.value)}
onChange={(e) => setCurrentLocale(e.target.value)} className="border rounded-md px-3 py-2"
className="border rounded-md px-3 py-2" >
> {locales.map((locale) => (
{locales.map((locale) => ( <option key={locale} value={locale}>
<option key={locale} value={locale}> {locale}
{locale} </option>
</option> ))}
))} </select>
</select>
<Input
placeholder="添加新语言"
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button onClick={handleAddCustomLocale}></Button>
</div>
</div>
{/* 标题输入 */}
<div className="space-y-2">
<Label htmlFor="description-title"></Label>
<Input <Input
id="description-title" placeholder="添加新语言"
value={description.title} value={customLocale}
onChange={(e) => onChange={(e) => setCustomLocale(e.target.value)}
setDescription({ ...description, title: e.target.value })
}
placeholder="输入题目标题"
/> />
<Button onClick={handleAddCustomLocale}></Button>
</div> </div>
</div>
{/* 编辑/预览切换 */} {/* 标题输入 */}
<div className="flex items-center justify-between"> <div className="space-y-2">
<div className="flex items-center space-x-2"> <Label htmlFor="description-title"></Label>
<Button <Input
type="button" id="description-title"
variant={viewMode === "edit" ? "default" : "outline"} value={description.title}
onClick={() => setViewMode("edit")} onChange={(e) =>
> setDescription({ ...description, title: e.target.value })
</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>
</div>
<div className="flex items-center">
<Button onClick={handleSave}></Button>
</div>
</div>
{/* 编辑/预览区域 */}
<div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
} }
> placeholder="输入题目标题"
{(viewMode === "edit" || viewMode === "compare") && ( />
<div className="relative h-[600px]"> </div>
<CoreEditor
value={description.content} {/* 编辑/预览切换 */}
onChange={(newVal) => <div className="flex items-center justify-between">
setDescription({ ...description, content: newVal || "" }) <div className="flex items-center space-x-2">
} <Button
language="markdown" type="button"
className="absolute inset-0 rounded-md border border-input" variant={viewMode === "edit" ? "default" : "outline"}
/> onClick={() => setViewMode("edit")}
</div> >
)}
{viewMode !== "edit" && ( </Button>
<div className="prose dark:prose-invert"> <Button
<MdxPreview type="button"
source={description.content} variant={viewMode === "preview" ? "default" : "outline"}
components={{ Accordion, VideoEmbed }} onClick={() =>
/> setViewMode(viewMode === "preview" ? "edit" : "preview")
</div> }
)} >
{viewMode === "preview" ? "取消" : "预览"}
</Button>
<Button
type="button"
variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode("compare")}
>
</Button>
</div> </div>
</CardContent> <div className="flex items-center">
</Card> <Button onClick={handleSave}></Button>
<ScrollBar orientation="horizontal" /> </div>
</ScrollArea> </div>
{/* 编辑/预览区域 */}
<div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]">
<CoreEditor
value={description.content}
onChange={(newVal) =>
setDescription({ ...description, content: newVal || "" })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
)}
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview
source={description.content}
components={{ Accordion, VideoEmbed }}
/>
</div>
)}
</div>
</CardContent>
</Card>
</PanelLayout> </PanelLayout>
); );
} }

View File

@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button";
import { Difficulty } from "@/generated/client"; import { Difficulty } from "@/generated/client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { updateProblemDetail } from "@/components/creater/problem-maintain"; import { updateProblemDetail } from "@/components/creater/problem-maintain";
@ -80,91 +79,88 @@ export default function EditDetailPanel({ problemId }: { problemId: string }) {
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <Card className="w-full rounded-none border-none bg-background">
<Card className="w-full rounded-none border-none bg-background"> <CardHeader className="px-6 py-4">
<CardHeader className="px-6 py-4"> <div className="flex items-center justify-between">
<span></span>
<Button type="button" onClick={handleSave}>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="display-id">ID</Label>
<Input
id="display-id"
type="number"
value={problemDetails.displayId}
onChange={(e) => handleNumberInputChange(e, "displayId")}
placeholder="输入显示ID"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty-select"></Label>
<select
id="difficulty-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={problemDetails.difficulty}
onChange={handleDifficultyChange}
>
<option value="EASY"></option>
<option value="MEDIUM"></option>
<option value="HARD"></option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="time-limit"> (ms)</Label>
<Input
id="time-limit"
type="number"
value={problemDetails.timeLimit}
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
placeholder="输入时间限制"
/>
</div>
<div className="space-y-2">
<Label htmlFor="memory-limit"> ()</Label>
<Input
id="memory-limit"
type="number"
value={problemDetails.memoryLimit}
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
placeholder="输入内存限制"
/>
</div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span></span> <div className="flex items-center space-x-2">
<Button type="button" onClick={handleSave}> <input
id="is-published"
</Button> type="checkbox"
</div> checked={problemDetails.isPublished}
</CardHeader> onChange={(e) =>
<CardContent> setProblemDetails({
<div className="space-y-6"> ...problemDetails,
<div className="grid grid-cols-2 gap-4"> isPublished: e.target.checked,
<div className="space-y-2"> })
<Label htmlFor="display-id">ID</Label> }
<Input 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"
id="display-id" />
type="number" <Label htmlFor="is-published" className="text-sm font-medium">
value={problemDetails.displayId}
onChange={(e) => handleNumberInputChange(e, "displayId")} </Label>
placeholder="输入显示ID"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty-select"></Label>
<select
id="difficulty-select"
className="block w-full p-2 border border-gray-300 rounded-md dark:bg-gray-800 dark:border-gray-700"
value={problemDetails.difficulty}
onChange={handleDifficultyChange}
>
<option value="EASY"></option>
<option value="MEDIUM"></option>
<option value="HARD"></option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="time-limit"> (ms)</Label>
<Input
id="time-limit"
type="number"
value={problemDetails.timeLimit}
onChange={(e) => handleNumberInputChange(e, "timeLimit")}
placeholder="输入时间限制"
/>
</div>
<div className="space-y-2">
<Label htmlFor="memory-limit"> ()</Label>
<Input
id="memory-limit"
type="number"
value={problemDetails.memoryLimit}
onChange={(e) => handleNumberInputChange(e, "memoryLimit")}
placeholder="输入内存限制"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<input
id="is-published"
type="checkbox"
checked={problemDetails.isPublished}
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 focus:ring-2"
/>
<Label htmlFor="is-published" className="text-sm font-medium">
</Label>
</div>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </CardContent>
<ScrollBar orientation="horizontal" /> </Card>
</ScrollArea>
</PanelLayout> </PanelLayout>
); );
} }

View File

@ -12,7 +12,6 @@ import { CoreEditor } from "@/components/core-editor";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { VideoEmbed } from "@/components/content/video-embed"; import { VideoEmbed } from "@/components/content/video-embed";
import { getProblemLocales } from "@/app/actions/getProblemLocales"; import { getProblemLocales } from "@/app/actions/getProblemLocales";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { updateProblemSolution } from "@/components/creater/problem-maintain"; import { updateProblemSolution } from "@/components/creater/problem-maintain";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -96,116 +95,113 @@ export default function EditSolutionPanel({
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <Card className="w-full rounded-none border-none bg-background">
<Card className="w-full rounded-none border-none bg-background"> <CardHeader>
<CardHeader> <CardTitle></CardTitle>
<CardTitle></CardTitle> </CardHeader>
</CardHeader> <CardContent className="space-y-6">
<CardContent className="space-y-6"> {/* 语言切换 */}
{/* 语言切换 */} <div className="space-y-2">
<div className="space-y-2"> <Label></Label>
<Label></Label>
<div className="flex space-x-2">
<select
value={currentLocale}
onChange={(e) => setCurrentLocale(e.target.value)}
className="border rounded-md px-3 py-2"
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
<Input
placeholder="添加新语言"
value={customLocale}
onChange={(e) => setCustomLocale(e.target.value)}
/>
<Button type="button" onClick={handleAddCustomLocale}>
</Button>
</div>
</div>
{/* 标题输入 (仅展示) */}
<div className="space-y-2">
<Label htmlFor="solution-title"></Label>
<Input
id="solution-title"
value={solution.title}
onChange={(e) =>
setSolution({ ...solution, title: e.target.value })
}
placeholder="输入题解标题"
disabled
/>
</div>
{/* 编辑/预览切换 */}
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <select
type="button" value={currentLocale}
variant={viewMode === "edit" ? "default" : "outline"} onChange={(e) => setCurrentLocale(e.target.value)}
onClick={() => setViewMode("edit")} className="border rounded-md px-3 py-2"
> >
{locales.map((locale) => (
</Button> <option key={locale} value={locale}>
<Button {locale}
type="button" </option>
variant={viewMode === "preview" ? "default" : "outline"} ))}
onClick={() => </select>
setViewMode(viewMode === "preview" ? "edit" : "preview") <Input
} placeholder="添加新语言"
> value={customLocale}
{viewMode === "preview" ? "取消" : "预览"} onChange={(e) => setCustomLocale(e.target.value)}
</Button> />
<Button <Button type="button" onClick={handleAddCustomLocale}>
type="button"
variant={viewMode === "compare" ? "default" : "outline"}
onClick={() => setViewMode("compare")}
>
</Button> </Button>
</div> </div>
</div>
{/* 编辑/预览区域 */} {/* 标题输入 (仅展示) */}
<div <div className="space-y-2">
className={ <Label htmlFor="solution-title"></Label>
viewMode === "compare" <Input
? "grid grid-cols-2 gap-6" id="solution-title"
: "flex flex-col gap-6" 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")
} }
> >
{(viewMode === "edit" || viewMode === "compare") && ( {viewMode === "preview" ? "取消" : "预览"}
<div className="relative h-[600px]">
<CoreEditor
value={solution.content}
onChange={(val) =>
setSolution({ ...solution, content: val || "" })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
)}
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview
source={solution.content}
components={{ Accordion, VideoEmbed }}
/>
</div>
)}
</div>
<Button type="button" onClick={handleSave}>
</Button> </Button>
</CardContent> <Button
</Card> type="button"
<ScrollBar orientation="horizontal" /> variant={viewMode === "compare" ? "default" : "outline"}
</ScrollArea> onClick={() => setViewMode("compare")}
>
</Button>
</div>
{/* 编辑/预览区域 */}
<div
className={
viewMode === "compare"
? "grid grid-cols-2 gap-6"
: "flex flex-col gap-6"
}
>
{(viewMode === "edit" || viewMode === "compare") && (
<div className="relative h-[600px]">
<CoreEditor
value={solution.content}
onChange={(val) =>
setSolution({ ...solution, content: val || "" })
}
language="markdown"
className="absolute inset-0 rounded-md border border-input"
/>
</div>
)}
{viewMode !== "edit" && (
<div className="prose dark:prose-invert">
<MdxPreview
source={solution.content}
components={{ Accordion, VideoEmbed }}
/>
</div>
)}
</div>
<Button type="button" onClick={handleSave}>
</Button>
</CardContent>
</Card>
</PanelLayout> </PanelLayout>
); );
} }

View File

@ -12,7 +12,6 @@ import { Button } from "@/components/ui/button";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { getProblemData } from "@/app/actions/getProblem"; import { getProblemData } from "@/app/actions/getProblem";
import { generateAITestcase } from "@/app/actions/ai-testcase"; import { generateAITestcase } from "@/app/actions/ai-testcase";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
@ -185,95 +184,85 @@ export default function EditTestcasePanel({
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <Card className="w-full rounded-none border-none bg-background">
<Card className="w-full rounded-none border-none bg-background"> <CardHeader className="px-6 py-4">
<CardHeader className="px-6 py-4"> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <span></span>
<span></span> <div className="flex items-center space-x-2">
<div className="flex items-center space-x-2"> <Button onClick={handleAITestcase} disabled={isGenerating}>
<Button onClick={handleAITestcase} disabled={isGenerating}> {isGenerating ? "生成中..." : "AI生成"}
{isGenerating ? "生成中..." : "AI生成"} </Button>
</Button> <Button onClick={handleAddTestcase}></Button>
<Button onClick={handleAddTestcase}></Button> <Button variant="secondary" onClick={handleSaveAll}>
<Button variant="secondary" onClick={handleSaveAll}>
</Button>
</div>
</div>
</CardHeader>
<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> </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> </div>
</CardHeader> ))}
<CardContent className="space-y-6"> </CardContent>
{testcases.map((tc, idx) => ( </Card>
<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 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>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</PanelLayout> </PanelLayout>
); );
} }

View File

@ -1,123 +1,14 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
CardFooter,
} from "@/components/ui/card";
import prisma from "@/lib/prisma";
import {
ChartDataPoint,
CodeAnalysisRadarChart,
} from "@/features/problems/analysis/components/radar-chart";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { PanelLayout } from "@/features/problems/layouts/panel-layout"; import { PanelLayout } from "@/features/problems/layouts/panel-layout";
import { AnalysisContent } from "@/features/problems/analysis/components/content";
export const description = "A server component to fetch code analysis data.";
interface AnalysisPanelProps { interface AnalysisPanelProps {
submissionId: string | undefined; submissionId: string | undefined;
} }
export const AnalysisPanel = async ({ submissionId }: AnalysisPanelProps) => { export const AnalysisPanel = ({ submissionId }: AnalysisPanelProps) => {
if (!submissionId) {
return (
<div className="p-4 text-center text-muted-foreground">
No submission ID provided.
</div>
);
}
const codeAnalysisData = await prisma.codeAnalysis.findUnique({
where: {
submissionId: submissionId,
},
});
if (!codeAnalysisData) {
return (
<div className="p-4 text-center text-muted-foreground">
No analysis data found for this submission.
</div>
);
}
// Transform the data into a format suitable for the RadarChart
const chartData: ChartDataPoint[] = [
{
kind: "overall",
score: codeAnalysisData.overallScore ?? 0,
fullMark: 100,
},
{
kind: "style",
score: codeAnalysisData.styleScore ?? 0,
fullMark: 100,
},
{
kind: "readability",
score: codeAnalysisData.readabilityScore ?? 0,
fullMark: 100,
},
{
kind: "efficiency",
score: codeAnalysisData.efficiencyScore ?? 0,
fullMark: 100,
},
{
kind: "correctness",
score: codeAnalysisData.correctnessScore ?? 0,
fullMark: 100,
},
];
return ( return (
<PanelLayout> <PanelLayout>
<ScrollArea className="h-full"> <AnalysisContent submissionId={submissionId} />
<Card className="w-full max-w-2xl mx-auto shadow-lg rounded-xl overflow-hidden border-0 bg-background/50 backdrop-blur-sm animate-fade-in">
<CardHeader className="items-center pb-2 space-y-1 px-6 pt-6">
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-primary to-foreground bg-clip-text text-transparent">
Code Analysis
</CardTitle>
<CardDescription className="text-muted-foreground">
Detailed evaluation of your code submission
</CardDescription>
</CardHeader>
<CardContent className="p-6">
<CodeAnalysisRadarChart chartData={chartData} />
</CardContent>
<CardFooter className="flex-col items-start gap-4 p-6 pt-0">
<div className="w-full space-y-3">
<div className="flex justify-between text-sm font-medium">
<span className="text-muted-foreground">Overall Score</span>
<span className="text-primary">
{codeAnalysisData.overallScore ?? "N/A"}
<span className="text-muted-foreground">/100</span>
</span>
</div>
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full bg-gradient-to-r from-primary to-purple-500 rounded-full transition-all duration-700 ease-out"
style={{
width: `${codeAnalysisData.overallScore ?? 0}%`,
transitionProperty: "width",
}}
/>
</div>
</div>
<div className="text-muted-foreground bg-muted/40 p-4 rounded-lg w-full border">
<h3 className="font-medium mb-2 text-foreground">Feedback</h3>
<p className="whitespace-pre-wrap leading-relaxed">
{codeAnalysisData.feedback}
</p>
</div>
</CardFooter>
</Card>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</PanelLayout> </PanelLayout>
); );
}; };

View File

@ -11,7 +11,7 @@ interface BotPanelProps {
export const BotPanel = ({ problemId }: BotPanelProps) => { export const BotPanel = ({ problemId }: BotPanelProps) => {
return ( return (
<PanelLayout> <PanelLayout isScroll={false}>
<Suspense fallback={<BotContentSkeleton />}> <Suspense fallback={<BotContentSkeleton />}>
<BotContent problemId={problemId} /> <BotContent problemId={problemId} />
</Suspense> </Suspense>

View File

@ -13,7 +13,7 @@ interface CodePanelProps {
export const CodePanel = ({ problemId }: CodePanelProps) => { export const CodePanel = ({ problemId }: CodePanelProps) => {
return ( return (
<PanelLayout> <PanelLayout isScroll={false}>
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<CodeToolbar className="border-b" /> <CodeToolbar className="border-b" />
<Suspense fallback={<CodeContentSkeleton />}> <Suspense fallback={<CodeContentSkeleton />}>

View File

@ -2,7 +2,6 @@ import prisma from "@/lib/prisma";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { MdxRenderer } from "@/components/content/mdx-renderer"; import { MdxRenderer } from "@/components/content/mdx-renderer";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import type { Locale, ProblemLocalization } from "@/generated/client"; import type { Locale, ProblemLocalization } from "@/generated/client";
const getLocalizedDescription = ( const getLocalizedDescription = (
@ -40,12 +39,7 @@ export const DescriptionContent = async ({
const description = getLocalizedDescription(descriptions, locale as Locale); const description = getLocalizedDescription(descriptions, locale as Locale);
return ( return <MdxRenderer source={description} className="p-4 md:p-6" />;
<ScrollArea className="h-full">
<MdxRenderer source={description} className="p-4 md:p-6" />
<ScrollBar orientation="horizontal" />
</ScrollArea>
);
}; };
export const DescriptionContentSkeleton = () => { export const DescriptionContentSkeleton = () => {

View File

@ -16,7 +16,7 @@ export const DetailPanel = ({ submissionId }: DetailPanelProps) => {
} }
return ( return (
<PanelLayout> <PanelLayout isScroll={false}>
<DetailHeader /> <DetailHeader />
<Suspense fallback={<DetailContentSkeleton />}> <Suspense fallback={<DetailContentSkeleton />}>
<DetailContent submissionId={submissionId} /> <DetailContent submissionId={submissionId} />

View File

@ -1,12 +1,27 @@
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
interface PanelLayoutProps { interface PanelLayoutProps {
isScroll?: boolean;
children: React.ReactNode; children: React.ReactNode;
} }
export const PanelLayout = ({ children }: PanelLayoutProps) => { export const PanelLayout = ({
isScroll = true,
children,
}: PanelLayoutProps) => {
return ( return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden"> <div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
<div className="relative flex-1"> <div className="relative flex-1">
<div className="absolute h-full w-full">{children}</div> <div className="absolute h-full w-full">
{isScroll ? (
<ScrollArea className="h-full">
{children}
<ScrollBar orientation="horizontal" />
</ScrollArea>
) : (
children
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -2,7 +2,6 @@ import prisma from "@/lib/prisma";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { MdxRenderer } from "@/components/content/mdx-renderer"; import { MdxRenderer } from "@/components/content/mdx-renderer";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import type { Locale, ProblemLocalization } from "@/generated/client"; import type { Locale, ProblemLocalization } from "@/generated/client";
const getLocalizedSolution = ( const getLocalizedSolution = (
@ -38,12 +37,7 @@ export const SolutionContent = async ({ problemId }: SolutionContentProps) => {
const solution = getLocalizedSolution(solutions, locale as Locale); const solution = getLocalizedSolution(solutions, locale as Locale);
return ( return <MdxRenderer source={solution} className="p-4 md:p-6" />;
<ScrollArea className="h-full">
<MdxRenderer source={solution} className="p-4 md:p-6" />
<ScrollBar orientation="horizontal" />
</ScrollArea>
);
}; };
export const SolutionContentSkeleton = () => { export const SolutionContentSkeleton = () => {

View File

@ -3,7 +3,6 @@ import { CodeXmlIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { SubmissionTable } from "@/features/problems/submission/components/table"; import { SubmissionTable } from "@/features/problems/submission/components/table";
interface SubmissionContentProps { interface SubmissionContentProps {
@ -45,11 +44,12 @@ export const SubmissionContent = async ({
const session = await auth(); const session = await auth();
const userId = session?.user?.id; const userId = session?.user?.id;
return ( return userId ? (
<ScrollArea className="h-full px-3"> <div className="px-3">
{userId ? <SubmissionTable problemId={problemId} /> : <LoginPromptCard />} <SubmissionTable problemId={problemId} />
<ScrollBar orientation="horizontal" /> </div>
</ScrollArea> ) : (
<LoginPromptCard />
); );
}; };

View File

@ -1,18 +1,12 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { TestcaseTable } from "@/features/problems/testcase/table"; import { TestcaseTable } from "@/features/problems/testcase/table";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
interface TestcaseContentProps { interface TestcaseContentProps {
problemId: string; problemId: string;
} }
export const TestcaseContent = ({ problemId }: TestcaseContentProps) => { export const TestcaseContent = ({ problemId }: TestcaseContentProps) => {
return ( return <TestcaseTable problemId={problemId} />;
<ScrollArea className="h-full">
<TestcaseTable problemId={problemId} />
<ScrollBar orientation="horizontal" />
</ScrollArea>
);
}; };
export const TestcaseContentSkeleton = () => { export const TestcaseContentSkeleton = () => {