Skip to content

组件文档 - Component Documentation

本文档说明乙巳观(道由天观)中各组件的功能、API 和用法。整体架构(VitePress 单栈、数据驱动圆环、同心自动布局)见仓库根的 README.mdCLAUDE.md

📚 目录


数据层

类型继承体系

定义于 src/data/rings/types.ts。项目使用类型继承消除重复代码:

RingItemBase (所有 item 通用)
  ├─ RingItem (段导向分格)
  ├─ PointItem (点导向点)
  └─ BodyItem (天体导向)

RingDataBase (所有环通用)
  ├─ RingData (段导向环)
  ├─ PointRingData (点导向环)
  └─ BodyRingData (天体导向环)

基础公共字段RingItemBase):

字段类型说明
labelstring标签文字
colorstring?自定义颜色(缺省用环默认)
fontSizenumber?自定义字号
highlightboolean?高亮(兼容旧数据)
highlightLevel0 | 1 | 2 | 3?分级高亮:0无/1弱/2中/3强(优先于 highlight)

基础公共字段RingDataBase):

字段类型说明
startDegreenumber?默认起始度数偏移
radiusnumber?默认外半径
innerRadiusnumber?默认内半径
labelColorstring?标签默认颜色
fontSizenumber?统一字号(item.fontSize 优先)
circleColorstring?圆环边线颜色
circleWidthnumber?圆环边线宽度

RingData / RingItem(段导向圆环数据契约)

段导向:每个条目占据一个角度区间 [startAngle, endAngle]

RingItem(单个分格)

字段类型说明
startAnglenumber?自定义起始角度(缺省按 items 均分 360°)
endAnglenumber?自定义结束角度

RingData(一个段导向环)

字段类型说明
itemsRingItem[]该环的分格数据(必填)
labelPositionnumber?标签径向位置比例 (0-1)
tickWidthnumber?刻度线宽
tickColorstring?刻度线颜色
showSectorsboolean?是否显示扇形背景
verticalTwoCharboolean?双字标签是否竖排

示例(src/data/rings/twelveShichen.ts 节选)

typescript
import type { RingData } from './types'

export const twelveShichen: RingData = {
  startDegree: -90,
  radius: 280,
  innerRadius: 250,
  circleColor: '#888888',
  tickColor: '#666666',
  tickWidth: 0.8,
  items: [
    { label: '子', color: '#0D47A1', startAngle: 345, endAngle: 15 },
    { label: '丑', color: '#795548', startAngle: 15, endAngle: 45 },
    // …其余地支
  ]
}

PointRingData / PointItem(点导向圆环数据契约)

点导向:每个条目落在精确角度上(如二十四节气按黄经精确到度)。

PointItem(单个点)

字段类型说明
anglenumber?点的精确角度(缺省按 items 均分 360°)
pointSizenumber?点的大小(像素)
pointColorstring?点的颜色(独立于标签)
pointSymbol'circle' | 'diamond' | 'tick'?点的符号形状

PointRingData(一个点导向环)

字段类型说明
itemsPointItem[]该环的点数据(必填)
labelOffsetnumber?标签径向偏移:正数向外,负数向内(相对于点位置)
labelAngleOffsetnumber?标签角度偏移(度):避免与刻度线重叠
pointSizenumber?默认点大小
pointColorstring?默认点颜色
pointSymbol'circle' | 'diamond' | 'tick'?默认点符号

符号类型

符号用途说明
tick二十四节气、度数标记径向短线,从外圆向内占 25% 环厚
circle行星、标记实心圆在点半径上
diamond特殊点(二分二至)菱形在点半径上

示例(src/data/rings/twentyFourSolarTerms.ts 节选)

typescript
import type { PointRingData } from './types'

export const twentyFourSolarTerms: PointRingData = {
  radius: 460,
  innerRadius: 440,
  pointSymbol: 'tick',
  labelOffset: -15,
  labelAngleOffset: 2.5,
  items: [
    { label: '春分', angle: 0, pointColor: '#00ff88', highlightLevel: 3 },
    { label: '清明', angle: 15, pointColor: '#88ddaa' },
    // …其余节气
  ]
}

BodyRingData / BodyItem(天体导向圆环数据契约)NEW!

天体导向:每个条目是一个在精确角度上的发光天体(太阳、月亮、五星、恒星等),带光晕、逆行标记、黄纬偏移等特殊状态。

BodyState(天体特殊状态)

字段类型说明
retrogradeboolean?是否逆行(触发逆行虚线环)
latitudenumber?黄纬(度),用于径向偏移
aspect'conjunction' | 'opposition'?相位事件:合 / 冲
mansion{ label: string; degree: number }?入宿信息
conjunctionKind'inferior' | 'superior'?上下合类型(仅内行星)

BodyItem(单个天体)

