From e410f7974ee60cd6806dc2ba7ee2aa7f26dd175a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Fri, 8 May 2026 21:46:11 +0800 Subject: [PATCH] 1 --- .hermes/shared-memory/pitfalls.md | 8 + ..._DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md | 15 +- ...ADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md | 24 +- server-rs/Cargo.lock | 1 - server-rs/crates/api-server/Cargo.toml | 1 - .../api-server/src/volcengine_speech.rs | 21 +- server-rs/crates/platform-speech/src/lib.rs | 2 + .../PlatformEntryFlowShellImpl.tsx | 76 +++- .../PuzzleRuntimeShell.test.tsx | 32 +- .../puzzle-runtime/PuzzleRuntimeShell.tsx | 299 ++++++------ .../RpgEntryHomeView.recharge.test.tsx | 115 +++-- src/components/rpg-entry/RpgEntryHomeView.tsx | 165 +++---- src/index.css | 424 +++++++++++++----- 13 files changed, 757 insertions(+), 426 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 6c86f356..90fc46a9 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -120,6 +120,14 @@ - 验证:`npm run api-server` 后 `/healthz` 返回 200,相关路由冒烟通过。 - 关联:`server-rs/crates/api-server/src/main.rs`、`server-rs/crates/api-server/src/app.rs`。 +## Windows api-server.exe 锁文件与强杀退出码容易混淆 + +- 现象:`cargo run -p api-server` 或 `npm run api-server` 报 `failed to remove file ... target\debug\api-server.exe`;清理旧进程后,旧终端可能继续打印 `process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)`。 +- 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条 `npm run api-server` 链路仍在运行,进程树为 `npm run api-server -> node scripts/api-server-dev.mjs -> cargo run -> api-server.exe`。`0xffffffff` 常见于排障时用 `Stop-Process -Force` 强制结束旧 `api-server.exe` 后由 Cargo 回显,不一定代表新启动失败。 +- 处理:先按目标路径确认并停止本仓库的旧 `api-server.exe` 及其父级 `cargo/node/cmd` 启动链路,再重新启动;不要同时开多个 `npm run api-server`。 +- 验证:确认没有匹配 `C:\Genarrative\server-rs\target\debug\api-server.exe` 的进程后,`Remove-Item` 能删除旧 exe;随后 `npm run api-server` 启动并访问 `/healthz` 返回 200。 +- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/main.rs`。 + ## Windows debug 长 SSE Future 触发 api-server 断连 - 现象:前端 Vite 代理请求 `/api/runtime/creative-agent/sessions/{sessionId}/messages/stream` 报 `read ECONNRESET`,随后 `api-server.exe` 以 `0xffffffff` 退出,`dev:rust` 回收 SpacetimeDB、Vite 和后台 Vite。 diff --git a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md index 87807c71..bc63c74e 100644 --- a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md +++ b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md @@ -31,10 +31,13 @@ - 数据来源沿用 `featuredEntries + latestEntries` 去重后的公开作品列表。 - 首个可运行作品自动进入推荐页内嵌运行态,主视口不再展示作品封面卡。 -- 主视口占据顶部栏与底部作品区之间的主要空间,保持黑色运行容器、圆角边界和短加载态,直接承载作品启动后的玩法画面。 +- 主视口占据顶部栏与作品信息区之间的主要空间,使用平台主题 token 控制运行容器背景、边框、阴影、加载态文字和错误态按钮;亮色主题不得残留纯黑底白字加载块。 - 主视口下方展示当前作品的游玩、点赞、评论/改造等紧凑指标、作者头像、作者名与作品名,不写规则说明类文案。 -- 底部作品区使用横向滑动切换器,条目只展示作品名、类型和核心指标;点击或滑动到其他作品时切换上方运行内容。 -- 点击作品元信息仍可进入既有详情页,点赞、改造、复制作品号等完整操作继续收敛在详情页。 +- 作品信息区不再提供详情箭头或点击详情入口;点击该区域无效,上滑切换下一个推荐作品,下滑切换上一个推荐作品。 +- 用户停留在推荐页时,底部当前 Tab 从“推荐”切换为“下一个”,图标使用向下的倒三角 / 双下箭头语义,点击后切换下一个推荐作品。 +- 推荐页不再展示额外的底部作品切换块;当前作品的完整操作继续收敛在详情页和作品自身运行态中。 +- 推荐页嵌入运行只调整平台外壳容器、主题注入和玩法壳层配色,不改写作品数据、关卡设定、道具设定或图片资产。 +- 点赞、改造、复制作品号等完整操作继续收敛在详情页,详情入口由作品自身运行态或其它广场列表承接,推荐页作品信息区只负责展示和上下滑切换。 - 无数据、加载中、启动失败和暂不支持内嵌运行的作品沿用短状态文案。 桌面端仍保持现有首页布局,只把一级导航文案从“首页”改为“推荐”。 @@ -74,14 +77,16 @@ ## 7. 验收 -1. 移动端底部导航显示“推荐 / 发现 / 创作 / 草稿 / 我的”,未登录时显示“推荐 / 创作 / 发现”。 +1. 移动端底部导航非推荐页显示“推荐 / 发现 / 创作 / 草稿 / 我的”,未登录时显示“推荐 / 创作 / 发现”;停留在推荐页时当前 Tab 显示“下一个”。 2. 点击“推荐”直接看到公开作品启动后的内容,不再先看到搜索框、频道 Tab 或封面卡流。 3. 点击“发现”可看到搜索、推荐、今日、分类、排行子 Tab。 4. 点击“草稿”看到原创作页作品列表。 5. 点击“创作”只看到新建创作入口。 6. “我的”里的“玩过”弹层包含原存档列表入口,点击存档能继续恢复。 7. 移动端底部导航为悬浮胶囊样式,保留当前明暗主题色变量,不新增第三套主题。 -8. 推荐页底部作品区可横向滑动并切换作品,切换后上方运行视口同步进入对应作品内容。 +8. 推荐页不出现额外底部作品卡或横滑切换块,运行视口、加载态和错误态跟随当前明暗主题。 +9. 拼图玩法背景、HUD、按钮、弹窗、排行榜和相似作品卡跟随平台主题色;暗色主题仍保留深色游戏感,亮色主题不得出现大面积固定黑底。 +10. 推荐页作品信息区点击无效,上滑切下一个、下滑切上一个;点击底部“下一个”也切下一个作品。 ## 8. 2026-05-07 未登录三栏补充 diff --git a/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md b/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md index a062a099..53b34e35 100644 --- a/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md +++ b/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md @@ -17,9 +17,8 @@ 1. 左侧保留返回按钮。 2. 中间居中展示关卡主信息: - - 第一行:拼图关卡名 - - 第二行:作者昵称 - - 第三行:`第 N 关` + - 第一行:`第 N 关` 与拼图关卡名,二者保持同一行。 + - 第二行:紧凑倒计时。 3. 右侧新增设置按钮。 同时移除以下冗余标识: @@ -31,12 +30,27 @@ ### 1.1 2026-04-30 顶栏与底部工具补充 -1. 顶栏作者信息不再只显示一行作者名,必须展示为作者头像与昵称组合;当前运行态只提供昵称时,用昵称首字生成圆形占位头像。 -2. 倒计时组件提升为顶栏中的强信息,字号、内边距和图标尺寸都需要明显大于作者昵称与关卡序号。 +1. 历史口径曾要求顶栏展示作者头像与昵称;该要求已被 2026-05-08 精简口径替代,当前拼图棋盘 HUD 不再展示作者信息。 +2. 倒计时组件保持为顶栏中的强信息,但需要采用紧凑尺寸,不得遮挡棋盘内容。 3. 底部只保留 `提示 / 原图 / 冻结` 三个功能按钮,并整体居中展示;三个按钮触控面积和图标字号都需要放大。 4. 底部不再展示“等待下一关候选”这类状态占位。通关后在三个道具按钮上方固定展示“下一关”按钮,展示条件只依赖当前关卡已通关,不依赖 `recommendedNextProfileId` 是否已有值。 5. 点击底部“下一关”按钮继续调用运行时壳层已有 `onAdvanceNextLevel` 事件;正式 run 由后端 `next-level` 选择候选,本地 run 由 `local-next-level` 生成或接续下一关,前端不在按钮层自行决定下一关来源。 +### 1.2 2026-05-08 推荐页嵌入态 HUD 精简 + +1. 拼图运行时顶栏不再展示作者头像、作者昵称或作者首字占位,作者信息只在推荐页作品信息、详情页和排行榜等非棋盘 HUD 区域展示。 +2. 顶部主信息压缩为两行:第一行 `第 N 关 + 关卡名`,第二行小号倒计时;倒计时不能使用此前的大号胶囊尺寸。 +3. 返回按钮和设置按钮上移并缩小移动端基础尺寸,减少对棋盘顶部空间的占用。 +4. 棋盘容器必须保留固定顶部安全区,确保关卡名和倒计时不会遮挡拼图内容。 +5. 推荐页嵌入时只调整外壳间距和 HUD 布局,不改写作品关卡、作者、道具、时间限制或图片资产等作品设定。 + +### 1.3 2026-05-08 主题色联动 + +1. 拼图运行态根容器、背景、棋盘底色、顶部 HUD、底部工具栏、加载态、失败弹窗、通关弹窗、道具确认弹窗、设置弹窗和相似作品卡必须通过 `src/index.css` 中的 `--puzzle-runtime-*` 主题变量控制。 +2. `platform-theme--light` 下拼图玩法背景应使用浅色平台底色与粉橙主色点缀,文字使用平台正文 token;不得继续使用固定 `bg-slate-950 text-white` 作为大面积底色。 +3. `platform-theme--dark` 下可保留深色棋盘氛围,但按钮、选中态、倒计时、弹窗边框和推荐页加载态仍要从主题 token 取色,避免局部色板漂移。 +4. 推荐页内嵌拼图时,父级必须保持 `platform-theme` 类可传递到 `PuzzleRuntimeShell`,不能让 runtime 脱离平台主题变量。 + ### 2. 拼图块显示规则 运行时单块右下角编号全部移除。 diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index c809ecda..ec124622 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -119,7 +119,6 @@ dependencies = [ "time", "tokio", "tokio-stream", - "tokio-tungstenite 0.27.0", "tower", "tower-http", "tracing", diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index 117b95b0..6dc372f8 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -42,7 +42,6 @@ shared-logging = { workspace = true } spacetime-client = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time"] } tokio-stream = { workspace = true } -tokio-tungstenite = { workspace = true } futures-util = { workspace = true } time = { workspace = true, features = ["formatting"] } tower-http = { workspace = true, features = ["trace"] } diff --git a/server-rs/crates/api-server/src/volcengine_speech.rs b/server-rs/crates/api-server/src/volcengine_speech.rs index cb631dcb..e6a5f746 100644 --- a/server-rs/crates/api-server/src/volcengine_speech.rs +++ b/server-rs/crates/api-server/src/volcengine_speech.rs @@ -6,18 +6,17 @@ use axum::{ ws::{Message as ClientWsMessage, WebSocket, WebSocketUpgrade}, }, http::{HeaderValue, StatusCode, header}, - response::{IntoResponse, Response}, + response::Response, }; use futures_util::{SinkExt, StreamExt, TryStreamExt}; use platform_speech::{ AsrAudioConfig, AsrFrameKind, PublicSpeechConfig, PublicSpeechEndpoints, SpeechError, - TtsAudioParams, TtsBidirectionClientEvent, TtsSseRequest, VolcengineSpeechClient, - VolcengineSpeechConfig, build_asr_frame, build_asr_full_client_request, + TtsAudioParams, TtsBidirectionClientEvent, TtsSseRequest, UpstreamWsError, UpstreamWsMessage, + VolcengineSpeechClient, VolcengineSpeechConfig, build_asr_frame, build_asr_full_client_request, build_tts_bidirection_frame_from_client_event, default_asr_request_payload, parse_asr_response_frame, parse_tts_response_frame, tts_response_to_client_value, }; use serde_json::{Value, json}; -use tokio_tungstenite::tungstenite::Message as UpstreamWsMessage; use tracing::{info, warn}; use crate::{ @@ -249,12 +248,12 @@ async fn proxy_asr_websocket(socket: WebSocket, client: VolcengineSpeechClient, } Ok(UpstreamWsMessage::Text(text)) => { browser_sender - .send(ClientWsMessage::Text(text)) + .send(ClientWsMessage::Text(text.to_string().into())) .await .map_err(map_client_ws_send_error)?; } - Ok(UpstreamWsMessage::Close(close)) => { - let _ = browser_sender.send(ClientWsMessage::Close(close)).await; + Ok(UpstreamWsMessage::Close(_)) => { + let _ = browser_sender.send(ClientWsMessage::Close(None)).await; break; } Ok(UpstreamWsMessage::Ping(bytes)) => { @@ -363,12 +362,12 @@ async fn proxy_tts_bidirection_websocket(socket: WebSocket, client: VolcengineSp } Ok(UpstreamWsMessage::Text(text)) => { browser_sender - .send(ClientWsMessage::Text(text)) + .send(ClientWsMessage::Text(text.to_string().into())) .await .map_err(map_client_ws_send_error)?; } - Ok(UpstreamWsMessage::Close(close)) => { - let _ = browser_sender.send(ClientWsMessage::Close(close)).await; + Ok(UpstreamWsMessage::Close(_)) => { + let _ = browser_sender.send(ClientWsMessage::Close(None)).await; break; } Ok(UpstreamWsMessage::Ping(bytes)) => { @@ -451,7 +450,7 @@ fn map_speech_error(error: SpeechError) -> AppError { } } -fn map_ws_send_error(error: tokio_tungstenite::tungstenite::Error) -> SpeechError { +fn map_ws_send_error(error: UpstreamWsError) -> SpeechError { SpeechError::Upstream(format!("发送火山语音 WebSocket 帧失败:{error}")) } diff --git a/server-rs/crates/platform-speech/src/lib.rs b/server-rs/crates/platform-speech/src/lib.rs index 2904554e..e907f194 100644 --- a/server-rs/crates/platform-speech/src/lib.rs +++ b/server-rs/crates/platform-speech/src/lib.rs @@ -17,6 +17,8 @@ use tokio_tungstenite::{ }; use uuid::Uuid; +pub use tokio_tungstenite::tungstenite::{Error as UpstreamWsError, Message as UpstreamWsMessage}; + pub const DEFAULT_ASR_WS_URL: &str = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"; pub const DEFAULT_TTS_BIDIRECTION_WS_URL: &str = "wss://openspeech.bytedance.com/api/v3/tts/bidirection"; diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 0a980202..dac4e955 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -4699,6 +4699,31 @@ export function PlatformEntryFlowShellImpl({ ], ); + const saveAndExitRecommendPuzzleRuntime = useCallback(async () => { + if (activeRecommendRuntimeKind !== 'puzzle') { + return; + } + + const currentRun = puzzleRunRef.current; + if (!currentRun) { + setActiveRecommendRuntimeKind(null); + return; + } + + // 中文注释:推荐页嵌入拼图的“保存并退出”沿用现有运行态语义; + // 正式 run 的每次交换/拖动/通关已写回后端,退出时只收口暂停和本地快照。 + const closedRun = currentRun.currentLevel + ? setLocalPuzzlePaused(currentRun, false) + : currentRun; + puzzleRunRef.current = null; + setPuzzleRun(null); + setActiveRecommendRuntimeKind(null); + + if (closedRun.currentLevel) { + setPuzzleError(null); + } + }, [activeRecommendRuntimeKind, setPuzzleError]); + const leaveAgentWorkspace = useCallback(() => { enterCreateTab(); sessionController.resetSessionViewState(); @@ -5952,6 +5977,9 @@ export function PlatformEntryFlowShellImpl({ async (entry: PlatformPublicGalleryCard) => { const entryKey = getPlatformPublicGalleryEntryKey(entry); const runtimeKind = getPlatformRecommendRuntimeKind(entry); + if (entryKey !== activeRecommendEntryKey) { + await saveAndExitRecommendPuzzleRuntime(); + } setActiveRecommendEntryKey(entryKey); setActiveRecommendRuntimeKind(runtimeKind); setActiveRecommendRuntimeError(null); @@ -6025,6 +6053,8 @@ export function PlatformEntryFlowShellImpl({ } }, [ + activeRecommendEntryKey, + saveAndExitRecommendPuzzleRuntime, selectedPuzzleDetail, setBigFishError, setMatch3DError, @@ -6037,6 +6067,38 @@ export function PlatformEntryFlowShellImpl({ startVisualNovelRunFromProfile, ], ); + const selectAdjacentRecommendRuntimeEntry = useCallback( + (direction: 1 | -1) => { + if (recommendRuntimeEntries.length === 0) { + return; + } + + const activeIndex = recommendRuntimeEntries.findIndex( + (entry) => + getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey, + ); + const baseIndex = activeIndex >= 0 ? activeIndex : 0; + const nextIndex = + (baseIndex + direction + recommendRuntimeEntries.length) % + recommendRuntimeEntries.length; + const nextEntry = recommendRuntimeEntries[nextIndex]; + if (!nextEntry) { + return; + } + if ( + getPlatformPublicGalleryEntryKey(nextEntry) === activeRecommendEntryKey + ) { + return; + } + + void selectRecommendRuntimeEntry(nextEntry); + }, + [ + activeRecommendEntryKey, + recommendRuntimeEntries, + selectRecommendRuntimeEntry, + ], + ); const recommendRuntimeContent = useMemo(() => { if ( @@ -6153,6 +6215,8 @@ export function PlatformEntryFlowShellImpl({ } error={puzzleError} embedded + hideBackButton + hideExitControls onBack={() => { setActiveRecommendRuntimeKind(null); }} @@ -6270,7 +6334,7 @@ export function PlatformEntryFlowShellImpl({ } return ( -
+
正在读取世界
); @@ -6291,6 +6355,7 @@ export function PlatformEntryFlowShellImpl({ match3dError, match3dFlow, match3dRun, + platformThemeClass, puzzleError, puzzleRun, recommendRuntimeEntries, @@ -7357,9 +7422,12 @@ export function PlatformEntryFlowShellImpl({ isVisualNovelBusy } recommendRuntimeError={activeRecommendRuntimeError} - onSelectRecommendEntry={(entry) => { - void selectRecommendRuntimeEntry(entry); - }} + onSelectNextRecommendEntry={() => + selectAdjacentRecommendRuntimeEntry(1) + } + onSelectPreviousRecommendEntry={() => + selectAdjacentRecommendRuntimeEntry(-1) + } onOpenLibraryDetail={(entry) => { runProtectedAction(() => { void detailNavigation.openLibraryDetail(entry); diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx index ba03cdbb..fa2a546f 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx @@ -240,7 +240,7 @@ test('首次退出引导的作品改造按钮进入改造流程', () => { expect(screen.queryByRole('dialog')).toBeNull(); }); -test('顶部作者显示头像昵称,底部功能居中放大且不显示等待候选', () => { +test('顶部不显示作者,关卡标题和倒计时更紧凑', () => { const runWithoutNext: PuzzleRunSnapshot = { ...clearedRun, recommendedNextProfileId: null, @@ -256,15 +256,17 @@ test('顶部作者显示头像昵称,底部功能居中放大且不显示等 />, ); - const avatar = screen.getByText('测'); const timer = screen.getByText('4:48'); const hintButton = screen.getByRole('button', { name: '提示' }); const referenceButton = screen.getByRole('button', { name: '原图' }); const freezeButton = screen.getByRole('button', { name: '冻结' }); - expect(avatar.className).toContain('rounded-full'); - expect(screen.getByText('测试作者')).toBeTruthy(); - expect(timer.className).toContain('text-2xl'); + expect(screen.queryByText('测试作者')).toBeNull(); + expect(screen.getByText('第 1 关')).toBeTruthy(); + expect(screen.getByText('潮雾拼图')).toBeTruthy(); + expect(timer.className).toContain('puzzle-runtime-timer'); + expect(timer.className).toContain('text-lg'); + expect(timer.className).not.toContain('text-2xl'); expect(hintButton.className).toContain('h-16'); expect(referenceButton.className).toContain('h-16'); expect(freezeButton.className).toContain('h-16'); @@ -467,14 +469,32 @@ test('拼图棋盘使用贴近移动端边缘的正方形舞台承载切块', () ); const board = screen.getByTestId('puzzle-board'); + expect(board.className).toContain('puzzle-runtime-board'); expect(board.className).toContain('aspect-square'); - expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_16.5rem))]'); + expect(board.className).toContain('max-w-[min(99vw,calc(100vh_-_14rem))]'); expect(board.className).not.toContain('aspect-video'); expect(board.className).not.toContain('aspect-[9/16]'); expect(board.getAttribute('style')).toContain('grid-template-rows'); expect(container.querySelector('.min-h-\\[4\\.5rem\\]')).toBeNull(); }); +test('拼图运行态主体使用主题语义类承接明暗主题', () => { + const { container } = renderPuzzleRuntime( + , + ); + + expect(container.firstElementChild?.className).toContain( + 'puzzle-runtime-shell', + ); + expect(container.querySelector('.puzzle-runtime-pill')).toBeTruthy(); +}); + test('合并块按实际拼块外轮廓描边', () => { const mergedRun: PuzzleRunSnapshot = { ...clearedRun, diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx index f0aeb93f..74c2f038 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx @@ -41,6 +41,7 @@ type PuzzleRuntimeShellProps = { isBusy?: boolean; error?: string | null; hideBackButton?: boolean; + hideExitControls?: boolean; embedded?: boolean; onBack: () => void; onRemodelWork?: (profileId: string) => void | Promise; @@ -157,10 +158,6 @@ function formatTimerMs(value: number | null | undefined) { return `${minutes}:${seconds.toString().padStart(2, '0')}`; } -function resolveAuthorAvatarLabel(authorDisplayName: string) { - return authorDisplayName.trim().slice(0, 1) || '玩'; -} - function resolveActiveFreezeElapsedMs( level: PuzzleRuntimeLevelSnapshot, nowMs: number, @@ -309,6 +306,7 @@ export function PuzzleRuntimeShell({ isBusy = false, error = null, hideBackButton = false, + hideExitControls = false, embedded = false, onBack, onRemodelWork, @@ -790,9 +788,9 @@ export function PuzzleRuntimeShell({ if (!run || !currentLevel || !board) { return (
-
+
正在进入拼图关卡
@@ -963,9 +961,6 @@ export function PuzzleRuntimeShell({ const canShowNextAction = canAdvanceDefaultNextLevel || hasSimilarWorkChoices; const levelLabel = `第 ${currentLevel.levelIndex} 关`; - const authorAvatarLabel = resolveAuthorAvatarLabel( - currentLevel.authorDisplayName, - ); const exitPromptProfileId = currentLevel.profileId.trim(); const leaderboardEntries = (currentLevel.leaderboardEntries ?? []).length > 0 @@ -979,6 +974,10 @@ export function PuzzleRuntimeShell({ isBusy || runtimeStatus !== 'playing' || Boolean(propDialog); const handleBackRequest = () => { + if (hideExitControls) { + return; + } + if ( onRemodelWork && exitPromptProfileId && @@ -1069,7 +1068,10 @@ export function PuzzleRuntimeShell({ if (propKind === 'freezeTime') { // 中文注释:正式 run 可能在冻结确认期间已被服务端结算为失败态; // 这种边界同步只关闭确认窗,不再播放冻结成功反馈。 - const resultLevel = (useResult ?? null)?.currentLevel ?? currentLevelRef.current; + const resultLevel = + useResult && typeof useResult === 'object' + ? useResult.currentLevel + : currentLevelRef.current; if (resultLevel?.status === 'playing') { setIsFreezeEffectVisible(true); window.setTimeout(() => { @@ -1084,9 +1086,9 @@ export function PuzzleRuntimeShell({ return (
-
+
{currentLevel.coverImageSrc ? ( ) : null} -
+
-
-
+
+
-
-
- {currentLevel.levelName} -
-
- - {formatTimerMs(displayRemainingMs)} -
-
- - - {currentLevel.authorDisplayName} - - +
+
+ {levelLabel} + + {currentLevel.levelName} + +
+
+ + {formatTimerMs(displayRemainingMs)}
@@ -1146,21 +1139,21 @@ export function PuzzleRuntimeShell({ onClick={() => setIsSettingsPanelOpen(true)} aria-label="打开拼图设置" title="打开拼图设置" - className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-200/60" + className="puzzle-runtime-icon-button inline-flex h-10 w-10 items-center justify-center rounded-full transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-button-primary-border)] sm:h-11 sm:w-11" >
-
+
)} -
+
) : ( '' @@ -1467,12 +1460,12 @@ export function PuzzleRuntimeShell({
{error ? ( -
+
{error}
) : null} {selectedPieceId && runtimeStatus === 'playing' ? ( -
+
已选择
) : null} @@ -1491,21 +1484,21 @@ export function PuzzleRuntimeShell({ levelId: run.nextLevelId ?? null, }); }} - className="inline-flex min-h-11 items-center gap-2 rounded-full bg-amber-200 px-5 py-2.5 text-sm font-bold text-slate-950 shadow-[0_14px_36px_rgba(251,191,36,0.26)] transition hover:bg-amber-100 disabled:opacity-45" + className="puzzle-runtime-primary-button inline-flex min-h-11 items-center gap-2 rounded-full px-5 py-2.5 text-sm font-bold transition hover:brightness-105 disabled:opacity-45" > {hasSimilarWorkChoices ? '换个作品' : '下一关'} ) : null} -
+
@@ -1565,7 +1558,7 @@ export function PuzzleRuntimeShell({ {propDialog ? (
{ if (!isPropConfirming) { setPropDialog(null); @@ -1576,35 +1569,35 @@ export function PuzzleRuntimeShell({ role="dialog" aria-modal="true" aria-labelledby="puzzle-prop-confirm-title" - className="pixel-nine-slice pixel-modal-shell w-full max-w-[22rem] overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]" + className="puzzle-runtime-dialog pixel-nine-slice pixel-modal-shell w-full max-w-[22rem] overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.24)]" style={getNineSliceStyle(UI_CHROME.modalPanel)} onClick={(event) => event.stopPropagation()} > -
- +
+

{propDialog.title}

-
+
消耗 1 光点 {propConfirmError ? ( -
+
{propConfirmError}
) : null}
-