refactor: RouteLine hover 跟随封装为组件自带能力——自动监听场景内 a[data-comp=course-card](章 id 取自 href),消费方零接线;演示场景从样例页移入组件页(两 app 同步);E2E 契约随迁 components 页(站点断言改场景内定位),27/27 绿

This commit is contained in:
cqh963852 committed 2026-10-08 16:07:56 +08:00
1 parent 3c9aaf5faf
commit 061b16cf79
12 files changed
+842 -87

No files matched your search

@@ -1,9 +1,10 @@
/** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族)
* 卡片联动:hovered 节点随卡浮起 -3px——站点 translateY 平滑跟随,
* 连线以 CSS d: path() 过渡重算(与卡片同样 150ms,不支持的浏览器瞬时跟随)
* 卡片联动(组件自带,消费方零接线):自动监听场景内 a[data-comp="course-card"]
* 的 hover——该卡浮起时站点 translateY(-3) 平滑跟随、连线以 CSS d: path() 过渡重算(150ms)
* 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边
* (圆心骑左右描边,各半在卡上);线层 z-10 置于卡之上,站点不被卡遮挡;
* 同口多线自动聚合胶囊(线止于胶囊边界)。 */
import { useEffect, useRef, useState } from 'react'
import type React from 'react'
import type { ChapterId, Edge } from '../lib/types'
@@ -38,16 +39,40 @@ export function RouteLine({
nodes,
width,
height,
hovered = null,
follow = true,
}: {
edges: Edge[]
nodes: Partial<Record<ChapterId, LaneNode>>
width: number
height: number
/** 正在浮起的卡(章 id)——线与站点跟随 */
hovered?: ChapterId | null
/** 自动跟随场景内卡片的 hover 浮起(默认开;传 false 关闭) */
follow?: boolean
}) {
const stationColor = (i: number) => STATION_COLORS[i % STATION_COLORS.length]
// 联动状态:hover 中的章(由场景卡自动驱动,无需消费方传参)
const [hovered, setHovered] = useState<ChapterId | null>(null)
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!follow) return
const host = svgRef.current?.parentElement
if (!host) return
const offs = [...host.querySelectorAll<HTMLElement>('a[data-comp="course-card"]')]
.map((card) => {
const m = card.getAttribute('href')?.match(/ch(\d)/)
if (!m) return () => {}
const id = +m[1] as ChapterId
const on = () => setHovered(id)
const off = () => setHovered(null)
card.addEventListener('mouseenter', on)
card.addEventListener('mouseleave', off)
return () => {
card.removeEventListener('mouseenter', on)
card.removeEventListener('mouseleave', off)
}
})
return () => offs.forEach((off) => off())
}, [follow])
/** 节点跟随位移:hovered 卡 -3px,其余 0 */
const dy = (id: ChapterId) => (id === hovered ? HOVER_LIFT : 0)
@@ -60,6 +85,7 @@ export function RouteLine({
let si = 0
return (
<svg
ref={svgRef}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}