继承自 RingItemBase,所有基础字段(labelcolorfontSizehighlightLevel)均可用。

字段类型说明
anglenumber天体精确角度(度,必填)
kindBodyKind天体类型:'sun' | 'moon' | 'mercury' | 'venus' | 'mars' | 'jupiter' | 'saturn' | 'star'
haloLevel0 | 1 | 2 | 3?光晕层数(优先于 highlightLevel
stateBodyState?天体特殊状态
symbolstring单字符号(必填)
sizenumber?本体半径(px,默认 14)
symbolColorstring?符号颜色(默认白色)

BodyRingData(一个天体导向环)

继承自 RingDataBase,所有基础字段均可用。

字段类型说明
itemsBodyItem[]该环的天体数据(1~N 个,必填)
defaultHalosHalo[]?默认光晕配置(外→内,每个 Halo = { radius, opacity }
latScalenumber?黄纬偏移缩放因子(像素/sin纬)
showLatLineboolean?是否显示黄纬偏移指示线
showRetrogradeRingboolean?是否显示逆行标记环
labelOffsetnumber?标签径向偏移:正数向外,负数向内

便捷构造函数(src/data/rings/sevenLuminaries.ts

函数用途
singlePlanetBody(key, angle, options)单行星研究盘(最常用)
twoPlanetsBody(key1, angle1, key2, angle2, options)双行星合冲对照
sevenLuminariesBody(angles, states)七曜全图盘
emptyBodyRing()空天体环(用于动态添加)

示例:单木星研究盘

typescript
import { singlePlanetBody } from '@/data/rings/sevenLuminaries'

// 木星在黄经 120 度,逆行,带 2.5 度黄纬偏移
const jupiterRing = singlePlanetBody('jupiter', 120, {
  retrograde: true,
  latitude: 2.5,
  highlightLevel: 3,
  mansion: { label: '井', degree: 12.5 }
})

现有数据文件:twentyFourSolarTermstwentyEightConstellationssixtyJiazisixtyJiaziNayinheavenlyStemstianganKongwangtwelveLongevityeightGatessiXiangtwelveShichensevenLuminariesjingFangSixtyGuajingFangEightPalaces,统一从 src/data/rings/index.ts 导出。


基础组件

PolarCanvas 极坐标画布组件

所有极坐标组件的基础画布,提供统一坐标系与动画。

特性

  • 标准极坐标系 — 0 度在正右方(3 点钟方向),顺时针增加
  • 统一动画管理 — 通过 useAnimation composable
  • 坐标转换工具 — 极坐标 ↔ 笛卡尔坐标
  • 路径生成 — 圆弧、扇形路径

Slot 工具函数

vue
<PolarCanvas>
  <template #default="slotProps">
    <!-- slotProps.centerX / centerY        圆心坐标 -->
    <!-- slotProps.totalRotation            总旋转角度 -->
    <!-- slotProps.polarToCartesian(angle, radius)        极坐标→笛卡尔 -->
    <!-- slotProps.getMidAngle(start, end)                角度中点 -->
    <!-- slotProps.generateArcPath(cx, cy, r, start, end) 圆弧/扇形路径 -->
  </template>
</PolarCanvas>

CircleRing 通用段导向圆环渲染器

真正负责绘制一个段导向圆环(扇形、刻度线、标签、高亮呼吸动画),构建于 PolarCanvas 之上。通常不直接使用,而是经由 DataRing 驱动。

Props(节选)

属性类型默认值说明
radiusnumber外半径(必填)
innerRadiusnumber0内半径(>0 为环形)
itemsRingItem[]分格数据
showLabelsbooleantrue显示标签文字
labelColorstring'white'标签默认色
labelPositionnumber0.7文字径向位置比例(0-1)
showTicksbooleantrue显示刻度线
tickWidth / tickColornumber / string0.5 / 'white'刻度线宽 / 色
showCirclebooleantrue显示圆环边线
circleWidth / circleColornumber / string1 / 'white'边线宽 / 色
showSectorsbooleanfalse显示扇形区域
rotationnumber0整体旋转角度
enableAnimationbooleanfalse自动旋转动画
animationSpeednumber0.5动画速度(度/帧,负数逆时针)
startDegreenumber0起始度数偏移
verticalTwoCharbooleanfalse双字标签竖排
rotationDirection'clockwise' | 'counterclockwise''clockwise'旋转方向

PointRing 通用点导向圆环渲染器

负责绘制一个点导向圆环(点标记、标签、高亮呼吸动画),构建于 PolarCanvas 之上。通常不直接使用,而是经由 DataPointRing 驱动。

点导向适用于:二十四节气(精确黄经点)二十八宿距星(精确赤经点)七曜(日月五星) 等本质是点而非区间的数据。

Props

属性类型默认值说明
radiusnumber点所在半径(必填)
innerRadiusnumber0内半径(用于画内边界圆,0=不画)
itemsPointItem[]点数据
showLabelsbooleantrue显示标签文字
labelColorstring'white'标签默认色
labelOffsetnumber15标签径向偏移:+向外 / -向内
labelAngleOffsetnumber0标签角度偏移(度),避免与刻度重叠
showPointsbooleantrue显示点标记
pointSizenumber4默认点大小
pointColorstring'#ffffff'默认点颜色
pointSymbol'circle' | 'diamond' | 'tick''circle'默认点符号
showCirclebooleantrue显示圆环边线
circleWidthnumber1圆环边线宽
circleColorstring'#888888'圆环边线颜色
rotationnumber0整体旋转角度
enableAnimationbooleanfalse自动旋转动画
animationSpeednumber0.5动画速度
startDegreenumber0起始度数偏移
rotationDirection'clockwise' | 'counterclockwise''clockwise'旋转方向

高亮

分级高亮对应不同呼吸动画强度:

  • highlightLevel >= 2 → 呼吸动画
  • highlightLevel >= 3 → 更快更强的呼吸动画

RingStack 同心圆环自动布局

解决「半径手动写死、叠加易重叠」的问题。声明 outerRadius 和每个环的径向厚度 thickness,容器从外向内自动累加分配 radius / innerRadius,并统一注入旋转方向。本组件输出一个 <g>,沿用项目约定由父级 <g transform> 定位。

支持段导向点导向两种圆环。

Props

属性类型默认值说明
outerRadiusnumber最外环的外缘半径(必填)
gapnumber2环间默认间隙
ringsRingConfig[]由外到内的环配置列表
rotationDirection'clockwise' | 'counterclockwise''clockwise'统一注入所有环

RingConfig

字段类型说明
componentComponent环组件(建议用 markRaw 包裹,避免响应式代理)
thicknessnumber该环径向厚度(外半径 - 内半径)
gapBeforenumber?与外侧相邻环的间隙,覆盖默认 gap0 表示紧贴)
propsRecord<string, unknown>?透传给该环的额外 props(如 datascaleInterval

使用示例

vue
<script setup lang="ts">
import { markRaw } from 'vue'
import RingStack from '@/components/base/RingStack.vue'
import DataRing from '@/components/rings/DataRing'
import DataPointRing from '@/components/rings/DataPointRing'
import DegreeScale from '@/components/rings/DegreeScale'
import { twentyFourSolarTerms, sixtyJiazi, sixtyJiaziNayin } from '@/data/rings'

const rings = [
  { component: markRaw(DegreeScale), thickness: 20, props: { scaleInterval: 6, showSectors: true } },
  { component: markRaw(DataPointRing), thickness: 20, props: { data: twentyFourSolarTerms } },
  { component: markRaw(DataRing), thickness: 30, props: { data: sixtyJiazi } },
  // 纳音紧贴六十甲子内侧
  { component: markRaw(DataRing), thickness: 26, gapBefore: 0, props: { data: sixtyJiaziNayin } },
]
</script>

<template>
  <svg width="1200" height="1200" viewBox="0 0 1200 1200">
    <g transform="translate(600, 600)">
      <RingStack :outer-radius="480" :gap="2" :rings="rings" rotation-direction="clockwise" />
    </g>
  </svg>
</template>

圆环组件

DataRing 数据驱动段圆环

接收一个 RingData,通过 CircleRing 渲染。取代了过去十余个近乎重复的传统圆环组件。布局参数由 RingStack 注入并覆盖数据中的默认值。

Props

属性类型默认值说明
dataRingData圆环数据(必填)
radiusnumber?data.radius ?? 200外半径(RingStack 注入)
innerRadiusnumber?data.innerRadius ?? 0内半径(RingStack 注入)
startDegreenumber?data.startDegree ?? 0起始度数
rotationDirection'clockwise' | 'counterclockwise''clockwise'旋转方向(RingStack 注入)
vue
<DataRing :data="twelveShichen" :radius="280" :inner-radius="252" />

DataPointRing 数据驱动点圆环

接收一个 PointRingData,通过 PointRing 渲染。使用 useRingBaseDataRing 共享基础逻辑,消除重复代码。

Props

属性类型默认值说明
dataPointRingData点圆环数据(必填)
radiusnumber?data.radius ?? 200外半径(RingStack 注入)
innerRadiusnumber?data.innerRadius ?? 0内半径(RingStack 注入)
startDegreenumber?data.startDegree ?? 0起始度数
rotationDirection'clockwise' | 'counterclockwise''clockwise'旋转方向(RingStack 注入)
vue
<DataPointRing :data="twentyFourSolarTerms" :radius="460" :inner-radius="440" />

DataBodyRing 数据驱动天体圆环 NEW!

接收一个 BodyRingData,通过复用 BodyMarker 渲染天体(光晕 + 本体 + 符号)。支持黄纬偏移、逆行标记、入宿标注等天文特性。与 DataRingDataPointRing 平级,可任意混用堆叠。

典型场景

  • 单行星深度研究盘(多层信息叠加)
  • 双行星合冲对照盘(实时追踪距离变化)
  • 五星聚可视化(分级高亮聚合度)
  • 七曜全图盘

Props

属性类型默认值说明
dataBodyRingData天体环数据(必填)
radiusnumber?200外半径(RingStack 注入)
innerRadiusnumber?140内半径(RingStack 注入)
rotationDirection'clockwise' | 'counterclockwise''clockwise'旋转方向(RingStack 注入)
bandOffsetnumber?0环带中线偏移(默认正中间,正值向外)

使用示例(单行星研究)

vue
<script setup lang="ts">
import { markRaw, computed } from 'vue'
import RingStack from '@/components/base/RingStack.vue'
import DataBodyRing from '@/components/rings/DataBodyRing.vue'
import { singlePlanetBody } from '@/data/rings/sevenLuminaries'

const controlledTime = ref(new Date())

// 单木星天体环(角度由天文计算而来)
const jupiterRing = computed(() => 
  singlePlanetBody('jupiter', jupiterLongitude.value, {
    retrograde: isJupiterRetrograde.value,
    latitude: jupiterLatitude.value,
    highlightLevel: 3
  })
)

const rings = [
  // 其他外环...
  { component: markRaw(DataBodyRing), thickness: 60, props: { data: jupiterRing } }
]
</script>

使用示例(七曜全图)

vue
<script setup lang="ts">
import { sevenLuminariesBody } from '@/data/rings/sevenLuminaries'

const allPlanetsRing = computed(() => 
  sevenLuminariesBody(
    // 七曜各自行星的角度(黄经或赤经)
    {
      sun: sunLongitude.value,
      moon: moonLongitude.value,
      mercury: mercuryLongitude.value,
      venus: venusLongitude.value,
      mars: marsLongitude.value,
      jupiter: jupiterLongitude.value,
      saturn: saturnLongitude.value
    },
    // 行星状态(逆行、黄纬)
    {
      mercury: { retrograde: mercuryRetro.value, latitude: mercuryLat.value },
      // ...其他行星
    }
  )
)
</script>

DegreeScale 度数刻度环

scaleInterval 生成度数刻度的环组件(数据驱动,刻度由间隔计算而来),构建于 PolarCanvas

Props

属性类型默认值说明
radiusnumber外半径(必填)
innerRadiusnumber0内半径(>0 为环形)
scaleIntervalnumber5刻度间隔(度数,建议为 360 的约数)
startDegreenumber0起始度数偏移
rotationnumber0整体旋转角度
enableAnimationbooleanfalse自动旋转动画
animationSpeednumber0.5动画速度
showLabelsbooleantrue显示度数标签
labelColorstring'#ffffff'标签颜色
labelPositionnumber0.5文字径向位置比例
showCirclebooleantrue显示圆环边线
circleWidth / circleColornumber / string1 / '#ffffff'边线宽 / 色
showSectorsbooleantrue显示扇形区域
sectorColorstring'#ffffff'扇形填充色
sectorOpacitynumber0.1扇形透明度
vue
<!-- 六十甲子:6 度间隔(60 格) -->
<DegreeScale :radius="200" :scale-interval="6" />
<!-- 十二地支:12 度(30 格) / 二十四节气:15 度(24 格) -->
<DegreeScale :radius="200" :scale-interval="15" :label-position="0.8" />

常用刻度间隔参考

间隔刻度数体系
904四象
458八卦
3610十天干
3012十二地支 / 十二时辰
1524二十四节气
660六十甲子
572七十二候
1360最高精度

天文组件

天文位置计算集中在 utils/celestial.ts(基于 astronomy-engine),上层通过 SevenLuminariesRingSkyChartMoonPhaseRingBeidouCenter 等专用环 / 圆心组件消费。详见 Astronomy Engine 集成


控制面板 · Sidebar

罗盘的时间 / 缩放 / 平移 / 旋转控制由左侧嵌入式 Sidebar 承载(曾经的 Control.vue 已下线)。核心变化:

  • 单点挂载<CompassSidebar> 挂在 docs/.vitepress/theme/layouts/CompassLayout.vue 里,9 个罗盘 View 共享一份 Sidebar
  • 状态解耦:不再走 v-model 五联 —— View 只负责 provideCompassContext({ time, viewport, onUserTimeChange })
  • View 专属工具位:通过 <Teleport to="#sidebar-view-tools"> 把 View 自己的控件塞进 Sidebar 的「视图选项」区块
  • 折叠形态:展开态左侧 260px;折叠态整体 translateX(-100%),屏幕左中悬浮一个 40×80 把手可再打开
  • 返回入口:Sidebar 顶部统一提供「← 罗盘列表」链接,替代 9 个 View 各自复制的 .back-link

CompassSidebar 罗盘左侧嵌入式 Sidebar

位置src/components/sidebar/CompassSidebar.vue

结构:

┌─────────────────────┐
│ SidebarHeader       │  ← 罗盘列表 · 罗盘名 · 折叠按钮
├─────────────────────┤
│ 视图选项 [Teleport] │  ← 空则自动隐藏(CSS :has(:empty))
├─────────────────────┤
│ TimePanel           │  ← 时间信息 / 播放 / 步进 / 输入
├─────────────────────┤
│ ViewportPanel       │  ← 缩放 / 平移 / 旋转方向 / 旋转角度
└─────────────────────┘

组件树:

子组件职责
SidebarHeader.vue顶部品牌区:返回链接 + 当前罗盘中文名 + 折叠按钮
SidebarToggleHandle.vue折叠态的左中悬浮把手(position: fixed; top: 50%
SidebarSection.vue通用可折叠 section 外壳(标题 + 内容 + ▸/▾ 指示)
SidebarButton.vue侧栏按钮统一样式(取代旧 ControlButton
TimePanel.vue时间面板:三条时间线(公历/朝代/干支)+ 播放/步进/输入
ViewportPanel.vue视口面板:缩放滑条 + 平移四向 + 旋转方向切换 + 旋转角度输入

Props:无。Sidebar 通过 useCompassContext() 读取当前 View 注册的上下文,View 未挂载时时间/视口面板降级隐藏、返回入口仍可用。

View 侧的四行接入

typescript
import { useUrlTime } from '@/composables/useUrlTime'
import { useViewport } from '@/composables/useViewport'
import { provideCompassContext } from '@/composables/useCompassContext'

const { controlledTime, clearUrlTime } = useUrlTime()
const viewport = useViewport()
provideCompassContext({
  time: controlledTime,
  viewport,
  onUserTimeChange: () => { /* 用户改时间时退出 liveMode */ }
})

View 专属工具位(Teleport)

Sidebar 的「视图选项」区块本质是一个常驻的挂载点:

vue
<!-- CompassSidebar.vue —— 简化 -->
<div id="sidebar-view-tools" class="view-tools-slot"></div>

空态自动隐藏(无需 JS 观察):

css
.section--auto-hide:has(.view-tools-slot:empty) { display: none; }

View 侧投递(示例:SuzhouStellarMapView 的三档朝向切换):

vue
<template>
  <div class="container">
    <svg ...>...</svg>

    <Teleport to="#sidebar-view-tools">
      <div class="orientation-toggle">
        <button v-for="opt in orientations" @click="orient = opt.key">
          {{ opt.label }}
        </button>
      </div>
    </Teleport>
  </div>
</template>

优点

  • 无侵入 —— Sidebar 不需要知道每个 View 有什么工具
  • 生命周期跟随 View —— View 卸载则 Teleport 内容自动清空 → CSS 自动隐藏 section
  • 样式作用域独立 —— 每个 View 用自己的 scoped CSS,不污染 Sidebar

可复用 Composable

useTimeController 受控时间控制器

位置src/composables/useTimeController.ts

替代旧 useTimePlayback。核心区别:

  • ✅ 接收外部 time: Ref<Date> 作为唯一真理源,内部不再有 currentTime 副本
  • ✅ 播放采用 wall-clock 增量差分 + visibilitychange 重置 —— 修复"切 tab 后瞬跳几十天"的 bug
  • ✅ 单帧 dt 上限 100ms —— 抑制 tab 冻结/长掉帧导致的瞬跳
typescript
import { useUrlTime } from '@/composables/useUrlTime'
import { useTimeController } from '@/composables/useTimeController'

const { controlledTime, clearUrlTime } = useUrlTime()
const time = useTimeController(controlledTime, {
  onUserChange: () => clearUrlTime()  // 用户任何主动操作 → 通知
})
// time.isPlaying / playSpeed / togglePlayPause / stepTime / stepMonth / stepYear / applyTime ...

useViewport 视口状态

位置src/composables/useViewport.ts

zoom / offsetX / offsetY / rotationDirection / rotationAngle 五个 ref 折叠成一个对象,同时暴露所有变换动作。

typescript
const viewport = useViewport()
// 模板:
// <g :transform="`translate(${600 + viewport.offsetX} ${600 + viewport.offsetY})
//                 scale(${viewport.zoom})
//                 rotate(${viewport.rotationDirection === 'clockwise'
//                   ? viewport.rotationAngle
//                   : -viewport.rotationAngle})`" />

viewport.zoomIn()
viewport.moveLeft()
viewport.toggleRotationDirection()
viewport.rotateLeft()  // -90°
viewport.resetZoom()
viewport.resetOffset()
viewport.resetRotationAngle()

边界:zoom clamp 在 [0.1, 3];rotationAngle 归一到 [0, 360)

useTimeShortcuts / useViewportShortcuts 快捷键

位置src/composables/useTimeShortcuts.ts / useViewportShortcuts.ts

分离时间和视口两组快捷键。共享 shouldIgnoreShortcut(e) 屏蔽输入框 / contenteditable / IME 组合中的按键。

快捷键功能
Space播放 / 暂停
R重置到当前时间
Y / ⇧Y+1 / -1 年
M / ⇧M+1 / -1 月
D / ⇧D+1 / -1 天
H / ⇧H+1 / -1 小时
N / ⇧N+1 / -1 分
S / ⇧S+1 / -1 秒
+ / - / 0放大 / 缩小 / 重置缩放
5/6/7/8/9缩放到 50%/75%/100%/125%/150%
方向键平移视图
Delete / Backspace重置平移
C切换旋转方向
Q / E左转 90° / 右转 90°
W重置旋转角度
typescript
useTimeShortcuts({
  togglePlayPause: time.togglePlayPause,
  resetToNow: time.resetToNow,
  stepYear: time.stepYear,
  stepMonth: time.stepMonth,
  stepTime: time.stepTime
})

useViewportShortcuts({
  zoomIn: viewport.zoomIn,
  zoomOut: viewport.zoomOut,
  resetZoom: viewport.resetZoom,
  setZoom: viewport.setZoom,
  moveUp: viewport.moveUp,
  moveDown: viewport.moveDown,
  moveLeft: viewport.moveLeft,
  moveRight: viewport.moveRight,
  resetOffset: viewport.resetOffset,
  toggleRotationDirection: viewport.toggleRotationDirection,
  rotateLeft: viewport.rotateLeft,
  rotateRight: viewport.rotateRight,
  resetRotationAngle: viewport.resetRotationAngle
})

useAltDragPan Alt + 拖拽平移 / 滚轮缩放

位置src/composables/useAltDragPan.ts

  • Alt + 鼠标左键拖拽 → 平移画布(更新 offsetX / offsetY
  • Alt + 滚轮 → 缩放(滚上放大,滚下缩小,Ctrl 无关,避免与浏览器缩放冲突)

屏幕像素通过 SVG preserveAspectRatio="xMidYMid meet" 的短边比例换算为 viewBox 单位,与当前 zoom 独立。

typescript
import { useAltDragPan } from '@/composables/useAltDragPan'

const svgRef = ref<SVGSVGElement | null>(null)
const viewport = useViewport()
const { isDragging, isAltPressed } = useAltDragPan({ svgRef, viewport })

isAltPressed 可用于切换 cursor 视觉反馈(grab / grabbing)。

useCompassContext 跨 Layout/View 状态桥

位置src/composables/useCompassContext.ts

因为 <CompassSidebar>(在 Layout)与 View(在 <Content />)是兄弟节点,provide/inject 传不过去。方案是模块级 shallowRef 单例。

typescript
// View 侧
provideCompassContext({ time: controlledTime, viewport, onUserTimeChange })

// Sidebar 侧
const ctx = useCompassContext()  // Ref<CompassContext | null>
// ctx.value?.time.value, ctx.value?.viewport.zoom.value ...

为什么安全:罗盘页任意时刻只有一个 View 存活,切页面时 onUnmounted 清空。

useSidebarLayout 侧栏折叠状态

位置src/composables/useSidebarLayout.ts

管理 Sidebar 的展开/折叠 + 内部各 section 的折叠状态,全部持久化到 localStorageyisiguan:sidebar:*,schema v2)。

typescript
const { expanded, toggleExpanded, expand, collapse,
        collapsed, toggleSection } = useSidebarLayout({
  sectionKeys: ['view-tools', 'time', 'playback', 'step', 'input', 'viewport', 'rotation']
})
  • Esc 键收起侧栏(焦点在输入元素时不触发)
  • 客户端 hydration 完成后再读 localStorage,避免 SSR 与首屏不一致

useUrlTime URL ↔ 受控时间双向绑定

位置src/composables/useUrlTime.ts

  • 首次挂载:URL ?t=YYYY-MM-DDTHH:mm 优先,其次 initialTime,最后 new Date()
  • 用户改时间:防抖 500ms 写回 URL(history.replaceState,不污染历史栈)
  • 精度到分钟:让每秒推进的实时时钟不会污染 URL
  • 支持古代日期(如 0665-01-15T12:00)—— 年份四位前导 0
typescript
const { controlledTime, hasUrlTime, clearUrlTime } = useUrlTime()

useLiveClock 1Hz 实时时钟

位置src/composables/useLiveClock.ts

LiushiJiaziView / PlanetMansionView 提供每秒推进的实时时钟。用户主动改时间(或 URL 携带 ?t=)时自动退出 liveMode。

useDayGridContext 日粒度年历上下文

位置src/composables/useDayGridContext.ts

奇门遁甲盘和五运六气盘共用的日粒度年历上下文。提供当日干支序号、24 节气定位、农历日期等信息,避免多个环组件重复计算。组件通过 useDayGridContext() 读取共享状态,无需通过 time prop 传入时间。

typescript
const ctx = useDayGridContext()
// ctx.jiaziIndex — 当日甲子序号(0-359)
// ctx.solarTermIndex — 当前节气索引
// ctx.lunarMonth — 农历月份

useTrueHeading 真北朝向计算

位置src/composables/useTrueHeading.ts

将手机传感器原始数据(alpha/beta/gamma)与磁偏角结合,计算真北朝向,并驱动盘面梯度平滑跟随手机旋转。从 FengShui24View 提取的领域逻辑。

typescript
const { trueHeading, displayHeading, directionLabel, betaDeviation } = useTrueHeading(
  phoneOrientation,
  magneticDeclination
)

useGuaRelationLayout 卦关系盘环配置

位置src/composables/useGuaRelationLayout.ts

GuaRelationView 提取的领域逻辑:环配置构建、全局模式悬停配对、聚焦模式焦点卦汇总等。分离视图的布局编排与业务计算。

typescript
const { rings, hoveredPair, focusedGuaLabel, focusSummary } = useGuaRelationLayout({
  layout: 'jingfang', // 或 'xiantian'
  mode: 'focus',
  selectedRelations: ['feifu', 'hu']
})

平台层

罗盘注册表

src/compasses/index.ts 是平台的单一注册表,仅承载元数据(不再挂载路由 / 懒加载组件)。它驱动 HomeView.vue 的卡片列表;具体的罗盘 View 通过 docs/.vitepress/theme/index.ts 全局注册。

CompassMeta

字段类型说明
idstring页面 slug,'liushi-jiazi'/O/compass/liushi-jiazi
namestring显示名(首页卡片标题)
descriptionstring首页卡片描述
categorystring?分类(天文 / 干支历 / 易学……),用于首页分组或筛选
typescript
// src/compasses/index.ts —— 纯元数据,无 component 字段
export const compasses: CompassMeta[] = [
  { id: 'liushi-jiazi',        name: '六十甲子六环',       description: '年月日时分秒六柱……',            category: '干支历' },
  { id: 'sixty-four-gua',      name: '先天六十四卦盘',     description: '伏羲/邵雍先天圆图……',           category: '易学' },
  { id: 'jingfang',            name: '京房六日七分纳甲盘',   description: '365 天刻度 + 60 卦六日七分……',  category: '易学' },
  { id: 'planet-mansion',      name: '七曜入宿天象盘',     description: '天极投影盖天图……',              category: '天文' },
  { id: 'tropical-year',       name: '回归年闰月盘',       description: '365 天回归年 vs 360 度甲子……', category: '天文' },
  { id: 'guan-dou',            name: '观斗盘',             description: '圆心真实北斗 + 紫微垣 + 地平圈……', category: '天文' },
  { id: 'gua-relation',        name: '卦关系盘',           description: '京房八宫 64 卦飞伏方向……',      category: '易学' },
  { id: 'suzhou-stellar-map',  name: '苏州石刻天文图',     description: '南宋 1247 年苏州府学石刻复原……', category: '天文' },
  { id: 'fengshui24',          name: '二十四山风水盘',     description: '手机端磁力计驱动的风水罗盘……', category: '风水' },
  { id: 'qi-men-dun-jia',      name: '阴阳遁九局盘',       description: '奇门遁甲阴阳遁九局体系可视化……', category: '术数' },
  { id: 'huangdi-neijing',     name: '黄帝内经·五运六气盘', description: '五运六气学说可视化……',           category: '术数' },
  { id: 'conjunction-cycles',  name: '会合周期盘',         description: '五星会合赤经序列连线……',        category: '天文' },
]

页面生成机制(VitePress-driven,无 Vue Router)

项目没有 SPA 入口或 vue-router。每个罗盘对应三个部分:

  1. src/compasses/index.ts 中的一项元数据(上表)

  2. src/views/XxxView.vue——Layer 2 View,持有 controlledTime 与 UI 状态

  3. docs/compass/xxx.md——极简 md:

    md
    ---
    layout: compass
    title: 六十甲子六环
    ---
    
    <ClientOnly>
      <LiushiJiaziView />
    </ClientOnly>

docs/.vitepress/theme/index.tsenhanceApp 里把所有 View + HomeView 全局注册;Layout 分派根据 frontmatter.layout === 'compass' 切换到 CompassLayout(全屏、隐藏 nav/sidebar/aside)。

URL 时间参数

罗盘页支持 ?t=YYYY-MM-DDTHH:MM 精确定位。因为不使用 vue-router,src/composables/useUrlTime.ts 使用纯 window.location + history.replaceState + popstate 事件实现 URL ↔ controlledTime 双向绑定。View 只需:

ts
const { controlledTime } = useUrlTime()

新增罗盘的完整步骤

  1. src/views/NewView.vue —— 用五层架构范式实现
  2. src/compasses/index.ts —— 追加一项元数据
  3. docs/compass/new.md —— 极简 md(frontmatter + <ClientOnly><NewView /></ClientOnly>
  4. docs/.vitepress/theme/index.ts —— import NewView from '@/views/NewView.vue' 并全局注册

新增的领域圆环组件(补录)

以下环随观斗盘 / 飞伏图盘 / 苏州石刻天文图三个罗盘一并加入。它们均遵循「时间驱动统一范式」(time?: MaybeRef<Date> + 内部 timeRef = computed(() => unref(props.time) ?? new Date()))。

组件类型领域
MonthEstablishRing.vueSegment斗建(月建):12 地支合月建,冬至→子月锚点
MonthGeneralRing.vueSegment月将(大吉/功曹…):太阳所在宫锚定
HourShichenRing.vueSegment12 时辰赤道环,当前时辰高亮
SunDiurnalRing.vueSegment日周:白昼-曙暮-夜三层弧背景,地轴倾斜驱动
guan-dou/SolarTermsRing.vuePoint观斗盘节气刻度(黄经→赤道映射)
conjunction-cycles/BranchZodiacRing.vueSegment十二地支宫格环(黄经/赤经对齐)

新增圆心组件:

组件领域
BeidouCenter.vue北斗七星(岁差修正)+ 紫微垣东西两藩 + 勾陈一 + 地平圈(浏览器定位)
SuzhouSkyMap.vue苏州石刻天文图圆心:拱极北斗 + 斗柄随本地恒星时旋转 + 自动标注所指之宿
conjunction-cycles/ConjunctionCanvas.vue会合周期连线画布(圆心区连线绘制)

新增工具函数(Layer 5):

工具职责
beidou.ts北斗七星赤经/赤纬(岁差修正)+ 斗柄指向
ziwei.ts紫微垣东西两藩恒星(勾陈一、天皇大帝等)
jianJiang.ts斗建 / 月将 / 太阳所在 宫位换算
jingFangYao.ts京房爻辞 + 飞伏查询
guaRelationArrows.ts京房八宫 64 卦飞伏方向
guaLayoutConstants.ts卦布局共享常量(GuaLayout + getGuaAngle + PURE_GUA_VALUES)
wuxing.ts五行相生相克 + 配色
conjunctions.ts五星会合周期二分搜索(astronomy-engine)
bodyRing.ts天体圆环纯函数(processBodyItems / getArrowParams 等)
guaDeriveChain.ts卦关系推衍链:从基准卦逐级派生目标卦序列

开发指南

新增一个段导向数据驱动圆环

  1. src/data/rings/myRing.ts 导出 RingData(必填 items,样式字段可选)。
  2. src/data/rings/index.ts 重新导出。
  3. 在视图的 RingStack 配置加一项:{ component: markRaw(DataRing), thickness: N, props: { data: myRing } }

新增一个点导向数据驱动圆环

  1. src/data/rings/myRing.ts 导出 PointRingData(必填 items,每个点指定 angle,样式字段可选)。
  2. src/data/rings/index.ts 重新导出。
  3. 在视图的 RingStack 配置加一项:{ component: markRaw(DataPointRing), thickness: N, props: { data: myRing } }

新增一个罗盘页面

  1. 新建 src/views/XxxView.vue,参考 LiushiJiaziView.vue / PlanetMansionView.vue
  2. src/compasses/index.tscompasses 数组追加一项。
  3. 首页卡片与路由自动生成。

性能建议

  1. computed 缓存昂贵计算(如 RingStack 的环数组、当前甲子序号)。
  2. 避免在模板里逐帧重建 rings 数组(见 LiushiJiaziViewcomputed)。
  3. 传入 RingStack 的组件用 markRaw 包裹,避免被响应式代理。
  4. 尽量减少 SVG DOM 节点;频繁切换用 v-show 而非 v-if

调试技巧

  1. 使用 Vue DevTools 检查组件状态。
  2. 注意角度单位(度数 vs 弧度)与坐标系方向(0° 在右、顺时针)。
  3. 天体不在预期位置时,先确认是否误对其包裹层施加了整体旋转。

本文档随项目更新维护,如有问题欢迎提交 Issue 或 PR。

道由天观 · 静观其变