fix: refine jump hop draft detail flow
This commit is contained in:
@@ -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` 透明 PNG,OSS 槽位固定为 `back-button/image.png`,提示词要求标准圆形、主题色材质包装、居中左箭头、纯绿色 key 背景,后端去绿后写入作品 profile;
|
4. 背景底图同样由 image2 生成,复用现有 `coverComposite` / `coverImageSrc` 作为运行态背景读写字段,OSS 槽位固定为 `background/image.png`;提示词必须严格以用户主题关键词为背景主题,结构以左右两侧氛围为主,中央纵轴 1/2 区域保持少元素、简洁、可读且有纵深感,两侧允许更强立体层次和行进感;背景只作为底图,禁止生成跳板、地块、落脚物、角色、UI、返回按钮、文字、路径箭头或海报排版;左上角返回按钮不允许画进背景,而是单独生成 `backButtonAsset` 透明 PNG,OSS 槽位固定为 `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 |
@@ -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>,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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' },
|
||||||
'读取跳一跳作品详情失败',
|
'读取跳一跳作品详情失败',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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() ||
|
||||||
'',
|
'',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user