diff --git a/docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md b/docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md new file mode 100644 index 00000000..e0a01d67 --- /dev/null +++ b/docs/technical/PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md @@ -0,0 +1,37 @@ +# Profile 主链 Vite 代理修复 + +## 1. 问题 + +“我的”和“存档”页面在本地开发环境报: + +```text +Unexpected token '<', "'; ### `profile_recharge_order` -- 作用:充值订单表,记录用户购买叙世币或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。 +- 作用:充值订单表,记录用户购买光点或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。 - 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Timestamp`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option`。 - 索引:`user_id`, `(user_id, created_at)`。 diff --git a/docs/technical/WORK_PUBLISH_SHARE_PANEL_2026-05-02.md b/docs/technical/WORK_PUBLISH_SHARE_PANEL_2026-05-02.md index 16aedd54..0831967f 100644 --- a/docs/technical/WORK_PUBLISH_SHARE_PANEL_2026-05-02.md +++ b/docs/technical/WORK_PUBLISH_SHARE_PANEL_2026-05-02.md @@ -12,7 +12,7 @@ 2. 标题下显示可复制的分享文本。 3. 分享文本下方显示主按钮“分享”,点击后复制完整分享文本。 4. 页面底部显示三个分享渠道 icon:微信、QQ、抖音。 -5. 移动端使用底部弹层,桌面端居中展示,复用 `UnifiedModal` 的平台弹窗外壳。 +5. 移动端与桌面端都使用居中独立面板,复用 `UnifiedModal` 的平台弹窗外壳。 ## 分享文本 @@ -39,6 +39,10 @@ 仓库现有 `media/social-media-group/wechat.png` 与 `qq.png` 是社群二维码,不作为本面板渠道 icon 使用。渠道 icon 采用轻量圆形文字标识,避免误导用户进入社群。 +## 面板样式约束 + +分享面板通过 `UnifiedModal` portal 挂载到页面根部时,需要在遮罩层补齐当前平台主题类,避免主题变量脱离页面容器后丢失。面板外壳继续使用 `platform-modal-shell` 的 `--platform-modal-fill` 背景,并在移动端覆盖平台弹窗默认底部抽屉布局,保持居中显示。 + ## 接入范围 - `RpgCreationResultActionBar`:RPG 发布成功后由父层回传分享数据并打开面板。 diff --git a/packages/shared/src/contracts/puzzleRuntimeSession.ts b/packages/shared/src/contracts/puzzleRuntimeSession.ts index 09433a83..d64f8fa6 100644 --- a/packages/shared/src/contracts/puzzleRuntimeSession.ts +++ b/packages/shared/src/contracts/puzzleRuntimeSession.ts @@ -124,6 +124,10 @@ export interface DragPuzzlePieceRequest { targetCol: number; } +export interface AdvancePuzzleNextLevelRequest { + targetProfileId?: string | null; +} + export interface UsePuzzleRuntimePropRequest { propKind: PuzzleRuntimePropKind; } diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index a5e28c08..8df96204 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -36,11 +36,12 @@ use shared_contracts::{ }, puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse}, puzzle_runtime::{ - DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse, - PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse, - PuzzleRecommendedNextWorkResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse, - PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, - SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest, + AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, + PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, + PuzzlePieceStateResponse, PuzzleRecommendedNextWorkResponse, PuzzleRunResponse, + PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, + SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, + UsePuzzleRuntimePropRequest, }, puzzle_works::{ PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse, @@ -1367,14 +1368,34 @@ pub async fn advance_puzzle_next_level( AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, + payload: Result, JsonRejection>, ) -> Result, Response> { ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?; + let payload = match payload { + Ok(Json(payload)) => payload, + Err(error) if error.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE => { + AdvancePuzzleNextLevelRequest { + target_profile_id: None, + } + } + Err(error) => { + return Err(puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": error.body_text(), + })), + )); + } + }; let run = state .spacetime_client() .advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput { run_id, owner_user_id: authenticated.claims().user_id().to_string(), + target_profile_id: payload.target_profile_id, advanced_at_micros: current_utc_micros(), }) .await diff --git a/server-rs/crates/module-auth/src/domain.rs b/server-rs/crates/module-auth/src/domain.rs index f1a4f599..188c0389 100644 --- a/server-rs/crates/module-auth/src/domain.rs +++ b/server-rs/crates/module-auth/src/domain.rs @@ -227,7 +227,7 @@ pub fn build_system_username(prefix: &str, sequence: u64) -> String { format!("{prefix}_{sequence:08}") } -// 公开叙世号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。 +// 公开百梦号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。 pub fn build_public_user_code(sequence: u64) -> String { format!("SY-{sequence:08}") } diff --git a/server-rs/crates/module-auth/src/errors.rs b/server-rs/crates/module-auth/src/errors.rs index 29541f18..22c39527 100644 --- a/server-rs/crates/module-auth/src/errors.rs +++ b/server-rs/crates/module-auth/src/errors.rs @@ -67,7 +67,7 @@ impl fmt::Display for PasswordEntryError { Self::InvalidDisplayName => f.write_str("昵称格式不正确"), Self::InvalidAvatarDataUrl => f.write_str("头像图片格式不正确"), Self::EmptyProfileUpdate => f.write_str("请至少修改昵称或头像"), - Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"), + Self::InvalidPublicUserCode => f.write_str("百梦号格式不正确"), Self::InvalidCredentials => f.write_str("手机号或密码错误"), Self::UserNotFound => f.write_str("用户不存在"), Self::Store(message) | Self::PasswordHash(message) => f.write_str(message), diff --git a/server-rs/crates/module-puzzle/src/application.rs b/server-rs/crates/module-puzzle/src/application.rs index 02140232..2793687f 100644 --- a/server-rs/crates/module-puzzle/src/application.rs +++ b/server-rs/crates/module-puzzle/src/application.rs @@ -1373,8 +1373,8 @@ pub fn advance_to_new_work_first_level_at( return Err(PuzzleFieldError::InvalidOperation); } - // 中文注释:跨作品代表进入一个新作品,关卡序号、切割规格和倒计时都从第 1 关重新开始。 - let next_level_index = 1; + // 中文注释:跨作品只切换到候选作品的第一张图,运行时关卡序号和难度循环继续累进。 + let next_level_index = run.current_level_index + 1; let level_config = resolve_puzzle_level_config(next_level_index); let grid_size = level_config.grid_size; let shuffle_seed = puzzle_shuffle_seed( @@ -1391,8 +1391,8 @@ pub fn advance_to_new_work_first_level_at( Ok(PuzzleRunSnapshot { run_id: run.run_id.clone(), - entry_profile_id: next_profile.profile_id.clone(), - cleared_level_count: 0, + entry_profile_id: run.entry_profile_id.clone(), + cleared_level_count: run.cleared_level_count, current_level_index: next_level_index, current_grid_size: grid_size, played_profile_ids, @@ -2998,7 +2998,7 @@ mod tests { } #[test] - fn advance_to_new_work_first_level_restarts_level_progress() { + fn advance_to_new_work_first_profile_level_keeps_runtime_progress() { let first_profile = build_published_profile("entry", "owner-a", vec!["奇幻", "遗迹"]); let next_profile = build_published_profile("next", "owner-b", vec!["奇幻", "魔法"]); let mut run = start_run("run-cross-work".to_string(), &first_profile, 2).expect("run"); @@ -3011,14 +3011,14 @@ mod tests { let next_run = advance_to_new_work_first_level_at(&run, &next_profile, 3_000).expect("next run"); - assert_eq!(next_run.entry_profile_id, "next"); - assert_eq!(next_run.cleared_level_count, 0); - assert_eq!(next_run.current_level_index, 1); + assert_eq!(next_run.entry_profile_id, "entry"); + assert_eq!(next_run.cleared_level_count, 3); + assert_eq!(next_run.current_level_index, 4); let next_level = next_run.current_level.expect("next level"); assert_eq!(next_level.profile_id, "next"); - assert_eq!(next_level.level_index, 1); - assert_eq!(next_level.grid_size, 3); - assert_eq!(next_level.time_limit_ms, 300_000); + assert_eq!(next_level.level_index, 4); + assert_eq!(next_level.grid_size, 5); + assert_eq!(next_level.time_limit_ms, 210_000); } #[test] diff --git a/server-rs/crates/module-puzzle/src/commands.rs b/server-rs/crates/module-puzzle/src/commands.rs index aefb3b15..0072a59a 100644 --- a/server-rs/crates/module-puzzle/src/commands.rs +++ b/server-rs/crates/module-puzzle/src/commands.rs @@ -213,6 +213,8 @@ pub struct PuzzleRunDragInput { pub struct PuzzleRunNextLevelInput { pub run_id: String, pub owner_user_id: String, + #[serde(default)] + pub target_profile_id: Option, pub advanced_at_micros: i64, } diff --git a/server-rs/crates/module-runtime/src/errors.rs b/server-rs/crates/module-runtime/src/errors.rs index 166c8d3b..03cdeb8f 100644 --- a/server-rs/crates/module-runtime/src/errors.rs +++ b/server-rs/crates/module-runtime/src/errors.rs @@ -75,7 +75,7 @@ impl std::fmt::Display for RuntimeProfileFieldError { Self::InvalidWalletAmount => f.write_str("profile.wallet_amount 必须大于 0"), Self::WalletAmountOverflow => f.write_str("profile.wallet_amount 超出上限"), Self::WalletBalanceOverflow => f.write_str("profile.wallet_balance 超出上限"), - Self::InsufficientWalletBalance => f.write_str("叙世币余额不足"), + Self::InsufficientWalletBalance => f.write_str("光点余额不足"), Self::MissingInviteCode => f.write_str("referral.invite_code 不能为空"), Self::MissingRedeemCode => f.write_str("兑换码不能为空"), Self::RedeemCodeDisabled => f.write_str("兑换码已停用"), diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 31025687..acfc87f4 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -22,57 +22,57 @@ pub fn runtime_profile_recharge_point_products() -> Vec Vec, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct UsePuzzleRuntimePropRequest { diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index b64be155..369ecc98 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -4783,6 +4783,7 @@ pub struct PuzzleRunDragRecordInput { pub struct PuzzleRunNextLevelRecordInput { pub run_id: String, pub owner_user_id: String, + pub target_profile_id: Option, pub advanced_at_micros: i64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 2a27a6cf..84d8a570 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.1.0 (commit 10a4779b1338eff3708493d87496b51842a7c412). +// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs index 18258482..7feb5b93 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs @@ -9,6 +9,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; pub struct PuzzleRunNextLevelInput { pub run_id: String, pub owner_user_id: String, + pub target_profile_id: Option, pub advanced_at_micros: i64, } diff --git a/server-rs/crates/spacetime-client/src/puzzle.rs b/server-rs/crates/spacetime-client/src/puzzle.rs index 1fb2e6bf..59be7208 100644 --- a/server-rs/crates/spacetime-client/src/puzzle.rs +++ b/server-rs/crates/spacetime-client/src/puzzle.rs @@ -559,6 +559,7 @@ impl SpacetimeClient { let procedure_input = PuzzleRunNextLevelInput { run_id: input.run_id, owner_user_id: input.owner_user_id, + target_profile_id: input.target_profile_id, advanced_at_micros: input.advanced_at_micros, }; diff --git a/server-rs/crates/spacetime-module/src/custom_world/mod.rs b/server-rs/crates/spacetime-module/src/custom_world/mod.rs index bfba055e..48c7300c 100644 --- a/server-rs/crates/spacetime-module/src/custom_world/mod.rs +++ b/server-rs/crates/spacetime-module/src/custom_world/mod.rs @@ -16,7 +16,7 @@ pub struct CustomWorldProfile { owner_user_id: String, // 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。 public_work_code: Option, - // 作者公开叙世号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。 + // 作者公开百梦号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。 author_public_user_code: Option, source_agent_session_id: Option, publication_status: CustomWorldPublicationStatus, diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 6df724e3..fde32b88 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -1769,17 +1769,36 @@ fn advance_puzzle_next_level_tx( let same_work_next_profile = selected_profile_level_after_runtime_level(¤t_profile, current_level) .map(|level| profile_for_single_level(¤t_profile, &level)); + let candidates = if same_work_next_profile.is_none() { + list_published_puzzle_profiles(ctx)? + } else { + Vec::new() + }; let similar_work_next_profile = if same_work_next_profile.is_none() { - let candidates = list_published_puzzle_profiles(ctx)?; - select_next_profiles( + let selected_candidates = select_next_profiles( ¤t_profile, ¤t_run.played_profile_ids, &candidates, - 1, + 3, + ); + Some( + if let Some(target_profile_id) = input.target_profile_id.as_ref().and_then(|value| { + let trimmed = value.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + }) { + selected_candidates + .into_iter() + .find(|candidate| candidate.profile_id == target_profile_id) + .cloned() + .ok_or_else(|| "目标拼图作品不在当前下一关候选中".to_string())? + } else { + selected_candidates + .into_iter() + .next() + .cloned() + .ok_or_else(|| "没有可用的下一关候选".to_string())? + }, ) - .into_iter() - .next() - .cloned() } else { None }; diff --git a/src/components/common/PublishShareModal.test.tsx b/src/components/common/PublishShareModal.test.tsx index 2190c59f..408ed01f 100644 --- a/src/components/common/PublishShareModal.test.tsx +++ b/src/components/common/PublishShareModal.test.tsx @@ -47,6 +47,11 @@ describe('PublishShareModal', () => { ); const dialog = screen.getByRole('dialog', { name: '分享给朋友' }); + expect(dialog.parentElement?.className).toContain('!items-center'); + expect(dialog.parentElement?.className).toContain('platform-theme--light'); + expect(dialog.className).toContain('platform-modal-shell'); + expect(dialog.className).toContain('rounded-[1.75rem]'); + expect(dialog.getAttribute('style')).toBeNull(); expect(within(dialog).getByText(/邀请你来玩《暖灯猫街》/u)).toBeTruthy(); expect(within(dialog).getByRole('button', { name: '分享' })).toBeTruthy(); expect(within(dialog).getByRole('button', { name: '分享到微信' })).toBeTruthy(); diff --git a/src/components/common/PublishShareModal.tsx b/src/components/common/PublishShareModal.tsx index d2a1caec..e09fdb0c 100644 --- a/src/components/common/PublishShareModal.tsx +++ b/src/components/common/PublishShareModal.tsx @@ -2,6 +2,7 @@ import { Check, Copy, MessageCircle, Music2 } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { copyTextToClipboard } from '../../services/clipboard'; +import { useAuthUi } from '../auth/AuthUiContext'; import { buildPublishShareText, type PublishShareModalPayload, @@ -44,6 +45,7 @@ export function PublishShareModal({ payload, onClose, }: PublishShareModalProps) { + const platformTheme = useAuthUi()?.platformTheme ?? 'light'; const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>( 'idle', ); @@ -89,7 +91,8 @@ export function PublishShareModal({ title="分享给朋友" onClose={onClose} size="sm" - panelClassName="platform-remap-surface" + overlayClassName={`platform-theme platform-theme--${platformTheme} !items-center`} + panelClassName="platform-remap-surface rounded-[1.75rem]" bodyClassName="space-y-4 px-4 py-4 sm:px-5 sm:py-5" footerClassName="justify-center border-t-0 px-4 pb-5 pt-0 sm:px-5" footer={ diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index da85155b..b6d20b53 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -132,6 +132,8 @@ import { remixPuzzleGalleryWork, } from '../../services/puzzle-gallery'; import { + advancePuzzleNextLevel, + startPuzzleRun, submitPuzzleLeaderboard, } from '../../services/puzzle-runtime'; import { @@ -141,6 +143,7 @@ import { extendLocalPuzzleTime, isLocalPuzzleRun, refreshLocalPuzzleTimer, + resolvePuzzleRestartLevelId, restartLocalPuzzleLevel, setLocalPuzzlePaused, startLocalPuzzleRun, @@ -876,31 +879,20 @@ function mergePuzzleServiceRuntimeState( } const serviceLevel = serviceRun.currentLevel; - if ( - currentRun.currentLevel.status === 'cleared' && - serviceLevel.status !== 'cleared' - ) { - return { - ...currentRun, - recommendedNextProfileId: serviceRun.recommendedNextProfileId, - nextLevelMode: serviceRun.nextLevelMode, - nextLevelProfileId: serviceRun.nextLevelProfileId, - nextLevelId: serviceRun.nextLevelId, - recommendedNextWorks: serviceRun.recommendedNextWorks, - leaderboardEntries: - currentRun.currentLevel.leaderboardEntries.length > 0 - ? currentRun.currentLevel.leaderboardEntries - : currentRun.leaderboardEntries, - }; - } - const leaderboardEntries = serviceLevel.leaderboardEntries.length > 0 ? serviceLevel.leaderboardEntries : serviceRun.leaderboardEntries; + // 中文注释:拼块布局和通关状态由前端即时裁决;后端快照只合并榜单与下一关 handoff。 return { ...currentRun, + runId: serviceRun.runId, + entryProfileId: serviceRun.entryProfileId, + clearedLevelCount: Math.max( + currentRun.clearedLevelCount, + serviceRun.clearedLevelCount, + ), recommendedNextProfileId: serviceRun.recommendedNextProfileId, nextLevelMode: serviceRun.nextLevelMode, nextLevelProfileId: serviceRun.nextLevelProfileId, @@ -909,18 +901,10 @@ function mergePuzzleServiceRuntimeState( leaderboardEntries, currentLevel: { ...currentRun.currentLevel, - status: serviceLevel.status, - startedAtMs: serviceLevel.startedAtMs, - clearedAtMs: serviceLevel.clearedAtMs, - elapsedMs: serviceLevel.elapsedMs, - timeLimitMs: serviceLevel.timeLimitMs, - remainingMs: serviceLevel.remainingMs, - pausedAccumulatedMs: serviceLevel.pausedAccumulatedMs, - pauseStartedAtMs: serviceLevel.pauseStartedAtMs, - freezeAccumulatedMs: serviceLevel.freezeAccumulatedMs, - freezeStartedAtMs: serviceLevel.freezeStartedAtMs, - freezeUntilMs: serviceLevel.freezeUntilMs, - leaderboardEntries, + leaderboardEntries: + leaderboardEntries.length > 0 + ? leaderboardEntries + : currentRun.currentLevel.leaderboardEntries, }, }; } @@ -2181,8 +2165,12 @@ export function PlatformEntryFlowShellImpl({ setPuzzleError(null); try { - const item = detailItem ?? (await getPuzzleGalleryDetail(profileId)).item; - const run = startLocalPuzzleRun(item, levelId ?? null); + const item = + detailItem ?? (await getPuzzleGalleryDetail(profileId)).item; + const { run } = await startPuzzleRun({ + profileId: item.profileId, + levelId: levelId ?? null, + }); setSelectedPuzzleDetail(item); setPuzzleRun(run); setPuzzleRuntimeReturnStage(returnStage); @@ -2411,14 +2399,20 @@ export function PlatformEntryFlowShellImpl({ setIsPuzzleBusy(true); setPuzzleError(null); try { - setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload)); + setPuzzleRun( + swapLocalPuzzlePieces( + puzzleRun, + payload, + isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null, + ), + ); } catch (error) { setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。')); } finally { setIsPuzzleBusy(false); } }, - [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage], + [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage, selectedPuzzleDetail], ); const dragPuzzlePiece = useCallback( @@ -2430,14 +2424,20 @@ export function PlatformEntryFlowShellImpl({ setIsPuzzleBusy(true); setPuzzleError(null); try { - setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload)); + setPuzzleRun( + dragLocalPuzzlePiece( + puzzleRun, + payload, + isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null, + ), + ); } catch (error) { setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。')); } finally { setIsPuzzleBusy(false); } }, - [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage], + [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage, selectedPuzzleDetail], ); useEffect(() => { @@ -2515,18 +2515,46 @@ export function PlatformEntryFlowShellImpl({ ); const restartPuzzleCurrentLevel = useCallback(async () => { - const currentLevel = puzzleRun?.currentLevel ?? null; - if (!puzzleRun || !currentLevel || isPuzzleBusy) { + const currentRun = puzzleRunRef.current ?? puzzleRun; + const currentLevel = currentRun?.currentLevel ?? null; + if (!currentRun || !currentLevel || isPuzzleBusy) { return; } setPuzzleError(null); - const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun); - puzzleRunRef.current = nextRun; - setPuzzleRun(nextRun); + setIsPuzzleBusy(true); + try { + if (isLocalPuzzleRun(currentRun)) { + const nextRun = restartLocalPuzzleLevel(currentRun); + puzzleRunRef.current = nextRun; + setPuzzleRun(nextRun); + return; + } + + const detailItem = + selectedPuzzleDetail?.profileId === currentLevel.profileId + ? selectedPuzzleDetail + : await getPuzzleGalleryDetail(currentLevel.profileId).then( + (response) => response.item, + ); + const { run } = await startPuzzleRun({ + profileId: currentLevel.profileId, + levelId: resolvePuzzleRestartLevelId(currentRun, detailItem), + }); + setSelectedPuzzleDetail(detailItem); + puzzleRunRef.current = run; + setPuzzleRun(run); + } catch (error) { + setPuzzleError(resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。')); + } finally { + setIsPuzzleBusy(false); + } }, [ isPuzzleBusy, puzzleRun, + resolvePuzzleErrorMessage, + selectedPuzzleDetail, + setIsPuzzleBusy, setPuzzleError, ]); @@ -2565,14 +2593,19 @@ export function PlatformEntryFlowShellImpl({ gameState.currentLevelId.trim() ? gameState.currentLevelId : null; - const item = selectedPuzzleDetail?.profileId === profileId - ? selectedPuzzleDetail - : await getPuzzleGalleryDetail(profileId).then((response) => response.item); - const nextRun = startLocalPuzzleRun(item, levelId); - setSelectedPuzzleDetail(item); - setPuzzleRun(nextRun); - setPuzzleRuntimeReturnStage('platform'); - setSelectionStage('puzzle-runtime'); + const item = + selectedPuzzleDetail?.profileId === profileId + ? selectedPuzzleDetail + : await getPuzzleGalleryDetail(profileId).then( + (response) => response.item, + ); + await startPuzzleRunFromProfile( + item.profileId, + 'platform', + item, + false, + levelId, + ); } catch (error) { platformBootstrap.setSaveError( resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'), @@ -2587,7 +2620,7 @@ export function PlatformEntryFlowShellImpl({ selectedPuzzleDetail, resolvePuzzleErrorMessage, setPuzzleError, - setSelectionStage, + startPuzzleRunFromProfile, ], ); @@ -2651,7 +2684,7 @@ export function PlatformEntryFlowShellImpl({ ]); const advancePuzzleLevel = useCallback( - async (target?: { profileId?: string; levelId?: string | null }) => { + async (_target?: { profileId?: string; levelId?: string | null }) => { if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) { return; } @@ -2665,12 +2698,43 @@ export function PlatformEntryFlowShellImpl({ setPuzzleError(null); try { - const nextRun = advanceLocalPuzzleLevel( - puzzleRun, - selectedPuzzleDetail, - target, - ); - setPuzzleRun(nextRun); + if (isLocalPuzzleRun(puzzleRun)) { + const nextRun = advanceLocalPuzzleLevel( + puzzleRun, + selectedPuzzleDetail, + _target, + ); + setPuzzleRun(nextRun); + return; + } + + const targetProfileId = _target?.profileId?.trim() ?? ''; + if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) { + const itemPromise = + selectedPuzzleDetail?.profileId === targetProfileId + ? Promise.resolve(selectedPuzzleDetail) + : getPuzzleGalleryDetail(targetProfileId).then( + (response) => response.item, + ); + const [{ run }, item] = await Promise.all([ + advancePuzzleNextLevel(puzzleRun.runId, { + targetProfileId, + }), + itemPromise, + ]); + setSelectedPuzzleDetail(item); + setPuzzleRun(run); + pushAppHistoryPath( + buildPublicWorkStagePath( + 'puzzle-runtime', + buildPuzzlePublicWorkCode(item.profileId), + ), + ); + return; + } + + const { run } = await advancePuzzleNextLevel(puzzleRun.runId); + setPuzzleRun(run); } catch (error) { setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。')); } finally { @@ -3563,7 +3627,10 @@ export function PlatformEntryFlowShellImpl({ } if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) { - const work = mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail); + const work = + selectedPuzzleDetail?.profileId === selectedPublicWorkDetail.profileId + ? selectedPuzzleDetail + : mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail); if (!work) { setPublicWorkDetailError( '当前拼图作品信息不完整,暂时无法进入玩法。', @@ -3628,10 +3695,11 @@ export function PlatformEntryFlowShellImpl({ isPublicWorkDetailBusy, runProtectedAction, selectedDetailEntry, + selectedPuzzleDetail, selectedPublicWorkDetail, startBigFishRunFromWork, - startMatch3DRunFromProfile, startPuzzleRunFromProfile, + startMatch3DRunFromProfile, ]); const remixPublicWork = useCallback( diff --git a/src/components/puzzle-result/PuzzleResultView.tsx b/src/components/puzzle-result/PuzzleResultView.tsx index aa31afca..81607de4 100644 --- a/src/components/puzzle-result/PuzzleResultView.tsx +++ b/src/components/puzzle-result/PuzzleResultView.tsx @@ -132,7 +132,7 @@ function syncDraftFromEditState( workTitle: editState.workTitle.trim() || draft.workTitle, workDescription: editState.workDescription.trim(), levelName: primaryLevel.levelName, - summary: primaryLevel.pictureDescription, + summary: editState.workDescription.trim(), themeTags: editState.themeTags, candidates: primaryLevel.candidates, selectedCandidateId: primaryLevel.selectedCandidateId, @@ -1378,7 +1378,7 @@ export function PuzzleResultView({ workTitle: normalizedState.workTitle, workDescription: normalizedState.workDescription, levelName: firstLevel.levelName, - summary: firstLevel.pictureDescription, + summary: normalizedState.workDescription, themeTags: normalizedState.themeTags, coverImageSrc: resolveLevelFormalImageSrc(firstLevel) || null, coverAssetId: firstLevel.coverAssetId ?? null, @@ -1531,7 +1531,7 @@ export function PuzzleResultView({ workTitle: editState.workTitle.trim(), workDescription: editState.workDescription.trim(), levelName: firstLevel.levelName.trim(), - summary: firstLevel.pictureDescription.trim(), + summary: editState.workDescription.trim(), themeTags: editState.themeTags, levelsJson: JSON.stringify(editState.levels), }); @@ -1555,7 +1555,7 @@ export function PuzzleResultView({ candidateCount: 1, workTitle: editState.workTitle.trim(), workDescription: editState.workDescription.trim(), - summary: activeLevel.pictureDescription.trim(), + summary: editState.workDescription.trim(), themeTags: editState.themeTags, levelsJson: JSON.stringify(editState.levels), }); diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx index cec5b65e..7cd17dc3 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx @@ -1743,26 +1743,30 @@ export function PuzzleRuntimeShell({ {isExitRemodelPromptOpen ? (
event.stopPropagation()} > -
+
+
+
+ +

体验不佳?
试试改造功能!

-