fix: refine profile shortcuts and puzzle next button
This commit is contained in:
@@ -52,7 +52,7 @@
|
|||||||
8. 私有 generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签读取。
|
8. 私有 generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签读取。
|
||||||
9. 敲木鱼作品架读取当前用户作品列表时走 `GET /api/creation/wooden-fish/works`;发布成功后平台壳必须同时刷新作品架与公开广场,避免作品刚发布时仍停留在旧列表。
|
9. 敲木鱼作品架读取当前用户作品列表时走 `GET /api/creation/wooden-fish/works`;发布成功后平台壳必须同时刷新作品架与公开广场,避免作品刚发布时仍停留在旧列表。
|
||||||
|
|
||||||
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息组织,不保留旧的底部“填邀请码”次级入口;每日任务卡必须读取 `/api/profile/tasks` 的当前任务摘要并在领取后同步刷新卡片进度。字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
|
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息组织,不保留旧的底部“填邀请码”次级入口;常用功能当前只展示四项常驻入口时必须按四列铺满整行,不保留五列网格导致左对齐空位;每日任务卡必须读取 `/api/profile/tasks` 的当前任务摘要并在领取后同步刷新卡片进度。字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
|
||||||
|
|
||||||
## RPG / 自定义世界
|
## RPG / 自定义世界
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
|
|||||||
- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。
|
- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。
|
||||||
- 拼图试玩和正式运行态刷新恢复不复用创作私有 query。进入 `/runtime/puzzle` 时必须写入 `runtimeProfileId`、草稿 `runtimeSessionId`、可选 `runtimeLevelId`、公开作品 `work` 和 `mode=draft|published`;进入运行态的导航顺序必须先切到 `/runtime/puzzle`,再写这些 runtime query,避免被阶段导航清掉后刷新停在“正在进入拼图关卡”。
|
- 拼图试玩和正式运行态刷新恢复不复用创作私有 query。进入 `/runtime/puzzle` 时必须写入 `runtimeProfileId`、草稿 `runtimeSessionId`、可选 `runtimeLevelId`、公开作品 `work` 和 `mode=draft|published`;进入运行态的导航顺序必须先切到 `/runtime/puzzle`,再写这些 runtime query,避免被阶段导航清掉后刷新停在“正在进入拼图关卡”。
|
||||||
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
||||||
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回和设置按钮的点击容器只提供透明点击区,不再叠加默认白色圆形底;底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回、设置和下一关的点击容器只提供透明点击区,不再叠加默认白色圆形底、胶囊主按钮底或额外文字;下一关按钮在通关弹窗和底部入口中都直接使用 spritesheet 裁切出的 next 素材作为按钮本体。底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
||||||
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,启动拼图和后续排行榜 / 下一关等正式请求继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。`/api/runtime/puzzle/runs*` 后端统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,启动拼图和后续排行榜 / 下一关等正式请求继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。`/api/runtime/puzzle/runs*` 后端统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
||||||
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
||||||
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ server-rs + Axum + SpacetimeDB
|
|||||||
7. 主站入口已锁定移动端页面级缩放;单个游戏页面不要再重复实现整页缩放锁定。
|
7. 主站入口已锁定移动端页面级缩放;单个游戏页面不要再重复实现整页缩放锁定。
|
||||||
8. 图像输入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`。外层页面持有业务状态,组件只承担上传卡、预览、参考图缩略图、AI 重绘开关、错误展示和提交按钮。
|
8. 图像输入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`。外层页面持有业务状态,组件只承担上传卡、预览、参考图缩略图、AI 重绘开关、错误展示和提交按钮。
|
||||||
9. 发现页 `分类` 子频道的筛选必须打开独立 dialog / drawer / modal,至少支持玩法类型过滤与排序切换;筛选结果为空时显示空状态,不把筛选内容展开在当前列表下方。
|
9. 发现页 `分类` 子频道的筛选必须打开独立 dialog / drawer / modal,至少支持玩法类型过滤与排序切换;筛选结果为空时显示空状态,不把筛选内容展开在当前列表下方。
|
||||||
10. 移动端“我的”页顶部品牌行承载扫码和设置入口,正文按参考图顺序组织为头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息;`media/profile/` 中的陶泥素材作为该页图形资产。常用功能宫格固定承载泥点充值、邀请好友、兑换码、玩家社区、反馈与建议;页面不再提供独立存档按钮入口,也不在底部保留旧的填邀请码次级入口。填邀请码只由邀请链接 query 或其它明确引导打开独立弹窗,不作为“我的”页常驻按钮。
|
10. 移动端“我的”页顶部品牌行承载扫码和设置入口,正文按参考图顺序组织为头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息;`media/profile/` 中的陶泥素材作为该页图形资产。常用功能宫格固定承载泥点充值、邀请好友、兑换码、玩家社区、反馈与建议;当前只展示四项常驻入口时必须按四列铺满整行,不保留五列网格导致左对齐空位。页面不再提供独立存档按钮入口,也不在底部保留旧的填邀请码次级入口。填邀请码只由邀请链接 query 或其它明确引导打开独立弹窗,不作为“我的”页常驻按钮。
|
||||||
11. “我的”页每日任务卡必须展示后端 `/api/profile/tasks` 返回的当前任务摘要,包括奖励泥点数、进度和领取 / 去完成 / 已完成状态;任务领取成功后,卡片摘要必须跟随返回的任务中心数据同步刷新,不能继续硬编码 `0 / 1` 或只更新弹窗内任务列表。
|
11. “我的”页每日任务卡必须展示后端 `/api/profile/tasks` 返回的当前任务摘要,包括奖励泥点数、进度和领取 / 去完成 / 已完成状态;任务领取成功后,卡片摘要必须跟随返回的任务中心数据同步刷新,不能继续硬编码 `0 / 1` 或只更新弹窗内任务列表。
|
||||||
12. “我的”页泥点、游戏时长、已玩游戏数量三张统计卡只展示各自标签和值,三个统计 icon 使用小尺寸普通 UI 档位,内容不换行,不在统计区底部展示“更新于”时间;移动端昵称、会员卡、每日任务、常用功能和法律信息也应保持 `10px` 到 `14px` 的普通 UI 字号区间,避免展示级字号挤压内容。
|
12. “我的”页泥点、游戏时长、已玩游戏数量三张统计卡只展示各自标签和值,三个统计 icon 使用小尺寸普通 UI 档位,内容不换行,不在统计区底部展示“更新于”时间;移动端昵称、会员卡、每日任务、常用功能和法律信息也应保持 `10px` 到 `14px` 的普通 UI 字号区间,避免展示级字号挤压内容。
|
||||||
13. 移动端“我的”页需要兼容窄屏:头像 / 昵称 / 陶泥号、三张统计卡、每日任务、五项常用功能和法律信息都必须能在底部固定 TabBar 上方完整滚动露出,不得与底部 dock、刘海 safe-area 或相邻 UI 元素遮挡重叠。
|
13. 移动端“我的”页需要兼容窄屏:头像 / 昵称 / 陶泥号、三张统计卡、每日任务、五项常用功能和法律信息都必须能在底部固定 TabBar 上方完整滚动露出,不得与底部 dock、刘海 safe-area 或相邻 UI 元素遮挡重叠。
|
||||||
|
|||||||
@@ -617,7 +617,11 @@ test('通关后显示结算弹窗、排行榜和下一关按钮', () => {
|
|||||||
expect(within(dialog).getByText('#1')).toBeTruthy();
|
expect(within(dialog).getByText('#1')).toBeTruthy();
|
||||||
expect(within(dialog).getByText('测试作者')).toBeTruthy();
|
expect(within(dialog).getByText('测试作者')).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(within(dialog).getByRole('button', { name: '下一关' }));
|
const nextButton = within(dialog).getByRole('button', { name: '下一关' });
|
||||||
|
expect(nextButton.textContent).toContain('下一关');
|
||||||
|
expect(nextButton.querySelector('[data-puzzle-ui-sprite="next"]')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(nextButton);
|
||||||
|
|
||||||
expect(onAdvanceNextLevel).toHaveBeenCalledTimes(1);
|
expect(onAdvanceNextLevel).toHaveBeenCalledTimes(1);
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
@@ -876,13 +880,16 @@ test('运行态用 UI spritesheet 原图检测矩形裁切返回设置下一关
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: '打开拼图设置' }).className,
|
screen.getByRole('button', { name: '打开拼图设置' }).className,
|
||||||
).not.toContain('rounded-full');
|
).not.toContain('rounded-full');
|
||||||
const nextSprite = screen
|
const nextButton = screen.getByRole('button', { name: '下一关' });
|
||||||
.getByRole('button', { name: '下一关' })
|
expect(nextButton.dataset.puzzleUiSprite).toBe('next');
|
||||||
.querySelector('[data-puzzle-ui-sprite="next"]') as HTMLElement | null;
|
expect(nextButton.querySelector('[data-puzzle-ui-sprite="next"]')).toBeNull();
|
||||||
expect(nextSprite).toBeTruthy();
|
expect(nextButton.style.backgroundSize).toBe('320% 480%');
|
||||||
expect(nextSprite?.style.backgroundSize).toBe('320% 480%');
|
expect(nextButton.style.backgroundPosition).toBe('50% 57.89473684210527%');
|
||||||
expect(nextSprite?.style.backgroundPosition).toBe('50% 57.89473684210527%');
|
expect(nextButton.className).not.toContain('puzzle-runtime-primary-button');
|
||||||
expect(screen.getByRole('button', { name: '下一关' }).textContent).toBe('');
|
expect(nextButton.className).not.toContain('rounded-full');
|
||||||
|
expect(nextButton.className).not.toContain('px-5');
|
||||||
|
expect(nextButton.className).not.toContain('py-2.5');
|
||||||
|
expect(nextButton.textContent).toBe('');
|
||||||
expect(
|
expect(
|
||||||
screen
|
screen
|
||||||
.getByRole('button', { name: '提示' })
|
.getByRole('button', { name: '提示' })
|
||||||
@@ -962,7 +969,7 @@ test('运行态在只有 UI 背景 objectKey 时仍渲染生成背景', () => {
|
|||||||
expect(backgroundImage).toBeTruthy();
|
expect(backgroundImage).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('关闭通关弹窗后保留底部下一关入口', () => {
|
test('关闭通关弹窗后保留底部下一关入口', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const onAdvanceNextLevel = vi.fn();
|
const onAdvanceNextLevel = vi.fn();
|
||||||
const runWithoutRecommendedNextProfile: PuzzleRunSnapshot = {
|
const runWithoutRecommendedNextProfile: PuzzleRunSnapshot = {
|
||||||
@@ -988,10 +995,31 @@ test('关闭通关弹窗后保留底部下一关入口', () => {
|
|||||||
onAdvanceNextLevel={onAdvanceNextLevel}
|
onAdvanceNextLevel={onAdvanceNextLevel}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
await act(async () => {});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(1_400);
|
vi.advanceTimersByTime(1_400);
|
||||||
});
|
});
|
||||||
|
const dialog = screen.getByRole('dialog', { name: '通关完成' });
|
||||||
|
const dialogNextButton = within(dialog).getByRole('button', {
|
||||||
|
name: '下一关',
|
||||||
|
});
|
||||||
|
expect(dialogNextButton.dataset.puzzleUiSprite).toBe('next');
|
||||||
|
expect(
|
||||||
|
dialogNextButton.querySelector('[data-puzzle-ui-sprite="next"]'),
|
||||||
|
).toBeNull();
|
||||||
|
expect(dialogNextButton.style.backgroundSize).toBe('320% 480%');
|
||||||
|
expect(dialogNextButton.style.backgroundPosition).toBe(
|
||||||
|
'50% 57.89473684210527%',
|
||||||
|
);
|
||||||
|
expect(dialogNextButton.className).not.toContain(
|
||||||
|
'puzzle-runtime-primary-button',
|
||||||
|
);
|
||||||
|
expect(dialogNextButton.className).not.toContain('rounded-full');
|
||||||
|
expect(dialogNextButton.className).not.toContain('px-5');
|
||||||
|
expect(dialogNextButton.className).not.toContain('py-2.5');
|
||||||
|
expect(dialogNextButton.textContent).toBe('');
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭通关弹窗' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭通关弹窗' }));
|
||||||
});
|
});
|
||||||
@@ -1050,9 +1078,8 @@ test('推荐页关闭通关弹窗后保留底部下一关入口且不叠加下
|
|||||||
expect(screen.queryByRole('dialog', { name: '通关完成' })).toBeNull();
|
expect(screen.queryByRole('dialog', { name: '通关完成' })).toBeNull();
|
||||||
const nextButton = screen.getByRole('button', { name: /下一关/u });
|
const nextButton = screen.getByRole('button', { name: /下一关/u });
|
||||||
expect(nextButton).toBeTruthy();
|
expect(nextButton).toBeTruthy();
|
||||||
expect(
|
expect(nextButton.dataset.puzzleUiSprite).toBe('next');
|
||||||
nextButton.querySelector('[data-puzzle-ui-sprite="next"]'),
|
expect(nextButton.querySelector('[data-puzzle-ui-sprite="next"]')).toBeNull();
|
||||||
).toBeTruthy();
|
|
||||||
expect(nextButton.textContent?.trim()).toBe('');
|
expect(nextButton.textContent?.trim()).toBe('');
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,6 +162,15 @@ function PuzzleUiSprite({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePuzzleUiSpriteAspectRatio(
|
||||||
|
kind: PuzzleUiSpriteKind,
|
||||||
|
layout: PuzzleUiSpritesheetLayout | null,
|
||||||
|
fallback: string,
|
||||||
|
) {
|
||||||
|
const region = layout?.regions[kind];
|
||||||
|
return region ? `${region.width} / ${region.height}` : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function buildMergedGroupViewModels(
|
function buildMergedGroupViewModels(
|
||||||
groups: PuzzleMergedGroupState[],
|
groups: PuzzleMergedGroupState[],
|
||||||
pieces: PuzzleBoardPieceViewModel[],
|
pieces: PuzzleBoardPieceViewModel[],
|
||||||
@@ -1221,6 +1230,22 @@ export function PuzzleRuntimeShell({
|
|||||||
const clearResultOverlayClassName = embedded
|
const clearResultOverlayClassName = embedded
|
||||||
? `platform-ui-shell platform-theme ${platformThemeClass} puzzle-runtime-shell puzzle-runtime-modal-overlay puzzle-runtime-modal-overlay--fixed flex items-center justify-center px-4 py-6 backdrop-blur-sm`
|
? `platform-ui-shell platform-theme ${platformThemeClass} puzzle-runtime-shell puzzle-runtime-modal-overlay puzzle-runtime-modal-overlay--fixed flex items-center justify-center px-4 py-6 backdrop-blur-sm`
|
||||||
: 'puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm';
|
: 'puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm';
|
||||||
|
const nextSpriteButtonClassName =
|
||||||
|
'inline-flex h-12 appearance-none items-center justify-center border-0 bg-transparent p-0 leading-none shadow-none transition hover:brightness-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-button-primary-border)] disabled:cursor-not-allowed disabled:opacity-45';
|
||||||
|
const nextSpriteButtonStyle = hasUiSpritesheet
|
||||||
|
? {
|
||||||
|
...buildPuzzleUiSpriteBackgroundStyle({
|
||||||
|
src: resolvedUiSpritesheetImage,
|
||||||
|
kind: 'next',
|
||||||
|
layout: uiSpritesheetLayout,
|
||||||
|
}),
|
||||||
|
aspectRatio: resolvePuzzleUiSpriteAspectRatio(
|
||||||
|
'next',
|
||||||
|
uiSpritesheetLayout,
|
||||||
|
'2 / 1',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
const handleBackRequest = () => {
|
const handleBackRequest = () => {
|
||||||
if (hideExitControls) {
|
if (hideExitControls) {
|
||||||
return;
|
return;
|
||||||
@@ -1457,6 +1482,8 @@ export function PuzzleRuntimeShell({
|
|||||||
<footer className="puzzle-runtime-dialog__line flex items-center justify-end border-t px-5 py-4">
|
<footer className="puzzle-runtime-dialog__line flex items-center justify-end border-t px-5 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label="下一关"
|
||||||
|
data-puzzle-ui-sprite={hasUiSpritesheet ? 'next' : undefined}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAdvanceNextLevel({
|
onAdvanceNextLevel({
|
||||||
@@ -1464,14 +1491,23 @@ export function PuzzleRuntimeShell({
|
|||||||
levelId: run.nextLevelId ?? null,
|
levelId: run.nextLevelId ?? null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="puzzle-runtime-primary-button inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-black transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-45"
|
style={nextSpriteButtonStyle}
|
||||||
|
className={
|
||||||
|
hasUiSpritesheet
|
||||||
|
? nextSpriteButtonClassName
|
||||||
|
: 'puzzle-runtime-primary-button inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-black transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-45'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
{hasUiSpritesheet ? null : (
|
||||||
|
<>
|
||||||
{isBusy ? (
|
{isBusy ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
下一关
|
下一关
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1933,6 +1969,7 @@ export function PuzzleRuntimeShell({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
|
data-puzzle-ui-sprite={hasUiSpritesheet ? 'next' : undefined}
|
||||||
aria-label={hasSimilarWorkChoices ? '换个作品' : '下一关'}
|
aria-label={hasSimilarWorkChoices ? '换个作品' : '下一关'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hasSimilarWorkChoices) {
|
if (hasSimilarWorkChoices) {
|
||||||
@@ -1945,16 +1982,18 @@ export function PuzzleRuntimeShell({
|
|||||||
levelId: run.nextLevelId ?? null,
|
levelId: run.nextLevelId ?? null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="puzzle-runtime-primary-button inline-flex min-h-11 items-center justify-center rounded-full px-5 py-2.5 text-sm font-bold transition hover:brightness-105 disabled:opacity-45"
|
style={nextSpriteButtonStyle}
|
||||||
|
className={
|
||||||
|
hasUiSpritesheet
|
||||||
|
? nextSpriteButtonClassName
|
||||||
|
: 'puzzle-runtime-primary-button inline-flex min-h-11 items-center justify-center rounded-full px-5 py-2.5 text-sm font-bold transition hover:brightness-105 disabled:opacity-45'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PuzzleUiSprite
|
{hasUiSpritesheet ? null : (
|
||||||
src={resolvedUiSpritesheetImage}
|
<>
|
||||||
kind="next"
|
|
||||||
layout={uiSpritesheetLayout}
|
|
||||||
className="h-8 w-12 rounded-full"
|
|
||||||
/>
|
|
||||||
{resolvedUiSpritesheetImage ? null : (
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
{hasSimilarWorkChoices ? '换个作品' : '下一关'}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -2412,6 +2412,21 @@ test('mobile profile page matches the reference layout sections', async () => {
|
|||||||
.querySelector('.platform-profile-shortcut-grid')
|
.querySelector('.platform-profile-shortcut-grid')
|
||||||
?.classList.contains('platform-profile-shortcut-grid'),
|
?.classList.contains('platform-profile-shortcut-grid'),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shortcutRegion
|
||||||
|
.querySelector('.platform-profile-shortcut-grid')
|
||||||
|
?.className,
|
||||||
|
).toContain('!grid-cols-4');
|
||||||
|
expect(
|
||||||
|
shortcutRegion
|
||||||
|
.querySelector('.platform-profile-shortcut-grid')
|
||||||
|
?.className,
|
||||||
|
).toContain('w-full');
|
||||||
|
for (const shortcutButton of shortcutRegion.querySelectorAll(
|
||||||
|
'.platform-profile-shortcut-button',
|
||||||
|
)) {
|
||||||
|
expect(shortcutButton.className).toContain('w-full');
|
||||||
|
}
|
||||||
for (const label of [
|
for (const label of [
|
||||||
'泥点充值',
|
'泥点充值',
|
||||||
'兑换码',
|
'兑换码',
|
||||||
|
|||||||
@@ -2434,7 +2434,7 @@ function ProfileShortcutButton({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick ?? undefined}
|
onClick={onClick ?? undefined}
|
||||||
className="platform-profile-shortcut-button flex min-h-[5.25rem] flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
|
className="platform-profile-shortcut-button flex min-h-[5.25rem] w-full flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
|
||||||
>
|
>
|
||||||
<div className="platform-profile-shortcut-button__icon">
|
<div className="platform-profile-shortcut-button__icon">
|
||||||
{imageSrc ? (
|
{imageSrc ? (
|
||||||
@@ -6368,7 +6368,7 @@ export function RpgEntryHomeView({
|
|||||||
className="platform-profile-shortcut-panel"
|
className="platform-profile-shortcut-panel"
|
||||||
aria-label="常用功能"
|
aria-label="常用功能"
|
||||||
>
|
>
|
||||||
<div className="platform-profile-shortcut-grid">
|
<div className="platform-profile-shortcut-grid grid w-full !grid-cols-4">
|
||||||
<ProfileShortcutButton
|
<ProfileShortcutButton
|
||||||
label="泥点充值"
|
label="泥点充值"
|
||||||
subLabel="充值泥点"
|
subLabel="充值泥点"
|
||||||
|
|||||||
Reference in New Issue
Block a user