feat(textbooks): add force-graph component and update graph components
- Add force-graph.tsx for new graph visualization - Update graph-toolbar, knowledge-graph, knowledge-point-list, textbook-reader - Update use-graph-data hook and types - Update textbooks error boundary pages
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/shared/components/ui/sheet"
|
||||
import { ResizablePanel } from "@/shared/components/ui/resizable-panel"
|
||||
import { useTextSelection } from "../hooks/use-text-selection"
|
||||
import { useKnowledgePointActions } from "../hooks/use-knowledge-point-actions"
|
||||
import { buildChapterIndex, highlightKnowledgePoints } from "../utils"
|
||||
@@ -326,14 +327,111 @@ export function TextbookReader({
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 h-full">
|
||||
{/* P2-4 桌面端侧栏:lg 及以上内联显示 */}
|
||||
<div className="hidden lg:flex lg:col-span-4 lg:border-r lg:pr-6 flex-col min-h-0">
|
||||
{sidebarContent}
|
||||
// 图谱 tab 时侧栏(图谱)占更大比例,其他 tab 时侧栏(目录/知识点)占较小比例
|
||||
const sidebarInitialPct = activeTab === "graph" ? 60 : 35
|
||||
|
||||
const contentPanel = (
|
||||
<div className="flex flex-col h-full min-h-0 relative">
|
||||
{/* 移动端侧栏触发按钮 + 为此课文备课按钮 */}
|
||||
<div className="flex items-center gap-2 mb-3 px-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMobileSidebarOpen(true)}
|
||||
className="lg:hidden"
|
||||
aria-expanded={mobileSidebarOpen}
|
||||
aria-controls="mobile-sidebar-sheet"
|
||||
>
|
||||
<Menu className="mr-2 h-4 w-4" />
|
||||
{t("reader.openSidebar")}
|
||||
</Button>
|
||||
{selected && (
|
||||
<span className="text-sm text-muted-foreground truncate hidden lg:inline">
|
||||
{selected.title}
|
||||
</span>
|
||||
)}
|
||||
{selected && canCreateLessonPlan && (
|
||||
<Button asChild variant="default" size="sm" className="ml-auto">
|
||||
<Link
|
||||
href={`/teacher/lesson-plans/new?textbookId=${encodeURIComponent(textbookId)}&chapterId=${encodeURIComponent(selected.id)}`}
|
||||
>
|
||||
<GraduationCap className="mr-2 h-4 w-4" />
|
||||
{t("reader.prepareLesson")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* P2-4 移动端侧栏:lg 以下用 Sheet 抽屉式展示 */}
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("dialog.knowledge.deleteTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t("dialog.knowledge.deleteDesc")}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("dialog.knowledge.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDeleteKnowledgePoint}>
|
||||
{t("dialog.knowledge.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<KnowledgePointDialogs
|
||||
createDialogOpen={createDialogOpen}
|
||||
setCreateDialogOpen={setCreateDialogOpen}
|
||||
selectedText={selectedText}
|
||||
isCreating={isCreating}
|
||||
onCreateKnowledgePoint={onCreateKnowledgePoint}
|
||||
editKpDialogOpen={editKpDialogOpen}
|
||||
setEditKpDialogOpen={setEditKpDialogOpen}
|
||||
editingKp={editingKp}
|
||||
isUpdatingKp={isUpdatingKp}
|
||||
onUpdateKnowledgePoint={handleUpdateKnowledgePoint}
|
||||
questionDialogOpen={questionDialogOpen}
|
||||
setQuestionDialogOpen={setQuestionDialogOpen}
|
||||
targetKpForQuestion={targetKpForQuestion}
|
||||
renderQuestionCreator={canCreateQuestion ? renderQuestionCreator : undefined}
|
||||
/>
|
||||
|
||||
<TextbookSectionErrorBoundary
|
||||
fallbackTitle={t("error.loadFailed")}
|
||||
fallbackDescription={t("error.loadFailedDesc")}
|
||||
retryLabel={t("error.retry")}
|
||||
>
|
||||
<TextbookContentPanel
|
||||
chapter={{ selected, processedContent }}
|
||||
editing={{
|
||||
isEditing,
|
||||
editContent,
|
||||
setEditContent,
|
||||
isSaving,
|
||||
startEditing,
|
||||
cancelEditing: () => setIsEditing(false),
|
||||
saveContent: handleSaveContent,
|
||||
}}
|
||||
selection={{
|
||||
contentRef,
|
||||
onPointerDown: handleContentPointerDown,
|
||||
onContextMenuChange: handleContextMenuChange,
|
||||
selectedText,
|
||||
setCreateDialogOpen,
|
||||
}}
|
||||
highlight={{
|
||||
highlightedKpId,
|
||||
onHighlight: setHighlightedKpId,
|
||||
onSwitchToKnowledgeTab: () => setActiveTab("knowledge"),
|
||||
}}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
</TextbookSectionErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full lg:h-full">
|
||||
{/* 移动端:单列布局 + Sheet 抽屉侧栏 */}
|
||||
<div className="lg:hidden flex-1 min-h-0">{contentPanel}</div>
|
||||
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
||||
{/* 任意值 w-[85vw]:移动端抽屉占视口宽度,max-w-sm 防止超宽屏过大 */}
|
||||
<SheetContent side="left" className="w-[85vw] max-w-sm p-0 flex flex-col">
|
||||
@@ -344,100 +442,16 @@ export function TextbookReader({
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div className="lg:col-span-8 flex flex-col min-h-0 relative">
|
||||
{/* P2-4 移动端侧栏触发按钮 + 为此课文备课按钮 */}
|
||||
<div className="flex items-center gap-2 mb-3 px-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMobileSidebarOpen(true)}
|
||||
className="lg:hidden"
|
||||
aria-expanded={mobileSidebarOpen}
|
||||
aria-controls="mobile-sidebar-sheet"
|
||||
>
|
||||
<Menu className="mr-2 h-4 w-4" />
|
||||
{t("reader.openSidebar")}
|
||||
</Button>
|
||||
{selected && (
|
||||
<span className="text-sm text-muted-foreground truncate hidden lg:inline">
|
||||
{selected.title}
|
||||
</span>
|
||||
)}
|
||||
{selected && canCreateLessonPlan && (
|
||||
<Button asChild variant="default" size="sm" className="ml-auto">
|
||||
<Link
|
||||
href={`/teacher/lesson-plans/new?textbookId=${encodeURIComponent(textbookId)}&chapterId=${encodeURIComponent(selected.id)}`}
|
||||
>
|
||||
<GraduationCap className="mr-2 h-4 w-4" />
|
||||
{t("reader.prepareLesson")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("dialog.knowledge.deleteTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t("dialog.knowledge.deleteDesc")}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("dialog.knowledge.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDeleteKnowledgePoint}>
|
||||
{t("dialog.knowledge.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<KnowledgePointDialogs
|
||||
createDialogOpen={createDialogOpen}
|
||||
setCreateDialogOpen={setCreateDialogOpen}
|
||||
selectedText={selectedText}
|
||||
isCreating={isCreating}
|
||||
onCreateKnowledgePoint={onCreateKnowledgePoint}
|
||||
editKpDialogOpen={editKpDialogOpen}
|
||||
setEditKpDialogOpen={setEditKpDialogOpen}
|
||||
editingKp={editingKp}
|
||||
isUpdatingKp={isUpdatingKp}
|
||||
onUpdateKnowledgePoint={handleUpdateKnowledgePoint}
|
||||
questionDialogOpen={questionDialogOpen}
|
||||
setQuestionDialogOpen={setQuestionDialogOpen}
|
||||
targetKpForQuestion={targetKpForQuestion}
|
||||
renderQuestionCreator={canCreateQuestion ? renderQuestionCreator : undefined}
|
||||
{/* 桌面端:可拖拽分栏,左侧侧栏 + 右侧正文,切换 tab 时重置比例 */}
|
||||
<div className="hidden lg:flex h-full min-h-0">
|
||||
<ResizablePanel
|
||||
key={activeTab}
|
||||
initialLeft={sidebarInitialPct}
|
||||
minLeft={20}
|
||||
minRight={25}
|
||||
left={<div className="h-full pr-3 border-r">{sidebarContent}</div>}
|
||||
right={<div className="h-full pl-3">{contentPanel}</div>}
|
||||
/>
|
||||
|
||||
<TextbookSectionErrorBoundary
|
||||
fallbackTitle={t("error.loadFailed")}
|
||||
fallbackDescription={t("error.loadFailedDesc")}
|
||||
retryLabel={t("error.retry")}
|
||||
>
|
||||
<TextbookContentPanel
|
||||
chapter={{ selected, processedContent }}
|
||||
editing={{
|
||||
isEditing,
|
||||
editContent,
|
||||
setEditContent,
|
||||
isSaving,
|
||||
startEditing,
|
||||
cancelEditing: () => setIsEditing(false),
|
||||
saveContent: handleSaveContent,
|
||||
}}
|
||||
selection={{
|
||||
contentRef,
|
||||
onPointerDown: handleContentPointerDown,
|
||||
onContextMenuChange: handleContextMenuChange,
|
||||
selectedText,
|
||||
setCreateDialogOpen,
|
||||
}}
|
||||
highlight={{
|
||||
highlightedKpId,
|
||||
onHighlight: setHighlightedKpId,
|
||||
onSwitchToKnowledgeTab: () => setActiveTab("knowledge"),
|
||||
}}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
</TextbookSectionErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user