fix: show published big fish works in gallery
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -147,7 +147,7 @@
|
|||||||
|
|
||||||
## 6. HTTP contract
|
## 6. HTTP contract
|
||||||
|
|
||||||
所有接口挂在 `/api/runtime/big-fish/*`,全部需要 Bearer 鉴权。
|
所有接口挂在 `/api/runtime/big-fish/*`。创作、私有作品列表、删除、运行态启动与输入推进需要 Bearer 鉴权;公开广场读取接口不要求登录,只返回已发布作品。
|
||||||
|
|
||||||
开发态本地链路补充约定:
|
开发态本地链路补充约定:
|
||||||
|
|
||||||
@@ -191,12 +191,24 @@
|
|||||||
2. `GET /api/runtime/big-fish/runs/{runId}`
|
2. `GET /api/runtime/big-fish/runs/{runId}`
|
||||||
3. `POST /api/runtime/big-fish/runs/{runId}/input`
|
3. `POST /api/runtime/big-fish/runs/{runId}/input`
|
||||||
|
|
||||||
|
运行态启动规则:
|
||||||
|
|
||||||
|
1. 当前用户启动自己未发布草稿时,`session.owner_user_id` 必须等于当前登录用户。
|
||||||
|
2. 当前用户启动别人作品时,只允许启动 `stage = published` 的公开作品。
|
||||||
|
3. 新建的 `big_fish_runtime_run.owner_user_id` 始终写入当前游玩用户,不能写入作品作者,后续 run 查询与输入推进仍按游玩用户隔离。
|
||||||
|
|
||||||
### 6.3 作品列表
|
### 6.3 作品列表
|
||||||
|
|
||||||
1. `GET /api/runtime/big-fish/works`
|
1. `GET /api/runtime/big-fish/works`
|
||||||
|
|
||||||
开发态 Vite 必须把该同源接口代理到 Rust `api-server`;前端作品页只调用同源 `/api/runtime/big-fish/works`,不得直连 Rust 端口或回退到 `server-node`。
|
开发态 Vite 必须把该同源接口代理到 Rust `api-server`;前端作品页只调用同源 `/api/runtime/big-fish/works`,不得直连 Rust 端口或回退到 `server-node`。
|
||||||
|
|
||||||
|
### 6.4 公开广场
|
||||||
|
|
||||||
|
1. `GET /api/runtime/big-fish/gallery`
|
||||||
|
|
||||||
|
公开广场只返回 `status = published` 的大鱼吃小鱼作品。响应复用 `BigFishWorksResponse`,每个条目必须包含 `ownerUserId`,供前端生成稳定广场卡片 key 与后续运行态权限判断。发布动作完成后,前端必须同时刷新私有作品列表和公开广场列表,保证发布结果能立即出现在首页与分类页。
|
||||||
|
|
||||||
`input` 请求体:
|
`input` 请求体:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type BigFishWorkStatus = 'draft' | 'published';
|
|||||||
export interface BigFishWorkSummary {
|
export interface BigFishWorkSummary {
|
||||||
workId: string;
|
workId: string;
|
||||||
sourceSessionId: string;
|
sourceSessionId: string;
|
||||||
|
ownerUserId: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ use crate::{
|
|||||||
auth_sessions::auth_sessions,
|
auth_sessions::auth_sessions,
|
||||||
big_fish::{
|
big_fish::{
|
||||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
|
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
|
||||||
get_big_fish_session, get_big_fish_works, start_big_fish_run, stream_big_fish_message,
|
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, start_big_fish_run,
|
||||||
submit_big_fish_input, submit_big_fish_message,
|
stream_big_fish_message, submit_big_fish_input, submit_big_fish_message,
|
||||||
},
|
},
|
||||||
character_animation_assets::{
|
character_animation_assets::{
|
||||||
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
||||||
@@ -562,6 +562,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route("/api/runtime/big-fish/gallery", get(list_big_fish_gallery))
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/big-fish/works/{session_id}",
|
"/api/runtime/big-fish/works/{session_id}",
|
||||||
delete(delete_big_fish_work).route_layer(middleware::from_fn_with_state(
|
delete(delete_big_fish_work).route_layer(middleware::from_fn_with_state(
|
||||||
|
|||||||
@@ -144,6 +144,29 @@ pub async fn get_big_fish_works(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_big_fish_gallery(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let items = state
|
||||||
|
.spacetime_client()
|
||||||
|
.list_big_fish_gallery()
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
BigFishWorksResponse {
|
||||||
|
items: items
|
||||||
|
.into_iter()
|
||||||
|
.map(map_big_fish_work_summary_response)
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_big_fish_work(
|
pub async fn delete_big_fish_work(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
@@ -917,6 +940,7 @@ fn map_big_fish_work_summary_response(
|
|||||||
BigFishWorkSummaryResponse {
|
BigFishWorkSummaryResponse {
|
||||||
work_id: item.work_id,
|
work_id: item.work_id,
|
||||||
source_session_id: item.source_session_id,
|
source_session_id: item.source_session_id,
|
||||||
|
owner_user_id: item.owner_user_id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
subtitle: item.subtitle,
|
subtitle: item.subtitle,
|
||||||
summary: item.summary,
|
summary: item.summary,
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ pub struct BigFishSessionProcedureResult {
|
|||||||
pub struct BigFishWorkSummarySnapshot {
|
pub struct BigFishWorkSummarySnapshot {
|
||||||
pub work_id: String,
|
pub work_id: String,
|
||||||
pub source_session_id: String,
|
pub source_session_id: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub subtitle: String,
|
pub subtitle: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
@@ -274,6 +275,7 @@ pub struct BigFishWorkSummarySnapshot {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct BigFishWorksListInput {
|
pub struct BigFishWorksListInput {
|
||||||
pub owner_user_id: String,
|
pub owner_user_id: String,
|
||||||
|
pub published_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
@@ -747,6 +749,9 @@ pub fn validate_session_get_input(input: &BigFishSessionGetInput) -> Result<(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_works_list_input(input: &BigFishWorksListInput) -> Result<(), BigFishFieldError> {
|
pub fn validate_works_list_input(input: &BigFishWorksListInput) -> Result<(), BigFishFieldError> {
|
||||||
|
if input.published_only {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if normalize_required_string(&input.owner_user_id).is_none() {
|
if normalize_required_string(&input.owner_user_id).is_none() {
|
||||||
return Err(BigFishFieldError::MissingOwnerUserId);
|
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct BigFishWorkSummaryResponse {
|
pub struct BigFishWorkSummaryResponse {
|
||||||
pub work_id: String,
|
pub work_id: String,
|
||||||
pub source_session_id: String,
|
pub source_session_id: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub subtitle: String,
|
pub subtitle: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
|
|||||||
@@ -57,8 +57,28 @@ impl SpacetimeClient {
|
|||||||
&self,
|
&self,
|
||||||
owner_user_id: String,
|
owner_user_id: String,
|
||||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
let procedure_input = BigFishWorksListInput { owner_user_id };
|
let procedure_input = BigFishWorksListInput {
|
||||||
|
owner_user_id,
|
||||||
|
published_only: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.list_big_fish_works_with_input(procedure_input).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_big_fish_gallery(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
self.list_big_fish_works_with_input(BigFishWorksListInput {
|
||||||
|
owner_user_id: String::new(),
|
||||||
|
published_only: true,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_big_fish_works_with_input(
|
||||||
|
&self,
|
||||||
|
procedure_input: BigFishWorksListInput,
|
||||||
|
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
self.call_after_connect(move |connection, sender| {
|
self.call_after_connect(move |connection, sender| {
|
||||||
connection
|
connection
|
||||||
.procedures()
|
.procedures()
|
||||||
|
|||||||
@@ -4590,6 +4590,7 @@ pub struct BigFishSessionRecord {
|
|||||||
pub struct BigFishWorkSummaryRecord {
|
pub struct BigFishWorkSummaryRecord {
|
||||||
pub work_id: String,
|
pub work_id: String,
|
||||||
pub source_session_id: String,
|
pub source_session_id: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub subtitle: String,
|
pub subtitle: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
|||||||
#[sats(crate = __lib)]
|
#[sats(crate = __lib)]
|
||||||
pub struct BigFishWorksListInput {
|
pub struct BigFishWorksListInput {
|
||||||
pub owner_user_id: String,
|
pub owner_user_id: String,
|
||||||
|
pub published_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for BigFishWorksListInput {
|
impl __sdk::InModule for BigFishWorksListInput {
|
||||||
|
|||||||
@@ -77,8 +77,12 @@ fn start_big_fish_run_tx(
|
|||||||
.big_fish_creation_session()
|
.big_fish_creation_session()
|
||||||
.session_id()
|
.session_id()
|
||||||
.find(&input.session_id)
|
.find(&input.session_id)
|
||||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
|
||||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||||
|
if session.owner_user_id != input.owner_user_id
|
||||||
|
&& session.stage != BigFishCreationStage::Published
|
||||||
|
{
|
||||||
|
return Err("big_fish_creation_session 不存在".to_string());
|
||||||
|
}
|
||||||
let draft = session
|
let draft = session
|
||||||
.draft_json
|
.draft_json
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -124,8 +128,12 @@ fn submit_big_fish_input_tx(
|
|||||||
.big_fish_creation_session()
|
.big_fish_creation_session()
|
||||||
.session_id()
|
.session_id()
|
||||||
.find(&run.session_id)
|
.find(&run.session_id)
|
||||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
|
||||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||||
|
if session.owner_user_id != input.owner_user_id
|
||||||
|
&& session.stage != BigFishCreationStage::Published
|
||||||
|
{
|
||||||
|
return Err("big_fish_creation_session 不存在".to_string());
|
||||||
|
}
|
||||||
let draft = session
|
let draft = session
|
||||||
.draft_json
|
.draft_json
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|||||||
@@ -239,6 +239,10 @@ pub(crate) fn list_big_fish_works_tx(
|
|||||||
.big_fish_creation_session()
|
.big_fish_creation_session()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| {
|
.filter(|row| {
|
||||||
|
if input.published_only {
|
||||||
|
return row.stage == BigFishCreationStage::Published;
|
||||||
|
}
|
||||||
|
|
||||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||||
})
|
})
|
||||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||||
@@ -330,6 +334,7 @@ pub(crate) fn delete_big_fish_work_tx(
|
|||||||
ctx,
|
ctx,
|
||||||
BigFishWorksListInput {
|
BigFishWorksListInput {
|
||||||
owner_user_id: input.owner_user_id,
|
owner_user_id: input.owner_user_id,
|
||||||
|
published_only: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -642,6 +647,7 @@ pub(crate) fn build_big_fish_work_summary(
|
|||||||
Ok(BigFishWorkSummarySnapshot {
|
Ok(BigFishWorkSummarySnapshot {
|
||||||
work_id: format!("big-fish-work-{}", row.session_id),
|
work_id: format!("big-fish-work-{}", row.session_id),
|
||||||
source_session_id: row.session_id.clone(),
|
source_session_id: row.session_id.clone(),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
summary,
|
summary,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
startBigFishRuntimeRun,
|
startBigFishRuntimeRun,
|
||||||
submitBigFishRuntimeInput,
|
submitBigFishRuntimeInput,
|
||||||
} from '../../services/big-fish-runtime';
|
} from '../../services/big-fish-runtime';
|
||||||
|
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||||
import {
|
import {
|
||||||
deleteBigFishWork,
|
deleteBigFishWork,
|
||||||
listBigFishWorks,
|
listBigFishWorks,
|
||||||
@@ -64,7 +65,10 @@ import {
|
|||||||
type MiniGameDraftGenerationState,
|
type MiniGameDraftGenerationState,
|
||||||
} from '../../services/miniGameDraftGenerationProgress';
|
} from '../../services/miniGameDraftGenerationProgress';
|
||||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||||
import { isSamePuzzlePublicWorkCode } from '../../services/publicWorkCode';
|
import {
|
||||||
|
isSameBigFishPublicWorkCode,
|
||||||
|
isSamePuzzlePublicWorkCode,
|
||||||
|
} from '../../services/publicWorkCode';
|
||||||
import {
|
import {
|
||||||
createPuzzleAgentSession,
|
createPuzzleAgentSession,
|
||||||
executePuzzleAgentAction,
|
executePuzzleAgentAction,
|
||||||
@@ -91,7 +95,9 @@ import {
|
|||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
import { useAuthUi } from '../auth/AuthUiContext';
|
import { useAuthUi } from '../auth/AuthUiContext';
|
||||||
import {
|
import {
|
||||||
|
isBigFishGalleryEntry,
|
||||||
isPuzzleGalleryEntry,
|
isPuzzleGalleryEntry,
|
||||||
|
mapBigFishWorkToPlatformGalleryCard,
|
||||||
mapPuzzleWorkToPlatformGalleryCard,
|
mapPuzzleWorkToPlatformGalleryCard,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
@@ -146,7 +152,12 @@ function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
|
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
|
||||||
return `${isPuzzleGalleryEntry(entry) ? 'puzzle' : 'rpg'}:${entry.ownerUserId}:${entry.profileId}`;
|
const kind = isBigFishGalleryEntry(entry)
|
||||||
|
? 'big-fish'
|
||||||
|
: isPuzzleGalleryEntry(entry)
|
||||||
|
? 'puzzle'
|
||||||
|
: 'rpg';
|
||||||
|
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePlatformPublicGalleryEntries(
|
function mergePlatformPublicGalleryEntries(
|
||||||
@@ -393,6 +404,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const [selectedDetailEntry, setSelectedDetailEntry] =
|
const [selectedDetailEntry, setSelectedDetailEntry] =
|
||||||
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
||||||
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
|
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
|
||||||
|
const [bigFishGalleryEntries, setBigFishGalleryEntries] = useState<
|
||||||
|
BigFishWorkSummary[]
|
||||||
|
>([]);
|
||||||
const [bigFishRun, setBigFishRun] =
|
const [bigFishRun, setBigFishRun] =
|
||||||
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
||||||
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
|
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
|
||||||
@@ -460,6 +474,64 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refreshBigFishShelf = useCallback(async () => {
|
||||||
|
setIsBigFishLoadingLibrary(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const worksResponse = await listBigFishWorks();
|
||||||
|
setBigFishWorks(worksResponse.items);
|
||||||
|
setBigFishError(null);
|
||||||
|
} catch (error) {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsBigFishLoadingLibrary(false);
|
||||||
|
}
|
||||||
|
}, [resolveBigFishErrorMessage]);
|
||||||
|
|
||||||
|
const refreshBigFishGallery = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const galleryResponse = await listBigFishGallery();
|
||||||
|
setBigFishGalleryEntries(galleryResponse.items);
|
||||||
|
return galleryResponse.items;
|
||||||
|
} catch (error) {
|
||||||
|
setBigFishGalleryEntries([]);
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼广场失败。'),
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [resolveBigFishErrorMessage]);
|
||||||
|
|
||||||
|
const refreshPuzzleShelf = useCallback(async () => {
|
||||||
|
setIsPuzzleLoadingLibrary(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const worksResponse = await listPuzzleWorks();
|
||||||
|
setPuzzleWorks(worksResponse.items);
|
||||||
|
setPuzzleError(null);
|
||||||
|
} catch (error) {
|
||||||
|
setPuzzleError(
|
||||||
|
resolvePuzzleErrorMessage(error, '读取拼图作品列表失败。'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsPuzzleLoadingLibrary(false);
|
||||||
|
}
|
||||||
|
}, [resolvePuzzleErrorMessage]);
|
||||||
|
|
||||||
|
const refreshPuzzleGallery = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const galleryResponse = await listPuzzleGallery();
|
||||||
|
setPuzzleGalleryEntries(galleryResponse.items);
|
||||||
|
return galleryResponse.items;
|
||||||
|
} catch (error) {
|
||||||
|
setPuzzleGalleryEntries([]);
|
||||||
|
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图广场失败。'));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [resolvePuzzleErrorMessage]);
|
||||||
|
|
||||||
const sessionController = useRpgCreationSessionController({
|
const sessionController = useRpgCreationSessionController({
|
||||||
userId: authUi?.user?.id,
|
userId: authUi?.user?.id,
|
||||||
openLoginModal: authUi?.openLoginModal,
|
openLoginModal: authUi?.openLoginModal,
|
||||||
@@ -552,6 +624,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
platformBootstrap.refreshPublishedGallery(),
|
platformBootstrap.refreshPublishedGallery(),
|
||||||
platformBootstrap.refreshCustomWorldWorks(),
|
platformBootstrap.refreshCustomWorldWorks(),
|
||||||
|
refreshBigFishGallery(),
|
||||||
refreshPuzzleGallery(),
|
refreshPuzzleGallery(),
|
||||||
]);
|
]);
|
||||||
return latestSession;
|
return latestSession;
|
||||||
@@ -606,21 +679,35 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}, [agentResultPreview]);
|
}, [agentResultPreview]);
|
||||||
|
|
||||||
const featuredGalleryEntries = useMemo(() => {
|
const featuredGalleryEntries = useMemo(() => {
|
||||||
|
const bigFishPublicEntries = bigFishGalleryEntries.map(
|
||||||
|
mapBigFishWorkToPlatformGalleryCard,
|
||||||
|
);
|
||||||
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
||||||
mapPuzzleWorkToPlatformGalleryCard,
|
mapPuzzleWorkToPlatformGalleryCard,
|
||||||
);
|
);
|
||||||
return mergePlatformPublicGalleryEntries(
|
return mergePlatformPublicGalleryEntries(
|
||||||
platformBootstrap.publishedGalleryEntries,
|
platformBootstrap.publishedGalleryEntries,
|
||||||
puzzlePublicEntries,
|
[...bigFishPublicEntries, ...puzzlePublicEntries],
|
||||||
).slice(0, 6);
|
).slice(0, 6);
|
||||||
}, [platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries]);
|
}, [
|
||||||
|
bigFishGalleryEntries,
|
||||||
|
platformBootstrap.publishedGalleryEntries,
|
||||||
|
puzzleGalleryEntries,
|
||||||
|
]);
|
||||||
const latestGalleryEntries = useMemo(
|
const latestGalleryEntries = useMemo(
|
||||||
() =>
|
() =>
|
||||||
mergePlatformPublicGalleryEntries(
|
mergePlatformPublicGalleryEntries(
|
||||||
platformBootstrap.publishedGalleryEntries,
|
platformBootstrap.publishedGalleryEntries,
|
||||||
puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
|
[
|
||||||
|
...bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard),
|
||||||
|
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
[platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries],
|
[
|
||||||
|
bigFishGalleryEntries,
|
||||||
|
platformBootstrap.publishedGalleryEntries,
|
||||||
|
puzzleGalleryEntries,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const creationHubItems =
|
const creationHubItems =
|
||||||
@@ -680,50 +767,6 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setShowCreationTypeModal(true);
|
setShowCreationTypeModal(true);
|
||||||
}, [prepareCreationLaunch]);
|
}, [prepareCreationLaunch]);
|
||||||
|
|
||||||
const refreshBigFishShelf = useCallback(async () => {
|
|
||||||
setIsBigFishLoadingLibrary(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const worksResponse = await listBigFishWorks();
|
|
||||||
setBigFishWorks(worksResponse.items);
|
|
||||||
setBigFishError(null);
|
|
||||||
} catch (error) {
|
|
||||||
setBigFishError(
|
|
||||||
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsBigFishLoadingLibrary(false);
|
|
||||||
}
|
|
||||||
}, [resolveBigFishErrorMessage]);
|
|
||||||
|
|
||||||
const refreshPuzzleShelf = useCallback(async () => {
|
|
||||||
setIsPuzzleLoadingLibrary(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const worksResponse = await listPuzzleWorks();
|
|
||||||
setPuzzleWorks(worksResponse.items);
|
|
||||||
setPuzzleError(null);
|
|
||||||
} catch (error) {
|
|
||||||
setPuzzleError(
|
|
||||||
resolvePuzzleErrorMessage(error, '读取拼图作品列表失败。'),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsPuzzleLoadingLibrary(false);
|
|
||||||
}
|
|
||||||
}, [resolvePuzzleErrorMessage]);
|
|
||||||
|
|
||||||
const refreshPuzzleGallery = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const galleryResponse = await listPuzzleGallery();
|
|
||||||
setPuzzleGalleryEntries(galleryResponse.items);
|
|
||||||
return galleryResponse.items;
|
|
||||||
} catch (error) {
|
|
||||||
setPuzzleGalleryEntries([]);
|
|
||||||
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图广场失败。'));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}, [resolvePuzzleErrorMessage]);
|
|
||||||
|
|
||||||
const bigFishFlow = usePlatformCreationAgentFlowController<
|
const bigFishFlow = usePlatformCreationAgentFlowController<
|
||||||
BigFishSessionSnapshotResponse,
|
BigFishSessionSnapshotResponse,
|
||||||
Record<string, never>,
|
Record<string, never>,
|
||||||
@@ -761,6 +804,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSession(response.session);
|
setSession(response.session);
|
||||||
if (payload.action === 'big_fish_publish_game') {
|
if (payload.action === 'big_fish_publish_game') {
|
||||||
void refreshBigFishShelf();
|
void refreshBigFishShelf();
|
||||||
|
void refreshBigFishGallery();
|
||||||
}
|
}
|
||||||
if (payload.action !== 'big_fish_compile_draft') {
|
if (payload.action !== 'big_fish_compile_draft') {
|
||||||
return;
|
return;
|
||||||
@@ -1081,6 +1125,34 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSelectionStage,
|
setSelectionStage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const restartBigFishRun = useCallback(async () => {
|
||||||
|
const sessionId = bigFishSession?.sessionId ?? bigFishRun?.sessionId;
|
||||||
|
if (!sessionId || isBigFishBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsBigFishBusy(true);
|
||||||
|
setBigFishError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { run } = await startBigFishRuntimeRun(sessionId);
|
||||||
|
setBigFishRun(run);
|
||||||
|
setSelectionStage('big-fish-runtime');
|
||||||
|
} catch (error) {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '重新开始大鱼吃小鱼玩法失败。'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsBigFishBusy(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
bigFishRun?.sessionId,
|
||||||
|
bigFishSession?.sessionId,
|
||||||
|
isBigFishBusy,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
setSelectionStage,
|
||||||
|
]);
|
||||||
|
|
||||||
const startPuzzleRunFromProfile = useCallback(
|
const startPuzzleRunFromProfile = useCallback(
|
||||||
async (profileId: string) => {
|
async (profileId: string) => {
|
||||||
if (isPuzzleBusy) {
|
if (isPuzzleBusy) {
|
||||||
@@ -1390,8 +1462,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
|
|
||||||
void deleteBigFishWork(work.sourceSessionId)
|
void deleteBigFishWork(work.sourceSessionId)
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
setBigFishWorks(response.items);
|
setBigFishWorks(response.items);
|
||||||
|
await refreshBigFishGallery().catch(() => []);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setBigFishError(
|
setBigFishError(
|
||||||
@@ -1403,7 +1476,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[deletingCreationWorkId, resolveBigFishErrorMessage, runProtectedAction],
|
[
|
||||||
|
deletingCreationWorkId,
|
||||||
|
refreshBigFishGallery,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
runProtectedAction,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeletePuzzleWork = useCallback(
|
const handleDeletePuzzleWork = useCallback(
|
||||||
@@ -1499,6 +1577,33 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
|
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const startBigFishRunFromWork = useCallback(
|
||||||
|
async (item: BigFishWorkSummary) => {
|
||||||
|
const sessionId = item.sourceSessionId?.trim();
|
||||||
|
if (!sessionId) {
|
||||||
|
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsBigFishBusy(true);
|
||||||
|
setBigFishError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { run } = await startBigFishRuntimeRun(sessionId);
|
||||||
|
bigFishFlow.setSession(null);
|
||||||
|
setBigFishRun(run);
|
||||||
|
setSelectionStage('big-fish-runtime');
|
||||||
|
} catch (error) {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsBigFishBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
|
||||||
|
);
|
||||||
|
|
||||||
const handlePublicCodeSearch = useCallback(
|
const handlePublicCodeSearch = useCallback(
|
||||||
async (keyword: string) => {
|
async (keyword: string) => {
|
||||||
const normalizedKeyword = keyword.trim();
|
const normalizedKeyword = keyword.trim();
|
||||||
@@ -1514,15 +1619,19 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(
|
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(
|
||||||
normalizedKeyword,
|
normalizedKeyword,
|
||||||
);
|
);
|
||||||
|
const shouldSearchBigFishFirst = upperKeyword.startsWith('BF');
|
||||||
const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
|
const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
|
||||||
const shouldSearchWorkFirst =
|
const shouldSearchWorkFirst =
|
||||||
!shouldSearchUserIdFirst &&
|
!shouldSearchUserIdFirst &&
|
||||||
|
!shouldSearchBigFishFirst &&
|
||||||
!shouldSearchPuzzleFirst &&
|
!shouldSearchPuzzleFirst &&
|
||||||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
||||||
const shouldSearchUserFirst =
|
const shouldSearchUserFirst =
|
||||||
shouldSearchUserIdFirst ||
|
shouldSearchUserIdFirst ||
|
||||||
upperKeyword.startsWith('SY') ||
|
upperKeyword.startsWith('SY') ||
|
||||||
(!shouldSearchWorkFirst && !shouldSearchPuzzleFirst);
|
(!shouldSearchWorkFirst &&
|
||||||
|
!shouldSearchBigFishFirst &&
|
||||||
|
!shouldSearchPuzzleFirst);
|
||||||
|
|
||||||
const tryOpenGalleryEntry = async () => {
|
const tryOpenGalleryEntry = async () => {
|
||||||
const entry =
|
const entry =
|
||||||
@@ -1562,6 +1671,21 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
tab: platformBootstrap.platformTab,
|
tab: platformBootstrap.platformTab,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const tryOpenBigFishGalleryEntry = async () => {
|
||||||
|
const entries =
|
||||||
|
bigFishGalleryEntries.length > 0
|
||||||
|
? bigFishGalleryEntries
|
||||||
|
: await refreshBigFishGallery();
|
||||||
|
const matchedEntry = entries.find((entry) =>
|
||||||
|
isSameBigFishPublicWorkCode(normalizedKeyword, entry.sourceSessionId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchedEntry) {
|
||||||
|
throw new Error('未找到大鱼吃小鱼作品。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await startBigFishRunFromWork(matchedEntry);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldSearchUserIdFirst) {
|
if (shouldSearchUserIdFirst) {
|
||||||
@@ -1575,6 +1699,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldSearchBigFishFirst) {
|
||||||
|
await tryOpenBigFishGalleryEntry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldSearchWorkFirst) {
|
if (shouldSearchWorkFirst) {
|
||||||
try {
|
try {
|
||||||
await tryOpenGalleryEntry();
|
await tryOpenGalleryEntry();
|
||||||
@@ -1611,10 +1740,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
detailNavigation,
|
detailNavigation,
|
||||||
|
bigFishGalleryEntries,
|
||||||
openPuzzleDetail,
|
openPuzzleDetail,
|
||||||
platformBootstrap.platformTab,
|
platformBootstrap.platformTab,
|
||||||
puzzleGalleryEntries,
|
puzzleGalleryEntries,
|
||||||
|
refreshBigFishGallery,
|
||||||
refreshPuzzleGallery,
|
refreshPuzzleGallery,
|
||||||
|
startBigFishRunFromWork,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1631,39 +1763,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
[bigFishFlow, refreshBigFishShelf],
|
[bigFishFlow, refreshBigFishShelf],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startBigFishRunFromWork = useCallback(
|
|
||||||
async (item: BigFishWorkSummary) => {
|
|
||||||
const sessionId = item.sourceSessionId?.trim();
|
|
||||||
if (!sessionId) {
|
|
||||||
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsBigFishBusy(true);
|
|
||||||
setBigFishError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { session } = await getBigFishCreationSession(sessionId);
|
|
||||||
const { run } = await startBigFishRuntimeRun(sessionId);
|
|
||||||
bigFishFlow.setSession(session);
|
|
||||||
setBigFishRun(run);
|
|
||||||
setSelectionStage('big-fish-runtime');
|
|
||||||
} catch (error) {
|
|
||||||
setBigFishError(
|
|
||||||
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsBigFishBusy(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectionStage === 'platform') {
|
if (selectionStage === 'platform') {
|
||||||
|
void refreshBigFishGallery();
|
||||||
void refreshPuzzleGallery();
|
void refreshPuzzleGallery();
|
||||||
}
|
}
|
||||||
}, [refreshPuzzleGallery, selectionStage]);
|
}, [refreshBigFishGallery, refreshPuzzleGallery, selectionStage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -1836,6 +1941,33 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
onOpenCreateWorld={openCreationTypePicker}
|
onOpenCreateWorld={openCreationTypePicker}
|
||||||
onOpenCreateTypePicker={openCreationTypePicker}
|
onOpenCreateTypePicker={openCreationTypePicker}
|
||||||
onOpenGalleryDetail={(entry) => {
|
onOpenGalleryDetail={(entry) => {
|
||||||
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
|
runProtectedAction(() => {
|
||||||
|
void startBigFishRunFromWork({
|
||||||
|
workId: entry.workId,
|
||||||
|
sourceSessionId: entry.profileId,
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
title: entry.worldName,
|
||||||
|
subtitle: entry.subtitle,
|
||||||
|
summary: entry.summaryText,
|
||||||
|
coverImageSrc: entry.coverImageSrc,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: Number.parseInt(
|
||||||
|
entry.themeTags
|
||||||
|
.find((tag) => /^\d+级$/u.test(tag))
|
||||||
|
?.replace('级', '') ?? '0',
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
levelMainImageReadyCount: 0,
|
||||||
|
levelMotionReadyCount: 0,
|
||||||
|
backgroundReady: Boolean(entry.coverImageSrc),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPuzzleGalleryEntry(entry)) {
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
void openPuzzleDetail(entry.profileId, {
|
void openPuzzleDetail(entry.profileId, {
|
||||||
tab: platformBootstrap.platformTab,
|
tab: platformBootstrap.platformTab,
|
||||||
@@ -2109,10 +2241,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
isBusy={isBigFishBusy}
|
isBusy={isBigFishBusy}
|
||||||
error={bigFishError}
|
error={bigFishError}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setSelectionStage('big-fish-result');
|
setSelectionStage(
|
||||||
|
bigFishSession ? 'big-fish-result' : 'platform',
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onRestart={() => {
|
onRestart={() => {
|
||||||
void startBigFishRun();
|
void restartBigFishRun();
|
||||||
}}
|
}}
|
||||||
onSubmitInput={submitBigFishInput}
|
onSubmitInput={submitBigFishInput}
|
||||||
/>
|
/>
|
||||||
@@ -2128,7 +2262,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
exit={{ opacity: 0, y: -12 }}
|
exit={{ opacity: 0, y: -12 }}
|
||||||
className="flex h-full min-h-0 flex-col"
|
className="flex h-full min-h-0 flex-col"
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LazyPanelFallback label="正在加载拼图创作..." />}>
|
<Suspense
|
||||||
|
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
|
||||||
|
>
|
||||||
<PuzzleAgentWorkspace
|
<PuzzleAgentWorkspace
|
||||||
session={puzzleSession}
|
session={puzzleSession}
|
||||||
activeOperation={puzzleOperation}
|
activeOperation={puzzleOperation}
|
||||||
@@ -2201,7 +2337,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
exit={{ opacity: 0, y: -12 }}
|
exit={{ opacity: 0, y: -12 }}
|
||||||
className="flex h-full min-h-0 flex-col"
|
className="flex h-full min-h-0 flex-col"
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LazyPanelFallback label="正在加载拼图结果..." />}>
|
<Suspense
|
||||||
|
fallback={<LazyPanelFallback label="正在加载拼图结果..." />}
|
||||||
|
>
|
||||||
<PuzzleResultView
|
<PuzzleResultView
|
||||||
session={puzzleSession}
|
session={puzzleSession}
|
||||||
author={authUi?.user ?? null}
|
author={authUi?.user ?? null}
|
||||||
@@ -2226,7 +2364,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
exit={{ opacity: 0, y: -12 }}
|
exit={{ opacity: 0, y: -12 }}
|
||||||
className="flex h-full min-h-0 flex-col"
|
className="flex h-full min-h-0 flex-col"
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LazyPanelFallback label="正在加载拼图详情..." />}>
|
<Suspense
|
||||||
|
fallback={<LazyPanelFallback label="正在加载拼图详情..." />}
|
||||||
|
>
|
||||||
<PuzzleGalleryDetailView
|
<PuzzleGalleryDetailView
|
||||||
item={selectedPuzzleDetail}
|
item={selectedPuzzleDetail}
|
||||||
isBusy={isPuzzleBusy}
|
isBusy={isPuzzleBusy}
|
||||||
@@ -2249,7 +2389,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onStartGame={() => {
|
onStartGame={() => {
|
||||||
void startPuzzleRunFromProfile(selectedPuzzleDetail.profileId);
|
void startPuzzleRunFromProfile(
|
||||||
|
selectedPuzzleDetail.profileId,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -2264,7 +2406,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="fixed inset-0 z-[100]"
|
className="fixed inset-0 z-[100]"
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}>
|
<Suspense
|
||||||
|
fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}
|
||||||
|
>
|
||||||
<PuzzleRuntimeShell
|
<PuzzleRuntimeShell
|
||||||
run={puzzleRun}
|
run={puzzleRun}
|
||||||
isBusy={isPuzzleBusy || isPuzzleNextLevelGenerating}
|
isBusy={isPuzzleBusy || isPuzzleNextLevelGenerating}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
|||||||
import { beforeEach, expect, test, vi } from 'vitest';
|
import { beforeEach, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||||
import { ApiClientError } from '../../services/apiClient';
|
import { ApiClientError } from '../../services/apiClient';
|
||||||
@@ -15,6 +16,8 @@ import {
|
|||||||
executeBigFishCreationAction,
|
executeBigFishCreationAction,
|
||||||
getBigFishCreationSession,
|
getBigFishCreationSession,
|
||||||
} from '../../services/big-fish-creation';
|
} from '../../services/big-fish-creation';
|
||||||
|
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||||
|
import { startBigFishRuntimeRun } from '../../services/big-fish-runtime';
|
||||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||||
import {
|
import {
|
||||||
createPuzzleAgentSession,
|
createPuzzleAgentSession,
|
||||||
@@ -145,6 +148,15 @@ vi.mock('../../services/big-fish-works', () => ({
|
|||||||
listBigFishWorks: vi.fn(),
|
listBigFishWorks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../services/big-fish-gallery', () => ({
|
||||||
|
listBigFishGallery: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../services/big-fish-runtime', () => ({
|
||||||
|
startBigFishRuntimeRun: vi.fn(),
|
||||||
|
submitBigFishRuntimeInput: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../services/puzzle-agent', () => ({
|
vi.mock('../../services/puzzle-agent', () => ({
|
||||||
createPuzzleAgentSession: vi.fn(),
|
createPuzzleAgentSession: vi.fn(),
|
||||||
executePuzzleAgentAction: vi.fn(),
|
executePuzzleAgentAction: vi.fn(),
|
||||||
@@ -152,6 +164,69 @@ vi.mock('../../services/puzzle-agent', () => ({
|
|||||||
streamPuzzleAgentMessage: vi.fn(),
|
streamPuzzleAgentMessage: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
|
||||||
|
PuzzleAgentWorkspace: ({
|
||||||
|
session,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
session: { sessionId: string; messages: Array<{ text: string }> } | null;
|
||||||
|
onBack: () => void;
|
||||||
|
}) => (
|
||||||
|
<div className="puzzle-agent-workspace-mock">
|
||||||
|
<div>拼图工作区:{session?.sessionId ?? 'missing-session'}</div>
|
||||||
|
{session?.messages.map((message) => (
|
||||||
|
<div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
|
||||||
|
))}
|
||||||
|
<button type="button" onClick={onBack}>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../puzzle-result/PuzzleResultView', () => ({
|
||||||
|
PuzzleResultView: ({
|
||||||
|
session,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
session: { draft?: { levelName: string } | null };
|
||||||
|
onBack: () => void;
|
||||||
|
}) => (
|
||||||
|
<div className="puzzle-result-view-mock">
|
||||||
|
<div>拼图结果页</div>
|
||||||
|
<label>
|
||||||
|
关卡名
|
||||||
|
<input readOnly value={session.draft?.levelName ?? ''} />
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onBack}>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../puzzle-gallery/PuzzleGalleryDetailView', () => ({
|
||||||
|
PuzzleGalleryDetailView: ({
|
||||||
|
item,
|
||||||
|
onBack,
|
||||||
|
onStartGame,
|
||||||
|
}: {
|
||||||
|
item: { levelName: string };
|
||||||
|
onBack: () => void;
|
||||||
|
onStartGame: () => void;
|
||||||
|
}) => (
|
||||||
|
<div className="puzzle-gallery-detail-view-mock">
|
||||||
|
<div>{item.levelName}</div>
|
||||||
|
<button type="button" onClick={onStartGame}>
|
||||||
|
进入第 1 关
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onBack}>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../big-fish-creation/BigFishAgentWorkspace', () => ({
|
vi.mock('../big-fish-creation/BigFishAgentWorkspace', () => ({
|
||||||
BigFishAgentWorkspace: ({
|
BigFishAgentWorkspace: ({
|
||||||
session,
|
session,
|
||||||
@@ -232,8 +307,7 @@ const mockSession: CustomWorldAgentSessionSnapshot = {
|
|||||||
'玩家以返乡守灯人继承者身份切入,回港首夜撞见禁航区假航灯重亮,动机是阻止更多船只误入死潮。',
|
'玩家以返乡守灯人继承者身份切入,回港首夜撞见禁航区假航灯重亮,动机是阻止更多船只误入死潮。',
|
||||||
coreConflict:
|
coreConflict:
|
||||||
'守灯会与航运公会争夺航路解释权,有人在借假航灯持续清洗旧案证据,玩家返乡当夜就被卷进封航冲突。',
|
'守灯会与航运公会争夺航路解释权,有人在借假航灯持续清洗旧案证据,玩家返乡当夜就被卷进封航冲突。',
|
||||||
keyRelationships:
|
keyRelationships: '玩家与沈砺旧友互疑,沈砺知道沉船夜的另一半真相。',
|
||||||
'玩家与沈砺旧友互疑,沈砺知道沉船夜的另一半真相。',
|
|
||||||
hiddenLines:
|
hiddenLines:
|
||||||
'沉船夜与假航灯骗局属于同一操盘链条,表面像海雾自然失控,揭示节奏是先见异常,再见旧案,再见操盘者。',
|
'沉船夜与假航灯骗局属于同一操盘链条,表面像海雾自然失控,揭示节奏是先见异常,再见旧案,再见操盘者。',
|
||||||
iconicElements:
|
iconicElements:
|
||||||
@@ -956,6 +1030,34 @@ beforeEach(() => {
|
|||||||
vi.mocked(listBigFishWorks).mockResolvedValue({
|
vi.mocked(listBigFishWorks).mockResolvedValue({
|
||||||
items: [],
|
items: [],
|
||||||
});
|
});
|
||||||
|
vi.mocked(listBigFishGallery).mockResolvedValue({
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
vi.mocked(startBigFishRuntimeRun).mockResolvedValue({
|
||||||
|
run: {
|
||||||
|
runId: 'big-fish-run-1',
|
||||||
|
sessionId: 'big-fish-session-public-1',
|
||||||
|
status: 'running',
|
||||||
|
tick: 0,
|
||||||
|
playerLevel: 1,
|
||||||
|
winLevel: 8,
|
||||||
|
leaderEntityId: 'owned-1',
|
||||||
|
ownedEntities: [
|
||||||
|
{
|
||||||
|
entityId: 'owned-1',
|
||||||
|
level: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
radius: 12,
|
||||||
|
offscreenSeconds: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
wildEntities: [],
|
||||||
|
cameraCenter: { x: 0, y: 0 },
|
||||||
|
lastInput: { x: 0, y: 0 },
|
||||||
|
eventLog: ['机械鱼群开始巡游。'],
|
||||||
|
updatedAt: '2026-04-25T12:12:00.000Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
||||||
items: [],
|
items: [],
|
||||||
});
|
});
|
||||||
@@ -1329,6 +1431,7 @@ test('creation hub clears all private work shelves immediately after logout stat
|
|||||||
{
|
{
|
||||||
workId: 'big-fish-logout-cache-1',
|
workId: 'big-fish-logout-cache-1',
|
||||||
sourceSessionId: 'big-fish-logout-cache-session',
|
sourceSessionId: 'big-fish-logout-cache-session',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
title: '大鱼退出缓存作品',
|
title: '大鱼退出缓存作品',
|
||||||
subtitle: '登出后不应继续可见',
|
subtitle: '登出后不应继续可见',
|
||||||
summary: '这条大鱼私有作品只能在登录态展示。',
|
summary: '这条大鱼私有作品只能在登录态展示。',
|
||||||
@@ -1427,6 +1530,48 @@ test('published puzzle works appear on home and category public shelves', async
|
|||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('published big fish works appear on home and category public shelves', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const publishedBigFishWork: BigFishWorkSummary = {
|
||||||
|
workId: 'big-fish-work-public-1',
|
||||||
|
sourceSessionId: 'big-fish-session-public-1',
|
||||||
|
ownerUserId: 'user-2',
|
||||||
|
title: '机械深海 大鱼吃小鱼',
|
||||||
|
subtitle: '机械微生物吞并进化',
|
||||||
|
summary: '从微光孢子一路吞并成长到深海巨鲲。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: '2026-04-25T10:30:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 8,
|
||||||
|
levelMainImageReadyCount: 8,
|
||||||
|
levelMotionReadyCount: 16,
|
||||||
|
backgroundReady: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listBigFishGallery).mockResolvedValue({
|
||||||
|
items: [publishedBigFishWork],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getAllByText('机械深海 大鱼吃小鱼').length).toBeGreaterThan(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: '分类' }));
|
||||||
|
|
||||||
|
const categoryPanel = getPlatformTabPanel('category');
|
||||||
|
expect(
|
||||||
|
within(categoryPanel).getAllByText('机械深海 大鱼吃小鱼').length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
within(categoryPanel).getAllByRole('button', { name: /大鱼/u }).length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('published puzzle detail returns to the source platform tab', async () => {
|
test('published puzzle detail returns to the source platform tab', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const publishedPuzzleWork = {
|
const publishedPuzzleWork = {
|
||||||
@@ -1797,8 +1942,9 @@ test('public code search opens a published puzzle by PZ code', async () => {
|
|||||||
|
|
||||||
render(<TestWrapper withAuth />);
|
render(<TestWrapper withAuth />);
|
||||||
|
|
||||||
const searchInput =
|
const searchInput = await screen.findByPlaceholderText(
|
||||||
await screen.findByPlaceholderText('输入 SY / CW / PZ 编号');
|
'输入 SY / CW / BF / PZ 编号',
|
||||||
|
);
|
||||||
await user.type(searchInput, 'PZ-EPUBLIC1');
|
await user.type(searchInput, 'PZ-EPUBLIC1');
|
||||||
await user.click(screen.getByRole('button', { name: '搜索' }));
|
await user.click(screen.getByRole('button', { name: '搜索' }));
|
||||||
|
|
||||||
@@ -1812,6 +1958,49 @@ test('public code search opens a published puzzle by PZ code', async () => {
|
|||||||
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
|
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('public code search opens a published big fish work by BF code', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const bigFishWork: BigFishWorkSummary = {
|
||||||
|
workId: 'big-fish-work-public-1',
|
||||||
|
sourceSessionId: 'big-fish-session-public-1',
|
||||||
|
ownerUserId: 'user-2',
|
||||||
|
title: '机械深海 大鱼吃小鱼',
|
||||||
|
subtitle: '机械微生物吞并进化',
|
||||||
|
summary: '从微光孢子一路吞并成长到深海巨鲲。',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: '2026-04-25T10:30:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 8,
|
||||||
|
levelMainImageReadyCount: 8,
|
||||||
|
levelMotionReadyCount: 16,
|
||||||
|
backgroundReady: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listBigFishGallery).mockResolvedValue({
|
||||||
|
items: [bigFishWork],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TestWrapper withAuth />);
|
||||||
|
|
||||||
|
const searchInput = await screen.findByPlaceholderText(
|
||||||
|
'输入 SY / CW / BF / PZ 编号',
|
||||||
|
);
|
||||||
|
await user.type(searchInput, 'BF-NPUBLIC1');
|
||||||
|
await user.click(screen.getByRole('button', { name: '搜索' }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(startBigFishRuntimeRun).toHaveBeenCalledWith(
|
||||||
|
'big-fish-session-public-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(await screen.findByText('Lv.1/8 · 进行中')).toBeTruthy();
|
||||||
|
expect(getBigFishCreationSession).not.toHaveBeenCalledWith(
|
||||||
|
'big-fish-session-public-1',
|
||||||
|
);
|
||||||
|
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test('big fish draft card restores the bound agent session and opens the result view', async () => {
|
test('big fish draft card restores the bound agent session and opens the result view', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
@@ -1820,6 +2009,7 @@ test('big fish draft card restores the bound agent session and opens the result
|
|||||||
{
|
{
|
||||||
workId: 'big-fish-work-big-fish-session-1',
|
workId: 'big-fish-work-big-fish-session-1',
|
||||||
sourceSessionId: 'big-fish-session-1',
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
title: '机械深海 大鱼吃小鱼',
|
title: '机械深海 大鱼吃小鱼',
|
||||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||||
summary: '机械微生物吞并进化',
|
summary: '机械微生物吞并进化',
|
||||||
@@ -1867,10 +2057,12 @@ test('big fish draft card restores the bound agent session and opens the result
|
|||||||
|
|
||||||
test('big fish result publish action refreshes creation works', async () => {
|
test('big fish result publish action refreshes creation works', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const baseBigFishSession = (await getBigFishCreationSession('big-fish-session-1'))
|
const baseBigFishSession = (
|
||||||
.session;
|
await getBigFishCreationSession('big-fish-session-1')
|
||||||
|
).session;
|
||||||
vi.mocked(getBigFishCreationSession).mockClear();
|
vi.mocked(getBigFishCreationSession).mockClear();
|
||||||
vi.mocked(listBigFishWorks).mockClear();
|
vi.mocked(listBigFishWorks).mockClear();
|
||||||
|
vi.mocked(listBigFishGallery).mockClear();
|
||||||
const publishedBigFishSession = {
|
const publishedBigFishSession = {
|
||||||
...baseBigFishSession,
|
...baseBigFishSession,
|
||||||
stage: 'published',
|
stage: 'published',
|
||||||
@@ -1894,6 +2086,7 @@ test('big fish result publish action refreshes creation works', async () => {
|
|||||||
{
|
{
|
||||||
workId: 'big-fish-work-big-fish-session-1',
|
workId: 'big-fish-work-big-fish-session-1',
|
||||||
sourceSessionId: 'big-fish-session-1',
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
title: '机械深海 大鱼吃小鱼',
|
title: '机械深海 大鱼吃小鱼',
|
||||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||||
summary: '机械微生物吞并进化',
|
summary: '机械微生物吞并进化',
|
||||||
@@ -1913,6 +2106,7 @@ test('big fish result publish action refreshes creation works', async () => {
|
|||||||
{
|
{
|
||||||
workId: 'big-fish-work-big-fish-session-1',
|
workId: 'big-fish-work-big-fish-session-1',
|
||||||
sourceSessionId: 'big-fish-session-1',
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
title: '机械深海 大鱼吃小鱼',
|
title: '机械深海 大鱼吃小鱼',
|
||||||
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||||
summary: '机械微生物吞并进化',
|
summary: '机械微生物吞并进化',
|
||||||
@@ -1959,6 +2153,9 @@ test('big fish result publish action refreshes creation works', async () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(listBigFishWorks).toHaveBeenCalled();
|
expect(listBigFishWorks).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(listBigFishGallery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { afterEach, expect, test, vi } from 'vitest';
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import { AuthUiContext } from '../auth/AuthUiContext';
|
import { AuthUiContext } from '../auth/AuthUiContext';
|
||||||
import { RpgEntryHomeView, type RpgEntryHomeViewProps } from './RpgEntryHomeView';
|
import {
|
||||||
|
RpgEntryHomeView,
|
||||||
|
type RpgEntryHomeViewProps,
|
||||||
|
} from './RpgEntryHomeView';
|
||||||
import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';
|
import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';
|
||||||
|
|
||||||
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||||
@@ -343,7 +346,9 @@ test('mobile home search submits public work code', async () => {
|
|||||||
</AuthUiContext.Provider>,
|
</AuthUiContext.Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchInput = screen.getByPlaceholderText('输入 SY / CW / PZ 编号');
|
const searchInput = screen.getByPlaceholderText(
|
||||||
|
'输入 SY / CW / BF / PZ 编号',
|
||||||
|
);
|
||||||
await user.type(searchInput, 'PZ-PROFILE1{enter}');
|
await user.type(searchInput, 'PZ-PROFILE1{enter}');
|
||||||
|
|
||||||
expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1');
|
expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1');
|
||||||
@@ -359,8 +364,9 @@ test('public gallery cards hide work code until detail is opened', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
|
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
|
||||||
expect(screen.queryByRole('button', { name: '复制作品号 PZ-EPUBLIC1' }))
|
expect(
|
||||||
.toBeNull();
|
screen.queryByRole('button', { name: '复制作品号 PZ-EPUBLIC1' }),
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /查看作品/u }));
|
await user.click(screen.getByRole('button', { name: /查看作品/u }));
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
buildPlatformWorldTags,
|
buildPlatformWorldTags,
|
||||||
describePlatformThemeLabel,
|
describePlatformThemeLabel,
|
||||||
formatPlatformWorldTime,
|
formatPlatformWorldTime,
|
||||||
|
isBigFishGalleryEntry,
|
||||||
isPuzzleGalleryEntry,
|
isPuzzleGalleryEntry,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
type PlatformWorldCardLike,
|
type PlatformWorldCardLike,
|
||||||
@@ -223,7 +224,7 @@ function PublicCodeSearchBar({
|
|||||||
onSubmit();
|
onSubmit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="输入 SY / CW / PZ 编号"
|
placeholder="输入 SY / CW / BF / PZ 编号"
|
||||||
className="w-full min-w-0 bg-transparent text-sm text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
|
className="w-full min-w-0 bg-transparent text-sm text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -665,9 +666,11 @@ function DesktopTrendingItem({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="platform-pill platform-pill--neutral px-2.5">
|
<span className="platform-pill platform-pill--neutral px-2.5">
|
||||||
{isPuzzleGalleryEntry(entry)
|
{isBigFishGalleryEntry(entry)
|
||||||
? '拼图'
|
? '大鱼'
|
||||||
: describePlatformThemeLabel(entry.themeMode)}
|
: isPuzzleGalleryEntry(entry)
|
||||||
|
? '拼图'
|
||||||
|
: describePlatformThemeLabel(entry.themeMode)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -714,13 +717,20 @@ function buildPublicCategoryGroups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
|
function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
|
||||||
return `${isPuzzleGalleryEntry(entry) ? 'puzzle' : 'rpg'}:${entry.ownerUserId}:${entry.profileId}`;
|
const kind = isBigFishGalleryEntry(entry)
|
||||||
|
? 'big-fish'
|
||||||
|
: isPuzzleGalleryEntry(entry)
|
||||||
|
? 'puzzle'
|
||||||
|
: 'rpg';
|
||||||
|
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||||
return isPuzzleGalleryEntry(entry)
|
return isBigFishGalleryEntry(entry)
|
||||||
? '拼图'
|
? '大鱼'
|
||||||
: describePlatformThemeLabel(entry.themeMode);
|
: isPuzzleGalleryEntry(entry)
|
||||||
|
? '拼图'
|
||||||
|
: describePlatformThemeLabel(entry.themeMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSnapshotTime(value: string | null | undefined) {
|
function formatSnapshotTime(value: string | null | undefined) {
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import type {
|
|||||||
CustomWorldGalleryCard,
|
CustomWorldGalleryCard,
|
||||||
CustomWorldLibraryEntry,
|
CustomWorldLibraryEntry,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||||
import { buildPuzzlePublicWorkCode } from '../../services/publicWorkCode';
|
import {
|
||||||
|
buildBigFishPublicWorkCode,
|
||||||
|
buildPuzzlePublicWorkCode,
|
||||||
|
} from '../../services/publicWorkCode';
|
||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
|
|
||||||
export type PlatformWorldCardLike =
|
export type PlatformWorldCardLike =
|
||||||
| CustomWorldGalleryCard
|
| CustomWorldGalleryCard
|
||||||
| CustomWorldLibraryEntry<CustomWorldProfile>
|
| CustomWorldLibraryEntry<CustomWorldProfile>
|
||||||
|
| PlatformBigFishGalleryCard
|
||||||
| PlatformPuzzleGalleryCard;
|
| PlatformPuzzleGalleryCard;
|
||||||
|
|
||||||
export type PlatformPuzzleGalleryCard = {
|
export type PlatformPuzzleGalleryCard = {
|
||||||
@@ -30,8 +35,26 @@ export type PlatformPuzzleGalleryCard = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PlatformBigFishGalleryCard = {
|
||||||
|
sourceType: 'big-fish';
|
||||||
|
workId: string;
|
||||||
|
profileId: string;
|
||||||
|
publicWorkCode: string;
|
||||||
|
ownerUserId: string;
|
||||||
|
authorDisplayName: string;
|
||||||
|
worldName: string;
|
||||||
|
subtitle: string;
|
||||||
|
summaryText: string;
|
||||||
|
coverImageSrc: string | null;
|
||||||
|
themeTags: string[];
|
||||||
|
visibility: 'published';
|
||||||
|
publishedAt: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PlatformPublicGalleryCard =
|
export type PlatformPublicGalleryCard =
|
||||||
| CustomWorldGalleryCard
|
| CustomWorldGalleryCard
|
||||||
|
| PlatformBigFishGalleryCard
|
||||||
| PlatformPuzzleGalleryCard;
|
| PlatformPuzzleGalleryCard;
|
||||||
|
|
||||||
export function isLibraryWorldEntry(
|
export function isLibraryWorldEntry(
|
||||||
@@ -46,6 +69,12 @@ export function isPuzzleGalleryEntry(
|
|||||||
return 'sourceType' in entry && entry.sourceType === 'puzzle';
|
return 'sourceType' in entry && entry.sourceType === 'puzzle';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBigFishGalleryEntry(
|
||||||
|
entry: PlatformWorldCardLike,
|
||||||
|
): entry is PlatformBigFishGalleryCard {
|
||||||
|
return 'sourceType' in entry && entry.sourceType === 'big-fish';
|
||||||
|
}
|
||||||
|
|
||||||
export function mapPuzzleWorkToPlatformGalleryCard(
|
export function mapPuzzleWorkToPlatformGalleryCard(
|
||||||
work: PuzzleWorkSummary,
|
work: PuzzleWorkSummary,
|
||||||
): PlatformPuzzleGalleryCard {
|
): PlatformPuzzleGalleryCard {
|
||||||
@@ -67,6 +96,27 @@ export function mapPuzzleWorkToPlatformGalleryCard(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapBigFishWorkToPlatformGalleryCard(
|
||||||
|
work: BigFishWorkSummary,
|
||||||
|
): PlatformBigFishGalleryCard {
|
||||||
|
return {
|
||||||
|
sourceType: 'big-fish',
|
||||||
|
workId: work.workId,
|
||||||
|
profileId: work.sourceSessionId,
|
||||||
|
publicWorkCode: buildBigFishPublicWorkCode(work.sourceSessionId),
|
||||||
|
ownerUserId: work.ownerUserId,
|
||||||
|
authorDisplayName: '大鱼创作者',
|
||||||
|
worldName: work.title,
|
||||||
|
subtitle: work.subtitle || '大鱼吃小鱼',
|
||||||
|
summaryText: work.summary,
|
||||||
|
coverImageSrc: work.coverImageSrc,
|
||||||
|
themeTags: ['大鱼', `${work.levelCount}级`],
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: work.updatedAt,
|
||||||
|
updatedAt: work.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePlatformWorldCoverImage(entry: PlatformWorldCardLike) {
|
export function resolvePlatformWorldCoverImage(entry: PlatformWorldCardLike) {
|
||||||
if (entry.coverImageSrc) {
|
if (entry.coverImageSrc) {
|
||||||
return entry.coverImageSrc;
|
return entry.coverImageSrc;
|
||||||
@@ -88,6 +138,10 @@ export function resolvePlatformWorldLeadPortrait(entry: PlatformWorldCardLike) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
|
export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
|
||||||
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
|
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['大鱼'];
|
||||||
|
}
|
||||||
|
|
||||||
if (isPuzzleGalleryEntry(entry)) {
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['拼图'];
|
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['拼图'];
|
||||||
}
|
}
|
||||||
@@ -128,6 +182,10 @@ export function formatPlatformWorldTime(value: string | null) {
|
|||||||
export function resolvePlatformPublicWorkCode(
|
export function resolvePlatformPublicWorkCode(
|
||||||
entry: PlatformWorldCardLike,
|
entry: PlatformWorldCardLike,
|
||||||
): string | null {
|
): string | null {
|
||||||
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
|
return entry.publicWorkCode;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPuzzleGalleryEntry(entry)) {
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
return entry.publicWorkCode;
|
return entry.publicWorkCode;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/services/big-fish-gallery/bigFishGalleryClient.ts
Normal file
29
src/services/big-fish-gallery/bigFishGalleryClient.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { BigFishWorksResponse } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||||
|
|
||||||
|
const BIG_FISH_GALLERY_API_BASE = '/api/runtime/big-fish/gallery';
|
||||||
|
const BIG_FISH_GALLERY_READ_RETRY: ApiRetryOptions = {
|
||||||
|
maxRetries: 1,
|
||||||
|
baseDelayMs: 120,
|
||||||
|
maxDelayMs: 360,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取大鱼吃小鱼公开广场列表。
|
||||||
|
*/
|
||||||
|
export async function listBigFishGallery() {
|
||||||
|
return requestJson<BigFishWorksResponse>(
|
||||||
|
BIG_FISH_GALLERY_API_BASE,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
'读取大鱼吃小鱼广场失败',
|
||||||
|
{
|
||||||
|
retry: BIG_FISH_GALLERY_READ_RETRY,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bigFishGalleryClient = {
|
||||||
|
list: listBigFishGallery,
|
||||||
|
};
|
||||||
4
src/services/big-fish-gallery/index.ts
Normal file
4
src/services/big-fish-gallery/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
bigFishGalleryClient,
|
||||||
|
listBigFishGallery,
|
||||||
|
} from './bigFishGalleryClient';
|
||||||
@@ -13,6 +13,14 @@ export function buildPuzzlePublicWorkCode(profileId: string) {
|
|||||||
return `PZ-${suffix}`;
|
return `PZ-${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildBigFishPublicWorkCode(sessionId: string) {
|
||||||
|
const normalized = normalizePublicCodeText(sessionId);
|
||||||
|
const fallback = normalized || '00000000';
|
||||||
|
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||||
|
|
||||||
|
return `BF-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
||||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||||
|
|
||||||
@@ -22,3 +30,16 @@ export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
|||||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSameBigFishPublicWorkCode(
|
||||||
|
keyword: string,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
|
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizedKeyword ===
|
||||||
|
normalizePublicCodeText(buildBigFishPublicWorkCode(sessionId)) ||
|
||||||
|
normalizedKeyword === normalizePublicCodeText(sessionId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user