族谱卡:封面 16:9 横版、轴统一主题色、控件入画布浮层、去掉第三个动作
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m8s
Project CI / Repository checks (pull_request) Failing after 42s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m21s
Project CI / Frontend tests (pull_request) Successful in 2m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 7m6s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m8s
Project CI / Repository checks (pull_request) Failing after 42s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m21s
Project CI / Frontend tests (pull_request) Successful in 2m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 7m6s
This commit is contained in:
@@ -760,9 +760,12 @@ async function runBrowserStep({
|
||||
// 复制反馈:点击后必须有**可读反馈**——要么按钮变「作品 ID 已复制」,
|
||||
// 要么出现「复制失败:请手动选中作品 ID 文本。」。
|
||||
//
|
||||
// 为什么断言两种都算过:headless Chromium 里 `navigator.clipboard.writeText` 要求文档聚焦,
|
||||
// 页面不在前台的运行环境会直接拒绝写入(脚本自己的注释也把「真实剪贴板内容」列为人工项)。
|
||||
// 这里要钉的是「UI 一定会给出可观察的结果」,而不是把浏览器焦点策略当成被测行为。
|
||||
// 为什么不用 Playwright 的真实鼠标 click():复验实测该自动化会话里**连页面标题这类对照元素
|
||||
// 的 document 级 pointerdown/mousedown/click 都恒为 0**(见下面 probe 的计数),说明这个会话
|
||||
// 的鼠标事件通道本身不产生事件,用它只会测出「环境不通」而不是「按钮不工作」。
|
||||
// 独立复验用同一页面 `btn.click()` 调用时整条链路完全正常(文案变化 + 状态行 + 剪贴板内容正确),
|
||||
// 因此这里改为**派发 DOM click**(与真实用户点击走同一个 React onClick handler),
|
||||
// 并显式断言剪贴板内容,避免为了迁就环境而放宽语义。产品代码未为此改动。
|
||||
const copyId = page.getByRole('button', {
|
||||
name: REMIX_COPY_ID_LABEL,
|
||||
exact: true,
|
||||
@@ -774,9 +777,26 @@ async function runBrowserStep({
|
||||
`label=${REMIX_COPY_ID_LABEL}`,
|
||||
);
|
||||
if (copyIdVisible) {
|
||||
// 先尽量把页面带到前台:多数环境里这一步能让成功后分支真的发生。
|
||||
// 诊断探针:记录 document 级鼠标事件计数,作为「本会话鼠标通道是否可用」的证据。
|
||||
const mouseProbe = await page
|
||||
.evaluate(() => {
|
||||
const counters = { pointerdown: 0, mousedown: 0, click: 0 };
|
||||
const bump = (key) => () => {
|
||||
counters[key] += 1;
|
||||
};
|
||||
document.addEventListener('pointerdown', bump('pointerdown'), true);
|
||||
document.addEventListener('mousedown', bump('mousedown'), true);
|
||||
document.addEventListener('click', bump('click'), true);
|
||||
window.__lineageMouseProbe = counters;
|
||||
return counters;
|
||||
})
|
||||
.catch(() => null);
|
||||
await page.bringToFront().catch(() => {});
|
||||
await copyId.click();
|
||||
await copyId.hover({ timeout: 5_000 }).catch(() => {});
|
||||
// 派发 DOM click:不依赖该会话的鼠标通道,但会真正触发 React 的 onClick。
|
||||
await copyId.evaluate((element) => {
|
||||
element.click();
|
||||
});
|
||||
const copiedShown = await visible(
|
||||
page.getByRole('button', {
|
||||
name: REMIX_COPIED_ID_LABEL,
|
||||
@@ -800,10 +820,33 @@ async function runBrowserStep({
|
||||
.locator('#game-fork-remix-panel .game-fork-remix-status')
|
||||
.evaluateAll((nodes) => nodes.map((node) => node.textContent))
|
||||
.catch(() => []);
|
||||
const clipboardText = await page
|
||||
.evaluate(async () => {
|
||||
try {
|
||||
return await navigator.clipboard.readText();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.catch(() => '');
|
||||
check(
|
||||
'浏览器视觉:点「复制作品 ID」后给出可读反馈(已复制按钮或复制失败提示)',
|
||||
copiedShown || failedShown,
|
||||
`期望按钮名=${REMIX_COPIED_ID_LABEL} 或出现失败提示;实测按钮文案=${JSON.stringify(observedLabels)} 面板状态行=${JSON.stringify(observedStatusTexts)}(已授剪贴板权限;真实剪贴板内容仍属人工验收项)`,
|
||||
`期望按钮名=${REMIX_COPIED_ID_LABEL} 或出现失败提示;实测按钮文案=${JSON.stringify(observedLabels)} 面板状态行=${JSON.stringify(observedStatusTexts)}`,
|
||||
);
|
||||
// 剪贴板内容:派发 click 后 handler 真正跑过,权限已授的情况下这里能读到作品 ID。
|
||||
check(
|
||||
'浏览器视觉:复制后剪贴板内容为该作品 ID',
|
||||
clipboardText.trim() === parentId,
|
||||
`clipboard=${JSON.stringify(clipboardText.trim())} 期望=${parentId}`,
|
||||
);
|
||||
const mouseProbeAfter = await page
|
||||
.evaluate(() => window.__lineageMouseProbe ?? null)
|
||||
.catch(() => null);
|
||||
check(
|
||||
'诊断:本会话鼠标事件通道计数(0 = 自动化上下文不产生鼠标事件,非产品缺陷)',
|
||||
true,
|
||||
`before=${JSON.stringify(mouseProbe)} after=${JSON.stringify(mouseProbeAfter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2213,9 +2213,7 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
);
|
||||
|
||||
const leafCard = await cardOf('叶节点');
|
||||
fireEvent.click(
|
||||
within(leafCard).getByRole('button', { name: '选中 叶节点 的路径' }),
|
||||
);
|
||||
fireEvent.click(leafCard);
|
||||
|
||||
// 面包屑:母版 → 中间代 → 叶节点,祖先移出视口也能读。
|
||||
const breadcrumb = screen.getByLabelText('已选中的路径');
|
||||
@@ -2238,9 +2236,7 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
document.querySelectorAll('path.game-lineage-edge--active').length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.click(
|
||||
within(leafCard).getByRole('button', { name: '取消选中 叶节点 的路径' }),
|
||||
);
|
||||
fireEvent.click(leafCard);
|
||||
expect(screen.queryByLabelText('已选中的路径')).toBeNull();
|
||||
expect((await cardOf('旁支')).className).not.toContain(
|
||||
'game-lineage-node--dimmed',
|
||||
@@ -2265,9 +2261,15 @@ describe('GameLineagePage 可视化与交互', () => {
|
||||
expect(
|
||||
within(leafCard).getByRole('button', { name: 'Fork 叶节点' }),
|
||||
).toBeTruthy();
|
||||
// 卡片上只保留「打开作品 / Fork」两个动作按钮(第三个次要动作已按复验意见移除),
|
||||
// 封面+标题本身也是一个打开详情的按钮。
|
||||
expect(within(leafCard).getByRole('button', { name: '打开作品' })).toBeTruthy();
|
||||
expect(
|
||||
within(leafCard).getByRole('button', { name: '选中 叶节点 的路径' }),
|
||||
within(leafCard).getByRole('button', { name: 'Fork 叶节点' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(leafCard).queryByRole('button', { name: /选中|锁定/u }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('缩放按钮改变百分比,「适应窗口」重新适配', async () => {
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
getGameLineage,
|
||||
} from '../../services/gameDistributionClient';
|
||||
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { useCopyFeedback } from '../common/useCopyFeedback';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
buildLineageTreeLayout,
|
||||
collectLineageHighlight,
|
||||
generationAxisLabel,
|
||||
LINEAGE_COVER_ASPECT,
|
||||
type LineageLayoutNode,
|
||||
} from './lineageTreeLayout';
|
||||
|
||||
@@ -177,7 +177,11 @@ function LineageTreeNodeCard({
|
||||
onOpenDetail(gameId);
|
||||
}}
|
||||
>
|
||||
<span className="game-lineage-node__cover">
|
||||
<span
|
||||
className="game-lineage-node__cover"
|
||||
// 16:9 横版(与发布面板封面预览同一约定):容器比例由常量给出,CSS 不再另写一套。
|
||||
style={{ aspectRatio: LINEAGE_COVER_ASPECT }}
|
||||
>
|
||||
<GameCover
|
||||
game={coverGame}
|
||||
objectKey={layoutNode.coverObjectKey}
|
||||
@@ -221,23 +225,6 @@ function LineageTreeNodeCard({
|
||||
>
|
||||
Fork
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="game-lineage-node__pin"
|
||||
aria-pressed={isPinned}
|
||||
aria-label={
|
||||
isPinned
|
||||
? `取消选中 ${displayTitle} 的路径`
|
||||
: `选中 ${displayTitle} 的路径`
|
||||
}
|
||||
onClick={(event) => {
|
||||
// 键盘可达的「选中路径」:点击版的同一件事。
|
||||
event.stopPropagation();
|
||||
onTogglePin(isPinned ? '' : gameId);
|
||||
}}
|
||||
>
|
||||
{isPinned ? '已选中' : '选中路径'}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -328,11 +315,12 @@ export function GameLineagePage({
|
||||
nodes,
|
||||
compactTree
|
||||
? {
|
||||
nodeWidth: 168,
|
||||
nodeHeight: 78,
|
||||
columnGap: 20,
|
||||
rowGap: 86,
|
||||
axisWidth: 62,
|
||||
// 紧凑态:卡片 176 宽 → 16:9 封面 99 高 + 信息条;度量仍满足「同层 ≥ columnGap」。
|
||||
nodeWidth: 176,
|
||||
nodeHeight: 148,
|
||||
columnGap: 22,
|
||||
rowGap: 88,
|
||||
axisWidth: 72,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
@@ -372,6 +360,10 @@ export function GameLineagePage({
|
||||
}, [layout.nodes, pathIds, pinnedGameId]);
|
||||
const anchorId = highlightGameId?.trim() || normalizedRootId;
|
||||
const derivedCount = nodes.filter((node) => node.generation > 0).length;
|
||||
const nodeCountLabel =
|
||||
layout.nodes.length > 0
|
||||
? `${layout.nodes.length} 个节点${lineage?.truncated ? '(已截断)' : ''}`
|
||||
: '';
|
||||
|
||||
/** 适应窗口:宽高**两个维度同时**满足(取更小的那个比例),并居中。 */
|
||||
const fitToView = useCallback(
|
||||
@@ -546,47 +538,11 @@ export function GameLineagePage({
|
||||
<span className="game-eyebrow">创作族谱</span>
|
||||
<h2>{lineage?.root?.title?.trim() || '创作族谱'}</h2>
|
||||
</div>
|
||||
<div className="game-toolbar__actions">
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => zoomBy(1 / 1.2)}
|
||||
aria-label="缩小"
|
||||
>
|
||||
<Minus aria-hidden="true" />
|
||||
</PlatformActionButton>
|
||||
<span className="game-lineage-zoom" role="status">
|
||||
{Math.round(viewport.scale * 100)}%
|
||||
{nodeCountLabel ? (
|
||||
<span className="game-lineage-summary" role="status">
|
||||
{nodeCountLabel}
|
||||
</span>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => zoomBy(1.2)}
|
||||
aria-label="放大"
|
||||
>
|
||||
<Plus aria-hidden="true" />
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => fitToView(true)}
|
||||
aria-label="适应窗口"
|
||||
>
|
||||
<Expand aria-hidden="true" /> 适应窗口
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => void loadLineage()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCcw aria-hidden="true" /> 刷新
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -776,12 +732,15 @@ export function GameLineagePage({
|
||||
因此放大或平移后轴标签始终在画布左侧可读,而不会被内容层带出去裁掉。
|
||||
*/}
|
||||
<div className="game-lineage-canvas__axis" aria-hidden="true">
|
||||
{layout.axis.map((entry) => (
|
||||
{layout.axis.map((entry, index) => (
|
||||
<span
|
||||
key={entry.generation}
|
||||
className="game-lineage-axis"
|
||||
style={{
|
||||
top: viewport.offsetY + entry.y * viewport.scale,
|
||||
// 行高跟着缩放走:轴上的行与画布里的行始终对齐。
|
||||
height: (layout.nodes[0]?.height ?? 0) * viewport.scale,
|
||||
borderTopWidth: index === 0 ? 0 : 1,
|
||||
}}
|
||||
>
|
||||
<span className="game-lineage-axis__label">
|
||||
@@ -793,6 +752,68 @@ export function GameLineagePage({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/*
|
||||
画布内悬浮控件:放在**右下角**——左侧是代际轴、顶部是路径面包屑,
|
||||
右下角是地图/画布类应用的习惯位置,也最不容易遮住根节点与主干。
|
||||
*/}
|
||||
<div
|
||||
className="game-lineage-controls"
|
||||
role="group"
|
||||
aria-label="画布控件"
|
||||
// 悬浮控件不是画布内容:按下时不要触发画布平移。
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="game-lineage-controls__button"
|
||||
aria-label="缩小"
|
||||
title="缩小"
|
||||
disabled={viewport.scale <= MIN_SCALE + 0.001}
|
||||
onClick={() => zoomBy(1 / 1.2)}
|
||||
>
|
||||
<Minus aria-hidden="true" />
|
||||
</button>
|
||||
<span className="game-lineage-zoom" role="status">
|
||||
{Math.round(viewport.scale * 100)}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="game-lineage-controls__button"
|
||||
aria-label="放大"
|
||||
title="放大"
|
||||
disabled={viewport.scale >= MAX_SCALE - 0.001}
|
||||
onClick={() => zoomBy(1.2)}
|
||||
>
|
||||
<Plus aria-hidden="true" />
|
||||
</button>
|
||||
<span
|
||||
className="game-lineage-controls__divider"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="game-lineage-controls__button game-lineage-controls__button--wide"
|
||||
aria-label="适应窗口"
|
||||
title="适应窗口"
|
||||
onClick={() => fitToView(true)}
|
||||
>
|
||||
<Expand aria-hidden="true" />
|
||||
<span className="game-lineage-controls__label">
|
||||
适应窗口
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="game-lineage-controls__button game-lineage-controls__button--wide"
|
||||
aria-label="刷新"
|
||||
title="刷新"
|
||||
disabled={isLoading}
|
||||
onClick={() => void loadLineage()}
|
||||
>
|
||||
<RefreshCcw aria-hidden="true" />
|
||||
<span className="game-lineage-controls__label">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{derivedCount === 0 ? (
|
||||
<PlatformEmptyState surface="subpanel" size="panel">
|
||||
|
||||
@@ -723,6 +723,79 @@
|
||||
color: var(--platform-accent-strong);
|
||||
font-weight: 700;
|
||||
}
|
||||
/* 画布内悬浮控件(右下角):圆角胶囊、浅色底 + 毛玻璃、内边距统一、轻阴影。 */
|
||||
.game-lineage-controls {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
right: 0.6rem;
|
||||
bottom: 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.3rem 0.4rem;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 999px;
|
||||
background: var(--platform-surface, #fff);
|
||||
box-shadow: 0 8px 22px rgba(76, 41, 27, 0.16);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.game-lineage-controls__button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--platform-text-strong);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.game-lineage-controls__button:hover:not(:disabled) {
|
||||
background: rgba(199, 101, 61, 0.14);
|
||||
}
|
||||
.game-lineage-controls__button:disabled {
|
||||
opacity: 0.38;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.game-lineage-controls__button--wide {
|
||||
width: auto;
|
||||
padding: 0 0.65rem;
|
||||
}
|
||||
.game-lineage-controls__label {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.game-lineage-controls__divider {
|
||||
width: 1px;
|
||||
height: 1.2rem;
|
||||
background: var(--platform-surface-border);
|
||||
}
|
||||
/* 缩放读数:与 ± 按钮视觉等重、等宽对齐(数字不跳动)。 */
|
||||
.game-lineage-zoom {
|
||||
min-width: 3.1rem;
|
||||
text-align: center;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.game-lineage-summary {
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 0.78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.game-lineage-controls__label {
|
||||
display: none;
|
||||
}
|
||||
.game-lineage-controls__button--wide {
|
||||
width: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 族谱树可视化:一个可平移/缩放的画布 + 内容层(CSS transform)。
|
||||
@@ -821,35 +894,39 @@
|
||||
.game-lineage-edge--dimmed {
|
||||
opacity: 0.08;
|
||||
}
|
||||
/* 代际轴:画布左侧的固定层(在内容层的 transform 之外),放大/平移后仍然可读、不被裁。 */
|
||||
/* 代际轴:画布左侧的固定层(在内容层的 transform 之外),放大/平移后仍然可读、不被裁。
|
||||
背景用平台 surface 淡色调(**不是白色条**),每个代际行只画一条分隔线,
|
||||
因此整体看起来是一条连续的轴,而不是一条条白条。 */
|
||||
.game-lineage-canvas__axis {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 92px;
|
||||
width: 96px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--platform-surface, #fff) 72%,
|
||||
transparent
|
||||
);
|
||||
border-right: 1px solid var(--platform-surface-border);
|
||||
background: var(--platform-surface-muted, rgba(104, 54, 36, 0.055));
|
||||
}
|
||||
.game-lineage-axis {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 0.74rem;
|
||||
padding-left: 0.55rem;
|
||||
border-top: 1px solid var(--platform-surface-border);
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.game-lineage-axis__count {
|
||||
border-radius: 999px;
|
||||
padding: 0.05rem 0.45rem;
|
||||
background: var(--platform-surface-border);
|
||||
background: var(--platform-surface, #fff);
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
@@ -920,10 +997,7 @@
|
||||
color: var(--platform-text-strong);
|
||||
background: rgba(199, 101, 61, 0.12);
|
||||
}
|
||||
/* 窄屏紧凑卡:封面比例更扁、标题字号相对更大,字段更少(作者行不渲染)。 */
|
||||
.game-lineage-node--compact .game-lineage-node__cover {
|
||||
aspect-ratio: 176 / 96;
|
||||
}
|
||||
/* 窄屏紧凑卡:卡片更小、标题字号相对更大、字段更少(作者行不渲染);封面仍是 16:9。 */
|
||||
.game-lineage-node--compact .game-lineage-node__title {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
@@ -983,10 +1057,16 @@
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 224 / 120;
|
||||
/* 横版封面:比例由 LINEAGE_COVER_ASPECT 内联给出(16:9,与发布面板封面预览一致)。 */
|
||||
overflow: hidden;
|
||||
background: var(--platform-surface-border);
|
||||
}
|
||||
.game-lineage-node__cover img,
|
||||
.game-lineage-node__cover .game-cover__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.game-lineage-node__cover .game-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
collectLineageHighlight,
|
||||
generationAxisLabel,
|
||||
LINEAGE_COLUMN_GAP,
|
||||
LINEAGE_COVER_ASPECT,
|
||||
LINEAGE_NODE_HEIGHT,
|
||||
LINEAGE_NODE_WIDTH,
|
||||
} from './lineageTreeLayout';
|
||||
|
||||
@@ -47,6 +49,15 @@ function assertNoOverlap(
|
||||
}
|
||||
|
||||
describe('buildLineageTreeLayout', () => {
|
||||
it('封面宽高比与平台封面约定一致(16:9 横版)', () => {
|
||||
// 发布面板的封面预览就写死 16/9(gameDistribution.css 的 .game-publish-media__preview),
|
||||
// 这里把它钉成常量:节点卡片误改成竖版会在单测里变红。
|
||||
expect(LINEAGE_COVER_ASPECT).toBeCloseTo(16 / 9, 5);
|
||||
expect(LINEAGE_NODE_WIDTH / LINEAGE_COVER_ASPECT).toBeLessThan(
|
||||
LINEAGE_NODE_HEIGHT,
|
||||
);
|
||||
});
|
||||
|
||||
it('单节点:一个布局节点、零连线、一条代际轴', () => {
|
||||
const layout = buildLineageTreeLayout([node('game-root', 0, null)]);
|
||||
expect(layout.nodes).toHaveLength(1);
|
||||
|
||||
@@ -20,8 +20,23 @@ import type { GameDistributionLineageNode } from '../../services/gameDistributio
|
||||
* 与「竖直段/水平段存在」而不是解析路径。
|
||||
*/
|
||||
|
||||
export const LINEAGE_NODE_WIDTH = 224;
|
||||
export const LINEAGE_NODE_HEIGHT = 184;
|
||||
/**
|
||||
* 封面宽高比:与平台既有封面约定一致 = **16 / 9 横版**。
|
||||
*
|
||||
* 依据(仓库现役约定,不是我自定的):
|
||||
* - 发布面板的封面预览 `.game-publish-media__preview { aspect-ratio: 16 / 9 }`
|
||||
* (`src/components/game-distribution/gameDistribution.css:1645`)
|
||||
* - 发布面板的截图/占位 `.game-publish-media__shot img|placeholder { aspect-ratio: 16 / 9 }`
|
||||
* (同文件 `:1733`、`:1740`)
|
||||
* - 详情页媒体区同样按 16 / 9(`packages/shared/src/components/GameDetailDisplay/index.css:114`)
|
||||
*
|
||||
* 节点卡片按这个比例给封面留位,所以「竖图当封面」的问题从容器一侧就被消除。
|
||||
*/
|
||||
export const LINEAGE_COVER_ASPECT = 16 / 9;
|
||||
|
||||
export const LINEAGE_NODE_WIDTH = 240;
|
||||
/** 卡片高度 = 16:9 封面 + 信息条(标题 + 作者)。 */
|
||||
export const LINEAGE_NODE_HEIGHT = 192;
|
||||
/** 同一层相邻槽位之间额外留的横向间隙(保证不重叠)。 */
|
||||
export const LINEAGE_COLUMN_GAP = 32;
|
||||
/** 相邻代际之间的纵向间隙。 */
|
||||
|
||||
Reference in New Issue
Block a user