首屏「立即试玩」改为滚动到列表而不是打开第一款游戏
- GameGalleryPage 的 hero 按钮不再取 visibleGames[0] 直接开详情,改为把游戏列表滚进视野
- 滚动按平台页签面板(.platform-tab-panel)算相对偏移并走 scrollTo({behavior:'smooth'}),缺少 scrollTo 时退化成直接赋值
- 新增 gameListRef 锚在列表工具栏,退役只服务该按钮的 featuredGame 派生
- GameDistributionPages.test.tsx 新增「首屏「立即试玩」滚动到列表,而不是打开第一款游戏详情」,断言未触发 onOpenDetail 且滚动落在页签面板上
- 里程碑文档新增「网页侧交互修正(2026-09-30)」记录该修正与验证命令
This commit is contained in:
@@ -320,3 +320,8 @@
|
||||
| 同上路径但带 `Cookie` | **403 Forbidden** + `application/json`(`x-api-version: 2026-06-16`、`x-response-time-ms`)——发行读取**拒绝携带账号凭证**的请求,与本地用例断言一致 |
|
||||
|
||||
**生产环境**(`genarrative.world`)同一时刻 `/api/game-distribution/games` 返回 **0 条**:说明当前生产还没有任何已发布的游戏,条目 ④「真实环境整链路」需要在生产发布一个作品后再跑。也就是说此刻能拿到的真实环境证据是「dev 部署的公开读路径 + 发行隔离」,写路径与审核链路的真实环境验收仍需生产账号。
|
||||
|
||||
## 网页侧交互修正(2026-09-30)
|
||||
|
||||
- **广场首屏「立即试玩」不再替用户选中作品**:`src/components/game-distribution/GameGalleryPage.tsx` 原来是 `onClick={() => featuredGame && openDetail(featuredGame.id)}`,点一下等于直接打开第一款游戏的详情。现在按钮只把列表滚进视野(按平台页签面板计算相对偏移,`scrollTo({ behavior: 'smooth' })`,无 `scrollTo` 时退化成直接赋值),不再触发 `onOpenDetail`;`featuredGame` 派生随之退役。回归用例 `GameDistributionPages.test.tsx`「首屏「立即试玩」滚动到列表,而不是打开第一款游戏详情」断言 `onOpenDetail` 未被调用且滚动落在页签面板上。
|
||||
- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npm run typecheck`、`npx eslint`、`npm run check:encoding`、`git diff --check` 通过。视觉与真机走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。
|
||||
|
||||
@@ -375,6 +375,50 @@ describe('GameGalleryPage 筛选与返回上下文', () => {
|
||||
).toBe('180'),
|
||||
);
|
||||
});
|
||||
|
||||
it('首屏「立即试玩」滚动到列表,而不是打开第一款游戏详情', async () => {
|
||||
resetGalleryContext();
|
||||
const onOpenDetail = vi.fn();
|
||||
listGamesMock.mockResolvedValue([
|
||||
createGame({ id: 'game-1', title: '星轨防线' }),
|
||||
createGame({ id: 'game-2', title: '第二款' }),
|
||||
]);
|
||||
const scrollTo = vi.fn();
|
||||
const originalScrollTo = Object.getOwnPropertyDescriptor(
|
||||
Element.prototype,
|
||||
'scrollTo',
|
||||
);
|
||||
Object.defineProperty(Element.prototype, 'scrollTo', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: scrollTo,
|
||||
});
|
||||
try {
|
||||
const { container } = render(
|
||||
<div className="platform-tab-panel">
|
||||
<GameGalleryPage onOpenDetail={onOpenDetail} />
|
||||
</div>,
|
||||
);
|
||||
expect(await screen.findByText('星轨防线')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /立即试玩/u }));
|
||||
|
||||
expect(onOpenDetail).not.toHaveBeenCalled();
|
||||
expect(scrollTo).toHaveBeenCalledTimes(1);
|
||||
expect(scrollTo.mock.instances[0]).toBe(
|
||||
container.querySelector('.platform-tab-panel'),
|
||||
);
|
||||
expect(scrollTo.mock.calls[0]?.[0]).toMatchObject({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
} finally {
|
||||
if (originalScrollTo) {
|
||||
Object.defineProperty(Element.prototype, 'scrollTo', originalScrollTo);
|
||||
} else {
|
||||
delete (Element.prototype as { scrollTo?: unknown }).scrollTo;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GamePlayPage 启动超时与旋转提示', () => {
|
||||
|
||||
@@ -145,6 +145,7 @@ export function GameGalleryPage({
|
||||
const [error, setError] = useState('');
|
||||
const requestSerialRef = useRef(0);
|
||||
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
||||
const gameListRef = useRef<HTMLDivElement | null>(null);
|
||||
const hasRestoredScrollRef = useRef(false);
|
||||
/**
|
||||
* 顶栏搜索词只在「真的变了」时覆盖目录筛选。
|
||||
@@ -167,6 +168,31 @@ export function GameGalleryPage({
|
||||
container instanceof HTMLElement ? container : null;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 首屏「立即试玩」只把列表滚进视野,不替用户选定某一款游戏。
|
||||
*
|
||||
* 滚动容器是平台页签面板而不是窗口,所以按容器相对位置计算,缺少
|
||||
* `scrollTo` 时退化成直接赋值,保证无动画环境也能生效。
|
||||
*/
|
||||
const scrollToGameList = useCallback(() => {
|
||||
const anchor = gameListRef.current;
|
||||
if (!anchor) return;
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) {
|
||||
anchor.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
const nextScrollTop =
|
||||
container.scrollTop +
|
||||
anchor.getBoundingClientRect().top -
|
||||
container.getBoundingClientRect().top;
|
||||
if (typeof container.scrollTo === 'function') {
|
||||
container.scrollTo({ top: nextScrollTop, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
container.scrollTop = nextScrollTop;
|
||||
}, []);
|
||||
|
||||
const loadGames = useCallback(() => {
|
||||
const requestSerial = ++requestSerialRef.current;
|
||||
setIsLoading(true);
|
||||
@@ -231,7 +257,6 @@ export function GameGalleryPage({
|
||||
() => games.filter((game) => matchesGameDeviceFilter(game, device)),
|
||||
[device, games],
|
||||
);
|
||||
const featuredGame = useMemo(() => visibleGames[0] ?? null, [visibleGames]);
|
||||
const hasActiveFilters =
|
||||
activeKeyword.trim().length > 0 || category !== '全部' || device !== '全部';
|
||||
const resetFilters = () => {
|
||||
@@ -270,7 +295,7 @@ export function GameGalleryPage({
|
||||
<PlatformActionButton
|
||||
size="md"
|
||||
shape="pill"
|
||||
onClick={() => featuredGame && openDetail(featuredGame.id)}
|
||||
onClick={scrollToGameList}
|
||||
>
|
||||
<Gamepad2 aria-hidden="true" /> 立即试玩
|
||||
</PlatformActionButton>
|
||||
@@ -282,7 +307,7 @@ export function GameGalleryPage({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="game-toolbar">
|
||||
<div className="game-toolbar" ref={gameListRef}>
|
||||
<div className="game-toolbar__title">
|
||||
<span className="game-eyebrow">发现好玩的</span>
|
||||
<h2>游戏广场</h2>
|
||||
|
||||
Reference in New Issue
Block a user