fix: refine jump hop draft detail flow

This commit is contained in:
2026-06-06 21:02:11 +08:00
parent cd8088d1fd
commit 683a9115b3
11 changed files with 194 additions and 9 deletions

View File

@@ -149,9 +149,11 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
3. 地块只调用一次 image2输出一张 `5行*5列``1:1`、单一纯洋红 `#FF00FF` key 背景的主题地块图集;跳一跳地块常包含草地、花、雪、白石和云朵,后端透明化必须使用跳一跳专用洋红 key不启用近白底扣除也不清理非边缘连通的 key 色像素,避免把绿色或白色主体误扣;后处理必须对边缘连通 key 色做容差清理、去彩边 defringe 和底部残影清理,主体图不得自带洋红阴影、紫色底边、粉色脏边、彩色光晕或发光底边,运行态阴影统一由 DOM 绘制地块造型提示词要求以主题物体本身外轮廓为准允许苹果近似圆形、香蕉近似长条或长方形、西瓜近似扇形等自然差异只统一单格规格、安全留白、正面30度视角和 2D/2.5D 手绘风格包装所有地块素材必须保持统一正面30度视角相机位于物体正前方略高位置、镜头向下约30度必须看到清晰正面、侧壁、下沿、明显自身厚度和少量上表面主体正面或侧壁可见面积必须接近或大于顶面面积顶面只能作为辅助可见面水果主题需要明确要求橙瓣看到橙皮正面外侧和果肉厚度、椰子看到壳的正面侧壁和切口厚度、浆果不能只是从上往下看的圆形球顶避免生成纯俯视、正上方俯拍、鸟瞰地图块、平铺俯拍、圆形顶视图或扁平图标主题物体本身必须是唯一可落脚体只能用自身切面、边缘厚度、花瓣层或果皮边表现承重禁止在主题物体下方额外垫石台、土墩、木板、圆台、托盘、岛屿底座或通用地板前端和后端默认 `tilePrompt` 都必须使用“正面30度视角主题物体图集物体本身作为跳跃落点”的口径不再提交“平台素材 / 跳台 / 地块 / 地砖”等会把模型拉回通用平台造型的词,后端生成前也会清洗旧草稿遗留的这些词;当主题或地块提示词命中宝可梦 / 神奇宝贝 / 口袋妖怪 / Pokemon / Pikachu / 精灵球等宝可梦相关词时,仅生图请求侧改写为“原创幻想萌宠冒险道具 / 彩色冒险能量球 / 黄色闪电萌宠符号”,用户草稿标题和主题展示不改; 3. 地块只调用一次 image2输出一张 `5行*5列``1:1`、单一纯洋红 `#FF00FF` key 背景的主题地块图集;跳一跳地块常包含草地、花、雪、白石和云朵,后端透明化必须使用跳一跳专用洋红 key不启用近白底扣除也不清理非边缘连通的 key 色像素,避免把绿色或白色主体误扣;后处理必须对边缘连通 key 色做容差清理、去彩边 defringe 和底部残影清理,主体图不得自带洋红阴影、紫色底边、粉色脏边、彩色光晕或发光底边,运行态阴影统一由 DOM 绘制地块造型提示词要求以主题物体本身外轮廓为准允许苹果近似圆形、香蕉近似长条或长方形、西瓜近似扇形等自然差异只统一单格规格、安全留白、正面30度视角和 2D/2.5D 手绘风格包装所有地块素材必须保持统一正面30度视角相机位于物体正前方略高位置、镜头向下约30度必须看到清晰正面、侧壁、下沿、明显自身厚度和少量上表面主体正面或侧壁可见面积必须接近或大于顶面面积顶面只能作为辅助可见面水果主题需要明确要求橙瓣看到橙皮正面外侧和果肉厚度、椰子看到壳的正面侧壁和切口厚度、浆果不能只是从上往下看的圆形球顶避免生成纯俯视、正上方俯拍、鸟瞰地图块、平铺俯拍、圆形顶视图或扁平图标主题物体本身必须是唯一可落脚体只能用自身切面、边缘厚度、花瓣层或果皮边表现承重禁止在主题物体下方额外垫石台、土墩、木板、圆台、托盘、岛屿底座或通用地板前端和后端默认 `tilePrompt` 都必须使用“正面30度视角主题物体图集物体本身作为跳跃落点”的口径不再提交“平台素材 / 跳台 / 地块 / 地砖”等会把模型拉回通用平台造型的词,后端生成前也会清洗旧草稿遗留的这些词;当主题或地块提示词命中宝可梦 / 神奇宝贝 / 口袋妖怪 / Pokemon / Pikachu / 精灵球等宝可梦相关词时,仅生图请求侧改写为“原创幻想萌宠冒险道具 / 彩色冒险能量球 / 黄色闪电萌宠符号”,用户草稿标题和主题展示不改;
4. 背景底图同样由 image2 生成,复用现有 `coverComposite` / `coverImageSrc` 作为运行态背景读写字段OSS 槽位固定为 `background/image.png`;提示词必须严格以用户主题关键词为背景主题,结构以左右两侧氛围为主,中央纵轴 1/2 区域保持少元素、简洁、可读且有纵深感两侧允许更强立体层次和行进感背景只作为底图禁止生成跳板、地块、落脚物、角色、UI、返回按钮、文字、路径箭头或海报排版左上角返回按钮不允许画进背景而是单独生成 `backButtonAsset` 透明 PNGOSS 槽位固定为 `back-button/image.png`,提示词要求标准圆形、主题色材质包装、居中左箭头、纯绿色 key 背景,后端去绿后写入作品 profile 4. 背景底图同样由 image2 生成,复用现有 `coverComposite` / `coverImageSrc` 作为运行态背景读写字段OSS 槽位固定为 `background/image.png`;提示词必须严格以用户主题关键词为背景主题,结构以左右两侧氛围为主,中央纵轴 1/2 区域保持少元素、简洁、可读且有纵深感两侧允许更强立体层次和行进感背景只作为底图禁止生成跳板、地块、落脚物、角色、UI、返回按钮、文字、路径箭头或海报排版左上角返回按钮不允许画进背景而是单独生成 `backButtonAsset` 透明 PNGOSS 槽位固定为 `back-button/image.png`,提示词要求标准圆形、主题色材质包装、居中左箭头、纯绿色 key 背景,后端去绿后写入作品 profile
5. 后端按从上到下、从左到右均匀切分为 `tile-01``tile-25` 的透明 PNG每个切片必须使用唯一 slot/path 持久化,不能按重复的 `tileType` 复用槽位; 5. 后端按从上到下、从左到右均匀切分为 `tile-01``tile-25` 的透明 PNG每个切片必须使用唯一 slot/path 持久化,不能按重复的 `tileType` 复用槽位;
6. 结果页只展示陶泥儿 logo 透明角色预览、地块池预览和首屏 3 地块预览;不再提供旧角色图生成槽; 6. 结果页只展示陶泥儿 logo 透明角色预览、地块池预览和首屏 3 地块预览;不再提供旧角色图生成槽;移动端结果页必须由结果页根容器承接纵向滚动并保留底部安全区,确保素材预览较长时仍能下滑到返回编辑、试玩和发布按钮;
7. 前端跳一跳创作 client 的创建会话与执行生成动作请求都必须使用 20 分钟等待窗口,避免背景底图、地块图集、切片、抠图和 OSS 写入仍在后端执行时被共创会话默认 15 秒超时中断。 7. 前端跳一跳创作 client 的创建会话与执行生成动作请求都必须使用 20 分钟等待窗口,避免背景底图、地块图集、切片、抠图和 OSS 写入仍在后端执行时被共创会话默认 15 秒超时中断。
生成页“当前跳一跳信息”只展示实际参与创作提示词的主题、地块提示词等用户可理解信息;`stylePreset` 等未参与当前 image2 提示词组装的内部风格枚举不得作为兜底内容展示,避免把 `minimal-blocks``paper-toy` 等工程值暴露给创作者。
运行态规则真相必须沉到 `module-jump-hop`前端只做拖拽蓄力、角色位移、投影和落地反馈。失败、成功跳跃次数、游戏时长冻结、运行态快照和发布作品状态以后端为准。v1 不保留公开 combo / perfect / 通关语义,旧 `score` 兼容映射为成功跳跃次数。公开列表应走 `jump_hop_gallery_card_view` 订阅缓存,不要每次 HTTP 请求调用 procedure 组装全量列表。 运行态规则真相必须沉到 `module-jump-hop`前端只做拖拽蓄力、角色位移、投影和落地反馈。失败、成功跳跃次数、游戏时长冻结、运行态快照和发布作品状态以后端为准。v1 不保留公开 combo / perfect / 通关语义,旧 `score` 兼容映射为成功跳跃次数。公开列表应走 `jump_hop_gallery_card_view` 订阅缓存,不要每次 HTTP 请求调用 procedure 组装全量列表。
每屏只展示 3 个地块:当前地块、目标地块和下一预览地块。平台流按同一 seed 无限生成,前端不得自行生成正式路径。运行态 HUD 顶部只保留返回按钮和成功跳跃次数不展示计时器或右上角重开按钮生成背景和游戏舞台必须覆盖整个运行态视口HUD 直接绝对定位压在背景上,不再用外层白底、居中窄栏、卡片边框或游戏区域圆角裁切背景。返回按钮固定在左上角安全区,交互热区固定为移动端 `56px`、桌面约 `62px`,不显示“返回”文字,并通过顶部锚点微调与得分标题牌保持协调;运行态优先使用独立 `backButtonAsset` 透明 PNG 作为真实可点击按钮图,旧作品缺失该字段时才使用同尺寸 CSS 主题色圆形按钮兜底。上方成功跳跃次数 UI 复用拼图模板顶部 HUD 结构:`puzzle-runtime-header-card` 内包含陶泥儿 IP logo、居中的“得分”标题牌以及下挂 `puzzle-runtime-timer-card / puzzle-runtime-timer` 居中数字卡;数字卡展示成功跳跃次数而不是倒计时。游玩中不显示左下角“进行中”状态,也不在屏幕底部常驻排行榜。排行榜按作品维度展示玩家 ID、成功跳跃次数和游戏时长每位玩家只保留 1 条最佳记录,排序固定为 `成功跳跃次数 desc -> 游戏时长 asc -> 更新时间 asc`,并只在失败结算弹窗内展示,弹窗保留重开和返回动作。 每屏只展示 3 个地块:当前地块、目标地块和下一预览地块。平台流按同一 seed 无限生成,前端不得自行生成正式路径。运行态 HUD 顶部只保留返回按钮和成功跳跃次数不展示计时器或右上角重开按钮生成背景和游戏舞台必须覆盖整个运行态视口HUD 直接绝对定位压在背景上,不再用外层白底、居中窄栏、卡片边框或游戏区域圆角裁切背景。返回按钮固定在左上角安全区,交互热区固定为移动端 `56px`、桌面约 `62px`,不显示“返回”文字,并通过顶部锚点微调与得分标题牌保持协调;运行态优先使用独立 `backButtonAsset` 透明 PNG 作为真实可点击按钮图,旧作品缺失该字段时才使用同尺寸 CSS 主题色圆形按钮兜底。上方成功跳跃次数 UI 复用拼图模板顶部 HUD 结构:`puzzle-runtime-header-card` 内包含陶泥儿 IP logo、居中的“得分”标题牌以及下挂 `puzzle-runtime-timer-card / puzzle-runtime-timer` 居中数字卡;数字卡展示成功跳跃次数而不是倒计时。游玩中不显示左下角“进行中”状态,也不在屏幕底部常驻排行榜。排行榜按作品维度展示玩家 ID、成功跳跃次数和游戏时长每位玩家只保留 1 条最佳记录,排序固定为 `成功跳跃次数 desc -> 游戏时长 asc -> 更新时间 asc`,并只在失败结算弹窗内展示,弹窗保留重开和返回动作。
@@ -162,7 +164,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'``JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带地块图集和路径配置,必须先补读完整 work profile 再传入运行态。平台壳层必须同步注册 `jump-hop-workspace``jump-hop-generating``jump-hop-result``jump-hop-runtime``jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace``/creation/jump-hop/generating``/creation/jump-hop/result``/gallery/jump-hop/detail``/runtime/jump-hop`,同时持有 session、work、run、gallery、busy/error 与生成进度状态,避免只合入渲染分支但遗漏状态源或分享路径导致 typecheck 失败、刷新回首页。 平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'``JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带地块图集和路径配置,必须先补读完整 work profile 再传入运行态。平台壳层必须同步注册 `jump-hop-workspace``jump-hop-generating``jump-hop-result``jump-hop-runtime``jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace``/creation/jump-hop/generating``/creation/jump-hop/result``/gallery/jump-hop/detail``/runtime/jump-hop`,同时持有 session、work、run、gallery、busy/error 与生成进度状态,避免只合入渲染分支但遗漏状态源或分享路径导致 typecheck 失败、刷新回首页。
跳一跳作品架走创作中心的统一作品列表:前端通过 `/api/creation/jump-hop/works` 拉取作品摘要,草稿态会与 pending notice 合并后显示在作品架里,已发布作品点击后会先按 profileId 读取完整详情再进入详情或运行态。生成中作品仍以后端摘要里的 `generationStatus` 为准,刷新后应能恢复等待遮罩,不能只依赖内存 notice。 跳一跳作品架走创作中心的统一作品列表:前端通过 `/api/creation/jump-hop/works` 拉取作品摘要,草稿态会与 pending notice 合并后显示在作品架里,已完成但未发布草稿点击后必须通过私有创作接口 `GET /api/creation/jump-hop/works/{profile_id}` 读取完整详情并进入创作结果页;已发布作品点击后才通过公开运行态接口 `GET /api/runtime/jump-hop/works/{profile_id}` 读取完整详情再进入公开详情或运行态,该公开接口保持 published-only 校验。生成中作品仍以后端摘要里的 `generationStatus` 为准,刷新后应能恢复等待遮罩,不能只依赖内存 notice。
删除等破坏性动作当前未接入 jump-hop 删除 API如果后续要在作品架提供删除入口必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。 删除等破坏性动作当前未接入 jump-hop 删除 API如果后续要在作品架提供删除入口必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -222,6 +222,34 @@ pub async fn list_jump_hop_works(
)) ))
} }
pub async fn get_jump_hop_work_detail(
State(state): State<AppState>,
Path(profile_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &profile_id, "profileId")?;
let work = state
.spacetime_client()
.get_jump_hop_work_profile(
profile_id,
authenticated.claims().user_id().to_string(),
)
.await
.map_err(|error| {
jump_hop_error_response(
&request_context,
JUMP_HOP_CREATION_PROVIDER,
map_jump_hop_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
JumpHopWorkDetailResponse { item: work },
))
}
pub async fn get_jump_hop_runtime_work( pub async fn get_jump_hop_runtime_work(
State(state): State<AppState>, State(state): State<AppState>,
Path(profile_id): Path<String>, Path(profile_id): Path<String>,

View File

@@ -8,6 +8,7 @@ use crate::{
jump_hop::{ jump_hop::{
create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail, create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail,
get_jump_hop_leaderboard, get_jump_hop_runtime_work, get_jump_hop_session, get_jump_hop_leaderboard, get_jump_hop_runtime_work, get_jump_hop_session,
get_jump_hop_work_detail,
jump_hop_run_jump, list_jump_hop_gallery, list_jump_hop_works, publish_jump_hop_work, jump_hop_run_jump, list_jump_hop_gallery, list_jump_hop_works, publish_jump_hop_work,
restart_jump_hop_run, start_jump_hop_run, restart_jump_hop_run, start_jump_hop_run,
}, },
@@ -44,6 +45,13 @@ pub fn router(state: AppState) -> Router<AppState> {
require_bearer_auth, require_bearer_auth,
)), )),
) )
.route(
"/api/creation/jump-hop/works/{profile_id}",
get(get_jump_hop_work_detail).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route( .route(
"/api/creation/jump-hop/works/{profile_id}/publish", "/api/creation/jump-hop/works/{profile_id}/publish",
post(publish_jump_hop_work).route_layer(middleware::from_fn_with_state( post(publish_jump_hop_work).route_layer(middleware::from_fn_with_state(

View File

@@ -83,6 +83,24 @@ test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
); );
}); });
test('跳一跳结果页根容器允许移动端向下滚动到操作按钮', () => {
const { container } = render(
<JumpHopResultView
profile={buildProfile()}
onBack={() => {}}
onEdit={() => {}}
onStartTestRun={() => {}}
onPublish={() => {}}
onRegenerateTiles={() => {}}
/>,
);
const root = container.firstElementChild as HTMLElement;
expect(root.className).toContain('overflow-y-auto');
expect(root.className).toContain('overscroll-contain');
expect(root.className).toContain('safe-area-inset-bottom');
});
test('跳一跳草稿结果页不请求公开排行榜', () => { test('跳一跳草稿结果页不请求公开排行榜', () => {
render( render(
<JumpHopResultView <JumpHopResultView

View File

@@ -300,7 +300,7 @@ export function JumpHopResultView({
}; };
return ( return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4"> <div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-y-auto overscroll-contain px-3 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-3 sm:px-4 sm:pt-4">
<div className="mb-3 flex items-center justify-between gap-3"> <div className="mb-3 flex items-center justify-between gap-3">
<button <button
type="button" type="button"

View File

@@ -12878,12 +12878,18 @@ export function PlatformEntryFlowShellImpl({
} }
try { try {
const detail = await jumpHopClient.getWorkDetail(item.profileId); const detail = await jumpHopClient.getWorkDetail(item.profileId, {
audience: 'creation',
});
setJumpHopSession(null); setJumpHopSession(null);
setJumpHopRun(null); setJumpHopRun(null);
setJumpHopWork(detail.item); setJumpHopWork(detail.item);
setJumpHopRuntimeReturnStage('jump-hop-result'); setJumpHopRuntimeReturnStage('jump-hop-result');
enterCreateTab(); enterCreateTab();
pushAppHistoryPath('/creation/jump-hop/result');
writeCreationUrlState(
buildJumpHopCreationUrlState({ work: detail.item }),
);
setSelectionStage('jump-hop-result'); setSelectionStage('jump-hop-result');
} catch (error) { } catch (error) {
setJumpHopError( setJumpHopError(
@@ -14073,7 +14079,11 @@ export function PlatformEntryFlowShellImpl({
let work: JumpHopWorkProfileResponse | null = null; let work: JumpHopWorkProfileResponse | null = null;
try { try {
if (profileId) { if (profileId) {
work = (await jumpHopClient.getWorkDetail(profileId)).item; work = (
await jumpHopClient.getWorkDetail(profileId, {
audience: 'creation',
})
).item;
} }
} catch { } catch {
work = null; work = null;

View File

@@ -8055,10 +8055,84 @@ test('direct jump hop result route restores work detail by profile id', async ()
expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull(); expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith( expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
'jump-hop-profile-restore-1', 'jump-hop-profile-restore-1',
{ audience: 'creation' },
); );
expect(jumpHopClient.getSession).not.toHaveBeenCalled(); expect(jumpHopClient.getSession).not.toHaveBeenCalled();
}); });
test('completed unpublished jump hop draft opens result page without starting runtime', async () => {
const user = userEvent.setup();
const work = buildMockJumpHopWork({
summary: {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-draft-ready-1',
profileId: 'jump-hop-profile-draft-ready-1',
ownerUserId: 'user-1',
sourceSessionId: 'jump-hop-session-draft-ready-1',
themeText: '未发布跳一跳草稿',
workTitle: '未发布跳一跳草稿',
workDescription: '已经生成完成,但还没有发布。',
themeTags: ['草稿'],
difficulty: 'standard',
stylePreset: 'paper-toy',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-30T10:00:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
},
});
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({
items: [work.summary],
});
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
item: work,
} satisfies JumpHopWorkDetailResponse);
vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
...testCreationEntryConfig,
creationTypes: [
...testCreationEntryConfig.creationTypes,
{
id: 'jump-hop',
title: '跳一跳',
subtitle: '主题驱动平台跳跃',
badge: '可创建',
imageSrc: '/creation-type-references/jump-hop.webp',
visible: true,
open: true,
sortOrder: 55,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
],
});
render(<TestWrapper withAuth />);
await openDraftHub(user);
const draftPanel = getPlatformTabPanel('saves');
await user.click(
await within(draftPanel).findByRole('button', {
name: /继续创作《未发布跳一跳草稿》/u,
}),
);
expect(await screen.findByText('未发布跳一跳草稿')).toBeTruthy();
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
'jump-hop-profile-draft-ready-1',
{ audience: 'creation' },
);
expect(jumpHopClient.startRun).not.toHaveBeenCalled();
expect(window.location.pathname).toBe('/creation/jump-hop/result');
expect(window.location.search).toContain(
'profileId=jump-hop-profile-draft-ready-1',
);
});
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => { test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
const user = userEvent.setup(); const user = userEvent.setup();

View File

@@ -185,9 +185,16 @@ export function executeJumpHopCreationAction(
.then(normalizeJumpHopActionResponse); .then(normalizeJumpHopActionResponse);
} }
export async function getJumpHopWorkDetail(profileId: string) { export async function getJumpHopWorkDetail(
profileId: string,
options: { audience?: 'creation' | 'runtime' } = {},
) {
const base =
options.audience === 'creation'
? JUMP_HOP_WORKS_API_BASE
: `${JUMP_HOP_RUNTIME_API_BASE}/works`;
const response = await requestJson<JumpHopWorkDetailResponse>( const response = await requestJson<JumpHopWorkDetailResponse>(
`${JUMP_HOP_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}`, `${base}/${encodeURIComponent(profileId)}`,
{ method: 'GET' }, { method: 'GET' },
'读取跳一跳作品详情失败', '读取跳一跳作品详情失败',
); );

View File

@@ -529,6 +529,44 @@ describe('miniGameDraftGenerationProgress', () => {
]); ]);
}); });
test('jump hop generation anchors hide unused style preset fallback', () => {
const entries = buildJumpHopGenerationAnchorEntries({
sessionId: 'jump-hop-session-style-hidden',
ownerUserId: 'user-1',
status: 'generating',
draft: {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: 'jump-hop-profile-style-hidden',
themeText: '水果',
workTitle: '水果跳一跳',
workDescription: '水果主题跳一跳。',
themeTags: ['水果'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
characterPrompt: '内置默认 3D 角色',
tilePrompt: '',
endMoodPrompt: null,
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: null,
generationStatus: 'generating',
},
createdAt: '2026-06-06T10:00:00.000Z',
updatedAt: '2026-06-06T10:00:00.000Z',
});
expect(entries).toEqual([
{
id: 'jump-hop-theme',
label: '主题',
value: '水果',
},
]);
});
test('wooden fish draft generation exposes hit object, background and back button pipeline', () => { test('wooden fish draft generation exposes hit object, background and back button pipeline', () => {
const state = createMiniGameDraftGenerationState('wooden-fish'); const state = createMiniGameDraftGenerationState('wooden-fish');

View File

@@ -1074,7 +1074,7 @@ export function buildJumpHopGenerationAnchorEntries(
workTitle?: string; workTitle?: string;
themeText?: string; themeText?: string;
characterPrompt?: string; characterPrompt?: string;
stylePreset?: string; tilePrompt?: string;
} | null; } | null;
} }
| null | null
@@ -1098,7 +1098,7 @@ export function buildJumpHopGenerationAnchorEntries(
value: value:
formPayload?.tilePrompt?.trim() || formPayload?.tilePrompt?.trim() ||
config?.tilePrompt?.trim() || config?.tilePrompt?.trim() ||
draft?.stylePreset?.trim() || draft?.tilePrompt?.trim() ||
'', '',
}, },
]; ];