- 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
122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import { useTranslations } from "next-intl"
|
|
import { Search, RotateCcw, Loader2 } from "lucide-react"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/components/ui/select"
|
|
import type { GraphLayoutMode, GraphViewMode } from "../types"
|
|
|
|
interface GraphToolbarProps {
|
|
viewMode: GraphViewMode
|
|
onViewModeChange: (mode: GraphViewMode) => void
|
|
availableViewModes: GraphViewMode[]
|
|
layoutMode: GraphLayoutMode
|
|
onLayoutModeChange: (mode: GraphLayoutMode) => void
|
|
searchText: string
|
|
onSearchChange: (text: string) => void
|
|
onResetView: () => void
|
|
/** 切换模式刷新中(显示轻量指示器,保留旧数据) */
|
|
isRefreshing?: boolean
|
|
}
|
|
|
|
const ALL_VIEW_MODES: readonly GraphViewMode[] = [
|
|
"structure",
|
|
"student-mastery",
|
|
"class-mastery",
|
|
]
|
|
|
|
const ALL_LAYOUT_MODES: readonly GraphLayoutMode[] = ["hierarchical", "force"]
|
|
|
|
function isGraphViewMode(value: string): value is GraphViewMode {
|
|
return ALL_VIEW_MODES.some((mode) => mode === value)
|
|
}
|
|
|
|
function isGraphLayoutMode(value: string): value is GraphLayoutMode {
|
|
return ALL_LAYOUT_MODES.some((mode) => mode === value)
|
|
}
|
|
|
|
export function GraphToolbar({
|
|
viewMode,
|
|
onViewModeChange,
|
|
availableViewModes,
|
|
layoutMode,
|
|
onLayoutModeChange,
|
|
searchText,
|
|
onSearchChange,
|
|
onResetView,
|
|
isRefreshing,
|
|
}: GraphToolbarProps) {
|
|
const t = useTranslations("textbooks")
|
|
|
|
const handleViewModeChange = (v: string): void => {
|
|
if (isGraphViewMode(v)) {
|
|
onViewModeChange(v)
|
|
}
|
|
}
|
|
|
|
const handleLayoutModeChange = (v: string): void => {
|
|
if (isGraphLayoutMode(v)) {
|
|
onLayoutModeChange(v)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-2 p-2 border-b bg-background/95 shrink-0">
|
|
{/* 布局切换:分层有向图 / Obsidian 风格力导向图 */}
|
|
<Select value={layoutMode} onValueChange={handleLayoutModeChange}>
|
|
<SelectTrigger className="w-[120px] h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="hierarchical">{t("graph.layout.hierarchical")}</SelectItem>
|
|
<SelectItem value="force">{t("graph.layout.force")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={viewMode} onValueChange={handleViewModeChange}>
|
|
<SelectTrigger className="w-[140px] h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableViewModes.includes("structure") && (
|
|
<SelectItem value="structure">{t("graph.viewMode.structure")}</SelectItem>
|
|
)}
|
|
{availableViewModes.includes("student-mastery") && (
|
|
<SelectItem value="student-mastery">{t("graph.viewMode.studentMastery")}</SelectItem>
|
|
)}
|
|
{availableViewModes.includes("class-mastery") && (
|
|
<SelectItem value="class-mastery">{t("graph.viewMode.classMastery")}</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 任意值 min-w-[120px]:搜索框最小宽度,防止压缩过窄无法输入 */}
|
|
<div className="relative flex-1 min-w-[120px]">
|
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
|
<Input
|
|
value={searchText}
|
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
placeholder={t("graph.toolbar.search")}
|
|
className="h-8 pl-7 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<Button variant="outline" size="sm" className="h-8 px-2" onClick={onResetView}>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
<span className="text-xs">{t("graph.toolbar.resetView")}</span>
|
|
</Button>
|
|
|
|
{isRefreshing && (
|
|
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" aria-label={t("graph.toolbar.refreshing")} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|