组件 · COMPONENT
星图 StarMap
学习地图:一条折线穿过五个站点。走过的站墨色实心、实线相连;当前站朱色,外圈一道脉冲光晕;还没到的站白底灰边、虚线相连。
import { StarMapSvg } from './components/v4/StarMapSvg.jsx'
import { buildStarMap } from './domain/v4/mapGeometry.js'
const CATS = [{ ls: [['为什么是 Obsidian'], ['建库与初始设置'], ['界面与核心操作'], ['双链与反向链接'], ['图谱视图'], ['插件安装与安全'], ['Dataview'], ['Templater'], ['卡片盒方法论'], ['从收集到内化']] }]
const MAP = {
nodes: [
{ t: '认识与建库', ref: [[0, 0], [0, 1]] },
{ t: '连接想法', ref: [[0, 2], [0, 3]] },
{ t: '图谱与插件', ref: [[0, 4], [0, 5], [0, 6], [0, 7]] },
{ t: '笔记方法', ref: [[0, 8]] },
{ t: '内化输出', ref: [[0, 9]] },
],
pts: [[86, 292], [212, 148], [336, 262], [478, 128], [614, 268]],
}
const DONE = new Set(['0-0-0', '0-0-1', '0-0-2', '0-0-3', '0-0-4'])
export function Roadmap() {
const geo = buildStarMap(MAP, CATS, DONE, 0)
return (
<div style={{ width: '100%', maxWidth: 740, background: '#FFFFFF', borderRadius: 16, border: '1px solid rgba(0,0,0,.09)' }}>
<StarMapSvg geo={geo} onNodeClick={() => {}} />
</div>
)
}<div
style="
width: 100%;
max-width: 740px;
background: #ffffff;
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.09);
"
>
<svg viewBox="0 0 740 400" style="width: 100%; display: block">
<circle cx="573.2" cy="132.3" r="1.28" fill="#1D1D1B" opacity="0.11"></circle>
<circle cx="224.6" cy="185.3" r="1.97" fill="#1D1D1B" opacity="0.17"></circle>
<circle cx="364.5" cy="142.3" r="0.85" fill="#1D1D1B" opacity="0.23"></circle>
<circle cx="513.3" cy="330.1" r="0.99" fill="#1D1D1B" opacity="0.18"></circle>
<circle cx="237.7" cy="357.6" r="1.48" fill="#1D1D1B" opacity="0.18"></circle>
<circle cx="77.5" cy="296.6" r="1.56" fill="#1D1D1B" opacity="0.11"></circle>
<circle cx="331.6" cy="139.2" r="1.07" fill="#1D1D1B" opacity="0.23"></circle>
<circle cx="257.8" cy="295.7" r="1.85" fill="#1D1D1B" opacity="0.2"></circle>
<circle cx="546.2" cy="198.8" r="1.83" fill="#1D1D1B" opacity="0.22"></circle>
<circle cx="646.2" cy="154.6" r="1.91" fill="#1D1D1B" opacity="0.16"></circle>
<circle cx="495.3" cy="287.6" r="1.13" fill="#1D1D1B" opacity="0.12"></circle>
<circle cx="57.3" cy="123.3" r="1.48" fill="#1D1D1B" opacity="0.15"></circle>
<circle cx="510.2" cy="360.7" r="2.05" fill="#1D1D1B" opacity="0.26"></circle>
<circle cx="242" cy="60" r="1.39" fill="#1D1D1B" opacity="0.18"></circle>
<circle cx="333.3" cy="135.5" r="1.18" fill="#1D1D1B" opacity="0.26"></circle>
<circle cx="663.3" cy="279.9" r="1.62" fill="#1D1D1B" opacity="0.18"></circle>
<circle cx="662.7" cy="109.4" r="0.81" fill="#1D1D1B" opacity="0.27"></circle>
<circle cx="207.7" cy="309.3" r="1.76" fill="#1D1D1B" opacity="0.29"></circle>
<circle cx="215" cy="211.4" r="1.47" fill="#1D1D1B" opacity="0.28"></circle>
<circle cx="219.9" cy="98.9" r="1.09" fill="#1D1D1B" opacity="0.28"></circle>
<circle cx="223.7" cy="81.5" r="0.95" fill="#1D1D1B" opacity="0.28"></circle>
<circle cx="250.7" cy="16.2" r="1.02" fill="#1D1D1B" opacity="0.16"></circle>
<line
x1="86"
y1="292"
x2="212"
y2="148"
stroke="#1D1D1B"
stroke-width="1.6"
stroke-dasharray="0"
></line>
<line
x1="212"
y1="148"
x2="336"
y2="262"
stroke="#1D1D1B"
stroke-width="1.6"
stroke-dasharray="0"
></line>
<line
x1="336"
y1="262"
x2="478"
y2="128"
stroke="rgba(0,0,0,.17)"
stroke-width="1.2"
stroke-dasharray="3 5"
></line>
<line
x1="478"
y1="128"
x2="614"
y2="268"
stroke="rgba(0,0,0,.17)"
stroke-width="1.2"
stroke-dasharray="3 5"
></line>
<g class="zzm-node" style="cursor: pointer">
<circle cx="86" cy="292" r="26" fill="transparent"></circle>
<circle cx="86" cy="292" r="12" fill="#1D1D1B" stroke="#1D1D1B" stroke-width="1.5"></circle>
<text
x="86"
y="266"
text-anchor="middle"
font-size="9.5"
fill="#B0B0AA"
font-family="var(--zzm-mono)"
style="user-select: none"
>
第 1 站
</text>
<text
x="86"
y="295.5"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="#FFFFFF"
style="user-select: none"
>
✓
</text>
<text
x="86"
y="323"
text-anchor="middle"
font-size="12.5"
font-weight="500"
fill="#6E6E6A"
style="user-select: none"
>
认识与建库
</text>
<text
x="86"
y="340"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="var(--zzm-accent,#A6402F)"
style="user-select: none"
>
✓ 为什么是 Obsid…
</text>
<text
x="86"
y="354"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="var(--zzm-accent,#A6402F)"
style="user-select: none"
>
✓ 建库与初始设置
</text>
</g>
<g class="zzm-node" style="cursor: pointer">
<circle cx="212" cy="148" r="26" fill="transparent"></circle>
<circle cx="212" cy="148" r="12" fill="#1D1D1B" stroke="#1D1D1B" stroke-width="1.5"></circle>
<text
x="212"
y="122"
text-anchor="middle"
font-size="9.5"
fill="#B0B0AA"
font-family="var(--zzm-mono)"
style="user-select: none"
>
第 2 站
</text>
<text
x="212"
y="151.5"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="#FFFFFF"
style="user-select: none"
>
✓
</text>
<text
x="212"
y="179"
text-anchor="middle"
font-size="12.5"
font-weight="500"
fill="#6E6E6A"
style="user-select: none"
>
连接想法
</text>
<text
x="212"
y="196"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="var(--zzm-accent,#A6402F)"
style="user-select: none"
>
✓ 界面与核心操作
</text>
<text
x="212"
y="210"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="var(--zzm-accent,#A6402F)"
style="user-select: none"
>
✓ 双链与反向链接
</text>
</g>
<g class="zzm-node" style="cursor: pointer">
<circle cx="336" cy="262" r="26" fill="transparent"></circle>
<circle
cx="336"
cy="262"
r="19"
fill="none"
stroke="var(--zzm-accent,#A6402F)"
stroke-width="1.5"
style="animation: zzmPulse 2.6s ease-in-out infinite"
></circle>
<circle
cx="336"
cy="262"
r="12"
fill="var(--zzm-accent,#A6402F)"
stroke="var(--zzm-accent,#A6402F)"
stroke-width="1.5"
></circle>
<text
x="336"
y="236"
text-anchor="middle"
font-size="9.5"
fill="#B0B0AA"
font-family="var(--zzm-mono)"
style="user-select: none"
>
第 3 站
</text>
<text
x="336"
y="265.5"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="#FFFFFF"
style="user-select: none"
>
3
</text>
<text
x="336"
y="293"
text-anchor="middle"
font-size="12.5"
font-weight="600"
fill="#1D1D1B"
style="user-select: none"
>
图谱与插件
</text>
<text
x="336"
y="310"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="var(--zzm-accent,#A6402F)"
style="user-select: none"
>
✓ 图谱视图
</text>
<text
x="336"
y="324"
text-anchor="middle"
font-size="10.5"
font-weight="400"
fill="#8E8E88"
style="user-select: none"
>
插件安装与安全
</text>
<text
x="336"
y="338"
text-anchor="middle"
font-size="10.5"
font-weight="400"
fill="#8E8E88"
style="user-select: none"
>
Dataview
</text>
<text
x="336"
y="352"
text-anchor="middle"
font-size="10.5"
font-weight="400"
fill="#B0B0AA"
style="user-select: none"
>
… 共 4 节
</text>
</g>
<g class="zzm-node" style="cursor: pointer">
<circle cx="478" cy="128" r="26" fill="transparent"></circle>
<circle cx="478" cy="128" r="11" fill="#FFFFFF" stroke="#C6C6C0" stroke-width="1.5"></circle>
<text
x="478"
y="102"
text-anchor="middle"
font-size="9.5"
fill="#B0B0AA"
font-family="var(--zzm-mono)"
style="user-select: none"
>
第 4 站
</text>
<text
x="478"
y="131.5"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="#71716C"
style="user-select: none"
>
4
</text>
<text
x="478"
y="159"
text-anchor="middle"
font-size="12.5"
font-weight="500"
fill="#6E6E6A"
style="user-select: none"
>
笔记方法
</text>
<text
x="478"
y="176"
text-anchor="middle"
font-size="10.5"
font-weight="400"
fill="#8E8E88"
style="user-select: none"
>
卡片盒方法论
</text>
</g>
<g class="zzm-node" style="cursor: pointer">
<circle cx="614" cy="268" r="26" fill="transparent"></circle>
<circle cx="614" cy="268" r="11" fill="#FFFFFF" stroke="#C6C6C0" stroke-width="1.5"></circle>
<text
x="614"
y="242"
text-anchor="middle"
font-size="9.5"
fill="#B0B0AA"
font-family="var(--zzm-mono)"
style="user-select: none"
>
第 5 站
</text>
<text
x="614"
y="271.5"
text-anchor="middle"
font-size="10.5"
font-weight="600"
fill="#71716C"
style="user-select: none"
>
5
</text>
<text
x="614"
y="299"
text-anchor="middle"
font-size="12.5"
font-weight="500"
fill="#6E6E6A"
style="user-select: none"
>
内化输出
</text>
<text
x="614"
y="316"
text-anchor="middle"
font-size="10.5"
font-weight="400"
fill="#8E8E88"
style="user-select: none"
>
从收集到内化
</text>
</g>
</svg>
</div>/* tokens.css · 用到的令牌 */
:root {
--zzm-mono: ui-monospace, "SF Mono", Menlo, monospace;
--zzm-accent: #A6402F;
}
/* base.css */
.zzm-v4 .zzm-node { transition: opacity .15s ease; }
.zzm-v4 .zzm-node:hover { opacity: .75; }几何从哪来
buildStarMap(map, cats, doneSet, courseIdx) 把地图数据变成绘图几何:22 颗装饰背景星、站间连线、每站前三节课的预览。map.pts 是 740 × 400 画布上的坐标,与 map.nodes 一一对应;doneSet 里的键是 课程-系列-课节。
| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| geo | buildStarMap(...) 的返回值 | — | { stars, edges, nodes, … } |
| onNodeClick | (index) => void | — | 点击站点 |
铁律
✓ 要朱色只给当前站:它就是「你在这里」
✗ 不要给已完成的站也上朱色
✓ 要脉冲光晕只挂在当前站(zzmPulse 2.6s)