---
title: 星图 StarMap
url: https://ui.zhaozimin.cn/components/star-map/
markdown: https://ui.zhaozimin.cn/components/star-map.md
group: 组件
origin: 设计系统源码（src/system），本仓库是唯一源头
source:
  - src/system/components/v4/StarMapSvg.jsx  # https://ui.zhaozimin.cn/src/system/components/v4/StarMapSvg.jsx
  - src/system/domain/v4/mapGeometry.js  # https://ui.zhaozimin.cn/src/system/domain/v4/mapGeometry.js
version: v0.2.0  # 设计系统版本，发布于 2026-09-23
scope: .zzm-v4   # 根节点必须带这个类，否则样式不生效
css: https://ui.zhaozimin.cn/v/0.2.0/zzm.css   # 锁版本地址，本仓库再改也不影响你
---

# 星图 StarMap

> 学习地图：一条折线穿过五个站点。走过的站墨色实心、实线相连；当前站朱色，外圈一道脉冲光晕；还没到的站白底灰边、虚线相连。

### 样张 · 5 站 · 已完成 2 站

React：

```jsx
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>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<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>
```

## 几何从哪来

`buildStarMap(map, cats, doneSet, courseIdx)` 把地图数据变成绘图几何：22 颗装饰背景星、站间连线、每站前三节课的预览。`map.pts` 是 740 × 400 画布上的坐标，与 `map.nodes` 一一对应；`doneSet` 里的键是 `课程-系列-课节`。

| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| `geo` | buildStarMap(...) 的返回值 | — | { stars, edges, nodes, … } |
| `onNodeClick` | (index) => void | — | 点击站点 |

## 铁律

- ✓ 要：朱色只给当前站：它就是「你在这里」
- ✗ 不要：给已完成的站也上朱色
- ✓ 要：脉冲光晕只挂在当前站（zzmPulse 2.6s）

## 本页用到的 CSS

按样张里出现的类名，从源文件原样裁出（完整版见 zzm.css）：

```css
/* 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; }
```

---

上一页：[神秘卡](https://ui.zhaozimin.cn/components/mystery-card.md) · 下一页：[侧栏导航](https://ui.zhaozimin.cn/patterns/sidebar.md) · 全站目录：https://ui.zhaozimin.cn/llms.txt
