From c11f33c6b72ba32b703e7a6e495084bdc286fab3 Mon Sep 17 00:00:00 2001 From: cqh963852 Date: Thu, 8 Oct 2026 13:23:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20hover=20=E5=8D=A1=E7=89=87=E6=97=B6?= =?UTF-8?q?=E7=BA=BF=E4=B8=8E=E7=AB=99=E7=82=B9=E8=B7=9F=E9=9A=8F=E6=B5=AE?= =?UTF-8?q?=E8=B5=B7=E2=80=94=E2=80=94RouteLine=20=E5=A2=9E=20hovered=20?= =?UTF-8?q?=E8=81=94=E5=8A=A8=EF=BC=88=E7=AB=99=E7=82=B9=20translateY(-3)?= =?UTF-8?q?=20=E5=B9=B3=E6=BB=91=E8=BF=87=E6=B8=A1=E3=80=81=E8=BF=9E?= =?UTF-8?q?=E7=BA=BF=20CSS=20d:=20path()=20=E9=87=8D=E7=AE=97=E5=90=8C=201?= =?UTF-8?q?50ms=EF=BC=89=EF=BC=8C=E4=B9=A6=E6=9E=B6=E5=9C=BA=E6=99=AF=20ho?= =?UTF-8?q?ver=20=E7=8A=B6=E6=80=81=E6=8F=90=E5=8D=87=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=EF=BC=9B=E5=8D=A1=E7=89=87=E5=89=B2=E8=A3=82=E6=84=9F=E6=B6=88?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tutorial/src/components/RouteLine.tsx | 37 ++++++++++++++++------ apps/tutorial/src/pages/index/App.tsx | 32 ++++++++++++++++--- 2 files changed, 54 insertions(+), 15 deletions(-) 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)} + > + +