族谱树可视化(2/2):节点-连线树、视野控制与节点交互
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- 渲染分工:连线与代际轴在一个 SVG 里(坐标即内容层坐标,随内容层缩放);节点是绝对定位的真实 HTML 卡片——要真的可点击/可聚焦/可读屏,不能画成 SVG 矩形
- 交互:拖拽平移(pointer + touch-action:none)、滚轮缩放、放大/缩小按钮(0.35~1.8)、适应窗口、初次进入自动适配一次;hover/键盘聚焦高亮祖先路径 + 整棵子树(可勾选关闭子树),其余节点与连线降透明;当前作品常驻 aria-current 与「当前作品」标记
- 可达性:role=button + tabIndex=0,可访问名形如「第 1 代作品《标题》· 作者」;Enter/Space 等价点击;节点内 Fork 是独立 button 并 stopPropagation,不抢主点击;Fork 复用详情页同一条唤起 + 降级链路(openForkProtocol + FORK_LAUNCH_FALLBACK_DELAY_MS + 复制作品 ID 回退)
- 不可用父节点:布局补占位节点(只有 ID 与代际),标题即「原作品已不可用」,副行写「标题与作者名不对外展示」,不渲染作者名与 Fork/对比动作;可聚焦、不可点进详情(role 为空 + aria-disabled)
- 边界:截断提示、母版不可用提示、空树空态、加载/失败态照既有组件;超宽扇出与超深链由布局保证不重叠(单测钉住)
- 样式:画布 clamp + 点阵背景、连线活跃/降透明、prefers-reduced-motion 关闭过渡、窄屏可横向平移不溢出;删除上一版分组列表的死样式
- 测试:新增 5 条交互/可视化用例,另把两处旧可访问名查询改为 /《母版》/
This commit is contained in:
2026-10-06 14:09:46 +08:00
parent 7fbcf1f017
commit 1f85af8668
4 changed files with 850 additions and 182 deletions
@@ -723,6 +723,175 @@
color: var(--platform-accent-strong);
font-weight: 700;
}
/*
* 族谱树可视化:一个可平移/缩放的画布 + 内容层(CSS transform)。
* 连线在 SVG 里(坐标就是内容层坐标,随内容层一起缩放),节点是绝对定位的真实 HTML 卡片
* —— 因为节点要真的可点击 / 可聚焦 / 可读屏,不能画成 SVG 矩形。
*/
.game-lineage-viewport-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.game-lineage-toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--platform-text-soft);
font-size: 0.78rem;
}
.game-lineage-fork-notice {
color: var(--platform-text-soft);
font-size: 0.78rem;
overflow-wrap: anywhere;
}
.game-lineage-fork-copy {
border: 1px solid var(--platform-surface-border);
border-radius: 999px;
padding: 0.2rem 0.7rem;
background: transparent;
color: var(--platform-text-base);
font: inherit;
font-size: 0.74rem;
font-weight: 600;
cursor: pointer;
}
.game-lineage-canvas {
position: relative;
height: clamp(22rem, 62vh, 46rem);
max-width: 100%;
overflow: hidden;
border: 1px solid var(--platform-surface-border);
border-radius: 0.85rem;
background: radial-gradient(
circle at 1px 1px,
var(--platform-surface-border) 1px,
transparent 0
)
0 0 / 22px 22px;
cursor: grab;
/* 拖拽平移不要触发浏览器自身的滚动/缩放。 */
touch-action: none;
}
.game-lineage-canvas:active {
cursor: grabbing;
}
.game-lineage-canvas__content {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
transition: transform 180ms ease-out;
will-change: transform;
}
.game-lineage-canvas__edges {
position: absolute;
top: 0;
left: 0;
overflow: visible;
pointer-events: none;
}
.game-lineage-edge {
fill: none;
stroke: var(--platform-surface-border);
stroke-width: 2;
transition:
stroke 160ms ease,
opacity 160ms ease;
}
.game-lineage-edge--active {
stroke: var(--platform-accent-strong);
stroke-width: 2.5;
}
.game-lineage-edge--dimmed {
opacity: 0.18;
}
.game-lineage-axis {
position: absolute;
left: 0;
display: flex;
align-items: center;
gap: 0.3rem;
color: var(--platform-text-soft);
font-size: 0.74rem;
font-weight: 700;
white-space: nowrap;
}
.game-lineage-axis__count {
border-radius: 999px;
padding: 0.05rem 0.45rem;
background: var(--platform-surface-border);
color: var(--platform-text-soft);
font-size: 0.68rem;
font-weight: 600;
}
.game-lineage-node {
position: absolute;
box-sizing: border-box;
cursor: pointer;
transition:
opacity 160ms ease,
box-shadow 160ms ease;
}
.game-lineage-node--dimmed {
opacity: 0.34;
}
.game-lineage-node--stub {
border-style: dashed;
cursor: default;
}
.game-lineage-node:focus-visible {
outline: 2px solid var(--platform-accent-strong);
outline-offset: 2px;
}
.game-lineage-node__title {
color: var(--platform-text-strong);
font-size: 0.9rem;
font-weight: 700;
overflow-wrap: anywhere;
}
.game-lineage-node__actions {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.game-lineage-node__fork,
.game-lineage-node__compare {
border: 1px solid var(--platform-surface-border);
border-radius: 999px;
padding: 0.14rem 0.6rem;
background: transparent;
color: var(--platform-text-base);
font: inherit;
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
}
.game-lineage-node__fork:hover,
.game-lineage-node__compare:hover {
color: var(--platform-text-strong);
}
.game-lineage-zoom {
color: var(--platform-text-soft);
font-size: 0.76rem;
font-variant-numeric: tabular-nums;
}
/* 尊重「减少动效」偏好:只保留状态变化,不做过渡动画。 */
@media (prefers-reduced-motion: reduce) {
.game-lineage-canvas__content,
.game-lineage-node,
.game-lineage-edge {
transition: none;
}
}
/* 窄屏:节点尺寸适合触摸,画布可横向平移(内容层平移而不是页面溢出)。 */
@media (max-width: 720px) {
.game-lineage-canvas {
height: clamp(20rem, 56vh, 34rem);
}
}
.game-loading-panel {
display: grid;
min-height: 18rem;
@@ -1507,40 +1676,6 @@
font-size: 0.78rem;
cursor: pointer;
}
.game-lineage-tree {
display: flex;
flex-direction: column;
gap: 1rem;
}
.game-lineage-generation {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.game-lineage-generation__label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0;
color: var(--platform-text-strong);
font-size: 0.9rem;
font-weight: 700;
}
.game-lineage-generation__count {
border-radius: 999px;
padding: 0.05rem 0.5rem;
background: var(--platform-surface-border);
color: var(--platform-text-soft);
font-size: 0.72rem;
font-weight: 600;
}
.game-lineage-generation__nodes {
display: grid;
/* `min(13rem, 100%)`:桌面端列宽不变,视口窄于 13rem 时下限跟着收缩,
避免固定下限把内容撑出容器产生横向溢出。 */
grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
gap: 0.6rem;
}
.game-lineage-node {
display: flex;
flex-direction: column;