Files
NextEdu/src/modules/textbooks/components/graph-kp-node.tsx
SpecialX f3c223d914 feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks
settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
2026-07-03 10:26:00 +08:00

105 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { memo } from "react"
import { Handle, Position, type NodeProps } from "@xyflow/react"
import { useTranslations } from "next-intl"
import { cn } from "@/shared/lib/utils"
import type { GraphNodeData, MasteryLevel, KpWithRelations } from "../types"
import { NODE_WIDTH } from "../graph-layout"
/** 根据掌握度计算色彩等级 */
function getMasteryLevel(mastery: number | null): MasteryLevel {
if (mastery === null) return "unassessed"
if (mastery < 60) return "low"
if (mastery < 85) return "medium"
return "high"
}
const MASTERY_COLORS: Record<MasteryLevel, string> = {
low: "border-red-500 bg-red-50 dark:bg-red-950/30",
medium: "border-yellow-500 bg-yellow-50 dark:bg-yellow-950/30",
high: "border-green-500 bg-green-50 dark:bg-green-950/30",
unassessed: "border-border bg-card",
}
const MASTERY_BAR_COLORS: Record<MasteryLevel, string> = {
low: "bg-red-500",
medium: "bg-yellow-500",
high: "bg-green-500",
unassessed: "bg-muted",
}
/**
* 类型守卫:从 React Flow node.dataRecord<string, unknown>)安全提取图谱节点数据。
* 替代 as unknown as 双重断言,提供运行时安全。
*/
function extractNodeData(data: Record<string, unknown>): {
kp: KpWithRelations
graphData?: GraphNodeData
} {
const kp = data.kp as KpWithRelations
const graphData = data.graphData as GraphNodeData | undefined
return { kp, graphData }
}
function GraphKpNodeComponent(props: NodeProps) {
const t = useTranslations("textbooks")
const { data, selected } = props
const { kp, graphData } = extractNodeData(data)
const mastery = graphData?.mastery ?? null
const masteryLevel = getMasteryLevel(mastery?.masteryLevel ?? null)
const showMastery = graphData?.viewMode === "student-mastery" || graphData?.viewMode === "class-mastery"
return (
<div
className={cn(
"rounded-lg border-2 px-3 py-2 shadow-sm transition-all",
MASTERY_COLORS[masteryLevel],
selected && "ring-2 ring-primary ring-offset-1",
graphData?.isHighlighted && "ring-2 ring-primary",
!graphData?.isHighlighted && graphData !== undefined && "opacity-40",
)}
style={{ width: NODE_WIDTH }}
>
<Handle type="target" position={Position.Top} className="opacity-0" />
<div className="flex items-start justify-between gap-2 mb-1">
<span className="text-xs font-medium line-clamp-2 flex-1">{kp.name}</span>
{/* 任意值 text-[10px]图谱节点空间受限text-xs(12px) 过大 */}
{kp.questionCount > 0 && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary shrink-0">
{kp.questionCount} {t("graph.node.questions")}
</span>
)}
</div>
{showMastery && (
<div className="mt-2">
<div className="flex items-center justify-between text-[10px] text-muted-foreground mb-1">
<span>{t("graph.node.mastery")}</span>
<span>
{mastery ? `${Math.round(mastery.masteryLevel)}%` : t("graph.detail.masteryNotAssessed")}
</span>
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div
className={cn("h-full transition-all", MASTERY_BAR_COLORS[masteryLevel])}
style={{ width: `${mastery?.masteryLevel ?? 0}%` }}
/>
</div>
</div>
)}
{kp.chapterTitle && (
<div className="mt-1 text-[10px] text-muted-foreground truncate">
{kp.chapterTitle}
</div>
)}
<Handle type="source" position={Position.Bottom} className="opacity-0" />
</div>
)
}
export const GraphKpNode = memo(GraphKpNodeComponent)