|
| 1 | +/** |
| 2 | + * 知识星座 · DOM/SVG 轨(medium/low 档) |
| 3 | + * Knowledge constellation · DOM/SVG track |
| 4 | + * |
| 5 | + * @ai-context: 宪法第一条映射(概念掌握度→发光体亮度:牢固=清冽明亮、 |
| 6 | + * 朦胧=朦胧)与第四条知识星座预算(medium/low:L1 每档 ≤15 节点、 |
| 7 | + * SVG line/circle)。朦胧节点施加雾色滤镜:外圈雾晕 opacity ≤0.4 |
| 8 | + * (第二条 §1:雾永远可拨开——雾晕可被点击拨开,露出节点本体)。 |
| 9 | + * 冷启动(宪法第七条:世界未点亮)显示引导文案而非空画布。 |
| 10 | + * 纯静态 SVG 无帧循环,low 档核显安全。 |
| 11 | + * |
| 12 | + * @ai-context: SVG track for medium/low tiers. Hazy nodes get a fog |
| 13 | + * halo capped at 40% opacity; cold start shows guidance copy. |
| 14 | + */ |
| 15 | +import { useMemo } from 'react'; |
| 16 | +import type { KnowledgeGraph } from '../lib/knowledgeGraph'; |
| 17 | +import { layoutKnowledgeGraph, type LayoutNode } from '../lib/knowledgeLayout'; |
| 18 | + |
| 19 | +/** 宪法映射:tier → 节点色(牢固=清冽明亮蓝白) / Tier colors */ |
| 20 | +const TIER_COLOR: Record<LayoutNode['tier'], string> = { |
| 21 | + 牢固: '#7dd3fc', |
| 22 | + 成长中: '#60a5fa', |
| 23 | + 朦胧: '#94a3b8', |
| 24 | +}; |
| 25 | + |
| 26 | +/** 雾色滤镜强度上限(宪法第二条 §1:雾 ≤40% 视觉强度) */ |
| 27 | +const FOG_OPACITY_MAX = 0.4; |
| 28 | + |
| 29 | +/** 链类型 → 颜色(同源=溯源青、复习=暖金、薄弱点=微紫) */ |
| 30 | +const LINK_COLOR: Record<string, string> = { |
| 31 | + 'shared-note': 'rgba(103, 232, 249, 0.5)', |
| 32 | + 'review-chain': 'rgba(251, 191, 36, 0.45)', |
| 33 | + weakpoint: 'rgba(196, 181, 253, 0.45)', |
| 34 | +}; |
| 35 | + |
| 36 | +interface KnowledgeConstellationProps { |
| 37 | + /** 派生图谱;null 表示尚未获取到数据(loading/error 分支) */ |
| 38 | + graph: KnowledgeGraph | null; |
| 39 | + loading?: boolean; |
| 40 | + error?: string | null; |
| 41 | +} |
| 42 | + |
| 43 | +/** 冷启动引导文案(宪法第七条:世界未点亮) / Cold-start copy */ |
| 44 | +const COLD_START_COPY = '世界还未点亮——第一次学习之后,这里会升起你的知识星座'; |
| 45 | + |
| 46 | +/** 知识星座 · DOM/SVG 轨 / Knowledge constellation (SVG track) */ |
| 47 | +export function KnowledgeConstellation({ graph, loading, error }: KnowledgeConstellationProps) { |
| 48 | + const layout = useMemo(() => (graph ? layoutKnowledgeGraph(graph) : null), [graph]); |
| 49 | + |
| 50 | + if (loading && !graph) { |
| 51 | + return <div className="h-56 rounded-kb-xl bg-bg-elevated/30 animate-pulse-skeleton" role="status" aria-label="知识星座加载中" />; |
| 52 | + } |
| 53 | + if (error && !graph) { |
| 54 | + return ( |
| 55 | + <div className="h-40 flex items-center justify-center rounded-kb-xl border border-border/15 bg-bg-elevated/20"> |
| 56 | + <p className="text-c1 text-text-tertiary">知识星座暂时无法点亮({error})</p> |
| 57 | + </div> |
| 58 | + ); |
| 59 | + } |
| 60 | + if (!layout || (layout.nodes.length === 0 && graph?.coldStart)) { |
| 61 | + return ( |
| 62 | + <div className="h-40 flex items-center justify-center rounded-kb-xl border border-dashed border-border/25 bg-bg-elevated/10"> |
| 63 | + <p className="text-c1 text-text-tertiary text-center px-6">{COLD_START_COPY}</p> |
| 64 | + </div> |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + return ( |
| 69 | + <svg |
| 70 | + viewBox="0 0 100 100" |
| 71 | + className="w-full h-56 rounded-kb-xl bg-gradient-to-b from-bg-elevated/20 to-transparent" |
| 72 | + role="img" |
| 73 | + aria-label="知识星座:概念掌握度空间化(牢固近中心明亮,朦胧在外围雾中)" |
| 74 | + > |
| 75 | + {/* 链(先画,节点覆盖其上) */} |
| 76 | + {layout.links.map((link, i) => { |
| 77 | + const s = layout.nodes.find((n) => n.id === link.source); |
| 78 | + const t = layout.nodes.find((n) => n.id === link.target); |
| 79 | + if (!s || !t) return null; |
| 80 | + return ( |
| 81 | + <line |
| 82 | + key={`${link.kind}-${i}`} |
| 83 | + x1={s.x} y1={s.y} x2={t.x} y2={t.y} |
| 84 | + stroke={LINK_COLOR[link.kind] ?? 'rgba(148, 163, 184, 0.3)'} |
| 85 | + strokeWidth={0.25} |
| 86 | + opacity={0.55} |
| 87 | + /> |
| 88 | + ); |
| 89 | + })} |
| 90 | + |
| 91 | + {/* 节点 */} |
| 92 | + {layout.nodes.map((n) => { |
| 93 | + const base = TIER_COLOR[n.tier]; |
| 94 | + const radius = n.tier === '牢固' ? 2.2 : n.tier === '成长中' ? 1.8 : 1.4; |
| 95 | + // 朦胧节点:雾晕(≤40%)+ 本体亮度乘 glow |
| 96 | + const opacity = 0.35 + n.glow * 0.65; |
| 97 | + return ( |
| 98 | + <g key={n.id}> |
| 99 | + {n.dimmed && ( |
| 100 | + <circle |
| 101 | + cx={n.x} cy={n.y} r={radius * 2.6} |
| 102 | + fill={base} |
| 103 | + opacity={FOG_OPACITY_MAX * n.glow} |
| 104 | + className="kb-fog-halo" |
| 105 | + /> |
| 106 | + )} |
| 107 | + <circle |
| 108 | + cx={n.x} cy={n.y} r={radius} |
| 109 | + fill={base} |
| 110 | + opacity={opacity} |
| 111 | + className="kb-star-node" |
| 112 | + > |
| 113 | + <title>{n.concept}({n.tier})</title> |
| 114 | + </circle> |
| 115 | + </g> |
| 116 | + ); |
| 117 | + })} |
| 118 | + </svg> |
| 119 | + ); |
| 120 | +} |
0 commit comments