diff --git a/apps/tutorial/src/components/RouteLine.tsx b/apps/tutorial/src/components/RouteLine.tsx index f1f6313..5e3efef 100644 --- a/apps/tutorial/src/components/RouteLine.tsx +++ b/apps/tutorial/src/components/RouteLine.tsx @@ -1,7 +1,10 @@ /** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族) + * 卡片联动:hovered 节点随卡浮起 -3px——站点 translateY 平滑跟随, + * 连线以 CSS d: path() 过渡重算(与卡片同样 150ms,不支持的浏览器瞬时跟随) * 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边 * (圆心骑左右描边,各半在卡上);线层 z-10 置于卡之上,站点不被卡遮挡; * 同口多线自动聚合胶囊(线止于胶囊边界)。 */ +import type React from 'react' import type { ChapterId, Edge } from '../lib/types' const STATION_COLORS = ['var(--color-clash2a)', 'var(--color-clash1b)', 'var(--color-clash3a)', 'var(--color-clash1a)'] @@ -23,18 +26,25 @@ function stepPath(x0: number, y0: number, x1: number, y1: number, r = 8): string return `M ${x0} ${y0} H ${cornerX} A ${r} ${r} 0 0 ${sweep1} ${cornerX + r} ${y0 + (down ? r : -r)} V ${y1 - (down ? r : -r)} A ${r} ${r} 0 0 ${sweep2} ${cornerX + 2 * r} ${y1} H ${x1}` } +const HOVER_LIFT = -3 // 与 MemphisCard hover:-translate-y-[3px] 一致 + export function RouteLine({ edges, nodes, width, height, + hovered = null, }: { edges: Edge[] nodes: Partial> width: number height: number + /** 正在浮起的卡(章 id)——线与站点跟随 */ + hovered?: ChapterId | null }) { const stationColor = (i: number) => STATION_COLORS[i % STATION_COLORS.length] + /** 节点跟随位移:hovered 卡 -3px,其余 0 */ + const dy = (id: ChapterId) => (id === hovered ? HOVER_LIFT : 0) const seqEdges = edges.filter((e) => e.kind === 'seq') const insByTo = new Map() @@ -55,10 +65,11 @@ export function RouteLine({ const a = nodes[e.from] const b = nodes[e.to] if (!a || !b) return null - const d = stepPath(a.right, a.y, b.borderLeft, b.y) - if (e.kind === 'rel') return - if (e.kind === 'family') return - return + const d = stepPath(a.right, a.y + dy(e.from), b.borderLeft, b.y + dy(e.to)) + const follow = { d: `path("${d}")`, transition: 'd 150ms ease' } as React.CSSProperties + if (e.kind === 'rel') return + if (e.kind === 'family') return + return })} {seqEdges.map((e) => { const a = nodes[e.from] @@ -69,12 +80,18 @@ export function RouteLine({ // 源/目标站点:圆心骑卡描边,各半在卡上;目标:单线=圆点,多线=胶囊(只画一次) return ( - - {capsule ? ( - - ) : ins.length === 1 ? ( - - ) : null} + + + + {(capsule || ins.length === 1) && ( + + {capsule ? ( + + ) : ( + + )} + + )} ) })} diff --git a/apps/tutorial/src/pages/index/App.tsx b/apps/tutorial/src/pages/index/App.tsx index 283584b..6f8dc56 100644 --- a/apps/tutorial/src/pages/index/App.tsx +++ b/apps/tutorial/src/pages/index/App.tsx @@ -1,4 +1,6 @@ -/** 课程书架——教程首页:入口画像 → 六章卡片 → 关系与进度 */ +/** 课程书架——教程首页:入口画像 → 六章卡片 → 关系与进度 + * 主线场景:hover 卡片时线与站点跟随浮起(状态提升到场景层) */ +import { useState } from 'react' import { Page, Section } from '../../lib/ui' import { CHAPTERS } from '../../lib/chapters' import { MemphisCard } from '../../components/MemphisCard' @@ -24,6 +26,7 @@ const GRID: { id: ChapterId; entry?: 1 | 2 | 3 }[] = [ ] export function App() { + const [hovered, setHovered] = useState(null) return ( -
+
-
-
-
+
setHovered(1)} + onMouseLeave={() => setHovered(null)} + > + +
+
setHovered(2)} + onMouseLeave={() => setHovered(null)} + > + +
+
setHovered(5)} + onMouseLeave={() => setHovered(null)} + > + +