This commit is contained in:
119
src/components/platform-entry/PlatformEntryCreationTypeModal.tsx
Normal file
119
src/components/platform-entry/PlatformEntryCreationTypeModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import { PLATFORM_CREATION_TYPES } from './platformEntryCreationTypes';
|
||||
|
||||
export interface PlatformEntryCreationTypeModalProps {
|
||||
isOpen: boolean;
|
||||
isBusy: boolean;
|
||||
error: string | null;
|
||||
onClose: () => void;
|
||||
onSelectRpg: () => void;
|
||||
onSelectBigFish: () => void;
|
||||
onSelectPuzzle: () => void;
|
||||
}
|
||||
|
||||
function CreationTypeCard(props: {
|
||||
item: (typeof PLATFORM_CREATION_TYPES)[number];
|
||||
busy: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const { item, busy, onSelect } = props;
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onSelect}
|
||||
className={`platform-interactive-card relative overflow-hidden rounded-[1.65rem] border px-4 py-4 text-left ${
|
||||
item.locked
|
||||
? 'cursor-not-allowed border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] text-[var(--platform-text-soft)]'
|
||||
: 'border-[var(--platform-cool-border)] bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.24),transparent_34%),linear-gradient(135deg,rgba(255,79,139,0.96),rgba(255,145,110,0.9))] text-white'
|
||||
} ${busy && !item.locked ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span
|
||||
className={`platform-pill px-3 ${
|
||||
item.locked
|
||||
? 'platform-pill--neutral text-[var(--platform-text-soft)]'
|
||||
: 'platform-pill--neutral border-white/30 bg-white/18 text-white'
|
||||
}`}
|
||||
>
|
||||
{item.locked ? item.badge : busy ? '正在开启' : item.badge}
|
||||
</span>
|
||||
{item.locked ? (
|
||||
<span className="text-lg leading-none text-white/45">·</span>
|
||||
) : (
|
||||
<ArrowRight className="h-4 w-4 text-white/80" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 text-xl font-black leading-tight text-inherit">
|
||||
{item.title}
|
||||
</div>
|
||||
<div
|
||||
className={`mt-2 text-sm ${
|
||||
item.locked ? 'text-zinc-500' : 'text-zinc-200/82'
|
||||
}`}
|
||||
>
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台入口创作类型弹层。
|
||||
* 多玩法入口统一在这里分流,避免把非 RPG 玩法写进 RPG 命名脚本。
|
||||
*/
|
||||
export function PlatformEntryCreationTypeModal({
|
||||
isOpen,
|
||||
isBusy,
|
||||
error,
|
||||
onClose,
|
||||
onSelectRpg,
|
||||
onSelectBigFish,
|
||||
onSelectPuzzle,
|
||||
}: PlatformEntryCreationTypeModalProps) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={isOpen}
|
||||
title="选择创作类型"
|
||||
description="先选玩法类型,再进入对应创作工作台。"
|
||||
onClose={onClose}
|
||||
closeDisabled={isBusy}
|
||||
size="lg"
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-5">
|
||||
{PLATFORM_CREATION_TYPES.map((item) => (
|
||||
<CreationTypeCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
busy={isBusy}
|
||||
onSelect={() => {
|
||||
if (item.id === 'rpg') {
|
||||
onSelectRpg();
|
||||
}
|
||||
if (item.id === 'big-fish') {
|
||||
onSelectBigFish();
|
||||
}
|
||||
if (item.id === 'puzzle') {
|
||||
onSelectPuzzle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger mt-4 rounded-[1.25rem] text-sm leading-6">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
17
src/components/platform-entry/PlatformEntryFlowShell.tsx
Normal file
17
src/components/platform-entry/PlatformEntryFlowShell.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PlatformEntryFlowShellImpl } from './PlatformEntryFlowShellImpl';
|
||||
import type {
|
||||
PlatformEntryFlowShellProps,
|
||||
SelectionStage,
|
||||
} from './platformEntryTypes';
|
||||
|
||||
export type { PlatformEntryFlowShellProps, SelectionStage };
|
||||
|
||||
/**
|
||||
* 平台入口通用壳层。
|
||||
* RPG、Big Fish 等玩法创作入口在这里并列分流。
|
||||
*/
|
||||
export function PlatformEntryFlowShell(props: PlatformEntryFlowShellProps) {
|
||||
return <PlatformEntryFlowShellImpl {...props} />;
|
||||
}
|
||||
|
||||
export default PlatformEntryFlowShell;
|
||||
2593
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
Normal file
2593
src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
Normal file
File diff suppressed because it is too large
Load Diff
9
src/components/platform-entry/PlatformEntryHomeView.tsx
Normal file
9
src/components/platform-entry/PlatformEntryHomeView.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 平台首页视图的通用出口。
|
||||
* 当前先复用成熟的首页实现,但避免让 `platform-entry` 继续直接依赖 RPG 命名组件。
|
||||
*/
|
||||
export {
|
||||
RpgEntryHomeView as PlatformEntryHomeView,
|
||||
type RpgEntryHomeViewProps as PlatformEntryHomeViewProps,
|
||||
type PlatformHomeTab,
|
||||
} from '../rpg-entry/RpgEntryHomeView';
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 平台作品详情视图的通用出口。
|
||||
* 这里保留平台语义封装,减少 Big Fish 继续挂在 RPG 命名路径上的误导。
|
||||
*/
|
||||
export {
|
||||
RpgEntryWorldDetailView as PlatformEntryWorldDetailView,
|
||||
type RpgEntryWorldDetailViewProps as PlatformEntryWorldDetailViewProps,
|
||||
} from '../rpg-entry/RpgEntryWorldDetailView';
|
||||
9
src/components/platform-entry/index.ts
Normal file
9
src/components/platform-entry/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
PlatformEntryFlowShell,
|
||||
type PlatformEntryFlowShellProps,
|
||||
type SelectionStage,
|
||||
} from './PlatformEntryFlowShell';
|
||||
export {
|
||||
PlatformEntryCreationTypeModal,
|
||||
type PlatformEntryCreationTypeModalProps,
|
||||
} from './PlatformEntryCreationTypeModal';
|
||||
55
src/components/platform-entry/platformEntryCreationTypes.ts
Normal file
55
src/components/platform-entry/platformEntryCreationTypes.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export type PlatformCreationTypeId =
|
||||
| 'rpg'
|
||||
| 'big-fish'
|
||||
| 'puzzle'
|
||||
| 'airp'
|
||||
| 'visual-novel';
|
||||
|
||||
export type PlatformCreationTypeCard = {
|
||||
id: PlatformCreationTypeId;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
|
||||
*/
|
||||
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
|
||||
{
|
||||
id: 'rpg',
|
||||
title: '角色扮演 RPG',
|
||||
subtitle: 'Agent 共创',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
},
|
||||
{
|
||||
id: 'big-fish',
|
||||
title: '大鱼吃小鱼',
|
||||
subtitle: '实时成长玩法',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
},
|
||||
{
|
||||
id: 'puzzle',
|
||||
title: '拼图玩法',
|
||||
subtitle: '图像锚点共创',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
},
|
||||
{
|
||||
id: 'airp',
|
||||
title: 'AIRP',
|
||||
subtitle: '敬请期待',
|
||||
badge: '锁定',
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel',
|
||||
title: '视觉小说',
|
||||
subtitle: '敬请期待',
|
||||
badge: '锁定',
|
||||
locked: true,
|
||||
},
|
||||
];
|
||||
9
src/components/platform-entry/platformEntryShared.ts
Normal file
9
src/components/platform-entry/platformEntryShared.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 平台入口共享 helper 的通用封装层。
|
||||
* 先复用既有实现,同时把多玩法入口依赖从 RPG 命名中隔离出来。
|
||||
*/
|
||||
export {
|
||||
buildCreationHubFallbackItems,
|
||||
normalizeAgentBackedProfile,
|
||||
resolveRpgCreationErrorMessage,
|
||||
} from '../rpg-entry/rpgEntryShared';
|
||||
42
src/components/platform-entry/platformEntryTypes.ts
Normal file
42
src/components/platform-entry/platformEntryTypes.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
export type SelectionStage =
|
||||
| 'platform'
|
||||
| 'detail'
|
||||
| 'agent-workspace'
|
||||
| 'big-fish-agent-workspace'
|
||||
| 'big-fish-generating'
|
||||
| 'big-fish-result'
|
||||
| 'big-fish-runtime'
|
||||
| 'puzzle-agent-workspace'
|
||||
| 'puzzle-generating'
|
||||
| 'puzzle-result'
|
||||
| 'puzzle-gallery-detail'
|
||||
| 'puzzle-runtime'
|
||||
| 'custom-world-generating'
|
||||
| 'custom-world-result';
|
||||
|
||||
export type CustomWorldGenerationViewSource = 'agent-draft-foundation' | null;
|
||||
|
||||
export type CustomWorldResultViewSource = 'saved-profile' | 'agent-draft' | null;
|
||||
|
||||
export type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
export type SyncedAgentDraftResult = {
|
||||
session: CustomWorldAgentSessionSnapshot | null;
|
||||
profile: CustomWorldProfile | null;
|
||||
};
|
||||
|
||||
export type PlatformEntryFlowShellProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
hasSavedGame: boolean;
|
||||
savedSnapshot: HydratedSavedGameSnapshot | null;
|
||||
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
||||
handleStartNewGame: () => void;
|
||||
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
|
||||
};
|
||||
@@ -0,0 +1,311 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { TextStreamOptions } from '../../services/aiTypes';
|
||||
import type { SelectionStage } from './platformEntryTypes';
|
||||
|
||||
type CreationAgentMessageLike = {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type CreationAgentSessionLike = {
|
||||
sessionId: string;
|
||||
draft?: unknown;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
role: string;
|
||||
kind?: string;
|
||||
text: string;
|
||||
createdAt?: string;
|
||||
}>;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
type CreationAgentClientAdapter<
|
||||
TSession extends CreationAgentSessionLike,
|
||||
TCreatePayload,
|
||||
TCreateResponse,
|
||||
TMessagePayload extends CreationAgentMessageLike,
|
||||
TActionPayload,
|
||||
TActionResponse,
|
||||
> = {
|
||||
createSession: (payload: TCreatePayload) => Promise<TCreateResponse>;
|
||||
getSession: (sessionId: string) => Promise<TCreateResponse>;
|
||||
streamMessage: (
|
||||
sessionId: string,
|
||||
payload: TMessagePayload,
|
||||
options?: TextStreamOptions,
|
||||
) => Promise<TSession>;
|
||||
executeAction: (
|
||||
sessionId: string,
|
||||
payload: TActionPayload,
|
||||
) => Promise<TActionResponse>;
|
||||
selectSession: (response: TCreateResponse) => TSession;
|
||||
};
|
||||
|
||||
type PlatformCreationAgentFlowControllerOptions<
|
||||
TSession extends CreationAgentSessionLike,
|
||||
TCreatePayload,
|
||||
TCreateResponse,
|
||||
TMessagePayload extends CreationAgentMessageLike,
|
||||
TActionPayload,
|
||||
TActionResponse,
|
||||
> = {
|
||||
client: CreationAgentClientAdapter<
|
||||
TSession,
|
||||
TCreatePayload,
|
||||
TCreateResponse,
|
||||
TMessagePayload,
|
||||
TActionPayload,
|
||||
TActionResponse
|
||||
>;
|
||||
createPayload: TCreatePayload;
|
||||
workspaceStage: SelectionStage;
|
||||
resultStage: SelectionStage;
|
||||
platformStage: SelectionStage;
|
||||
isCompileAction: (payload: TActionPayload) => boolean;
|
||||
resolveErrorMessage: (error: unknown, fallback: string) => string;
|
||||
errorMessages: {
|
||||
open: string;
|
||||
restoreMissingSession: string;
|
||||
restore: string;
|
||||
submit: string;
|
||||
execute: string;
|
||||
};
|
||||
enterCreateTab: () => void;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
onSessionOpened?: () => void;
|
||||
onActionComplete?: (params: {
|
||||
payload: TActionPayload;
|
||||
response: TActionResponse;
|
||||
session: TSession;
|
||||
setSession: (session: TSession) => void;
|
||||
}) => Promise<void> | void;
|
||||
beforeExecuteAction?: (params: {
|
||||
payload: TActionPayload;
|
||||
session: TSession;
|
||||
}) => void;
|
||||
onActionError?: (params: {
|
||||
payload: TActionPayload;
|
||||
error: unknown;
|
||||
errorMessage: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
function buildOptimisticMessage<TMessagePayload extends CreationAgentMessageLike>(
|
||||
payload: TMessagePayload,
|
||||
) {
|
||||
return {
|
||||
id: payload.clientMessageId,
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: payload.text.trim(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量作品 Agent 创作流程的通用前端控制器。
|
||||
* 这里只处理跨玩法一致的会话、流式消息、忙碌态与草稿恢复,玩法结果页和运行态动作留给外层。
|
||||
*/
|
||||
export function usePlatformCreationAgentFlowController<
|
||||
TSession extends CreationAgentSessionLike,
|
||||
TCreatePayload,
|
||||
TCreateResponse,
|
||||
TMessagePayload extends CreationAgentMessageLike,
|
||||
TActionPayload,
|
||||
TActionResponse,
|
||||
>(
|
||||
options: PlatformCreationAgentFlowControllerOptions<
|
||||
TSession,
|
||||
TCreatePayload,
|
||||
TCreateResponse,
|
||||
TMessagePayload,
|
||||
TActionPayload,
|
||||
TActionResponse
|
||||
>,
|
||||
) {
|
||||
const [session, setSession] = useState<TSession | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [streamingReplyText, setStreamingReplyText] = useState('');
|
||||
const [isStreamingReply, setIsStreamingReply] = useState(false);
|
||||
|
||||
const openWorkspace = useCallback(async () => {
|
||||
if (isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBusy(true);
|
||||
setError(null);
|
||||
setStreamingReplyText('');
|
||||
setIsStreamingReply(false);
|
||||
|
||||
try {
|
||||
const response = await options.client.createSession(options.createPayload);
|
||||
setSession(options.client.selectSession(response));
|
||||
options.enterCreateTab();
|
||||
options.onSessionOpened?.();
|
||||
options.setSelectionStage(options.workspaceStage);
|
||||
} catch (caughtError) {
|
||||
setError(
|
||||
options.resolveErrorMessage(caughtError, options.errorMessages.open),
|
||||
);
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
}, [isBusy, options]);
|
||||
|
||||
const restoreDraft = useCallback(
|
||||
async (sessionId: string | null | undefined) => {
|
||||
const normalizedSessionId = sessionId?.trim();
|
||||
if (!normalizedSessionId) {
|
||||
setError(options.errorMessages.restoreMissingSession);
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsBusy(true);
|
||||
setError(null);
|
||||
setStreamingReplyText('');
|
||||
setIsStreamingReply(false);
|
||||
|
||||
try {
|
||||
const response = await options.client.getSession(normalizedSessionId);
|
||||
const nextSession = options.client.selectSession(response);
|
||||
setSession(nextSession);
|
||||
options.enterCreateTab();
|
||||
options.setSelectionStage(
|
||||
nextSession.draft ? options.resultStage : options.workspaceStage,
|
||||
);
|
||||
return nextSession;
|
||||
} catch (caughtError) {
|
||||
setError(
|
||||
options.resolveErrorMessage(caughtError, options.errorMessages.restore),
|
||||
);
|
||||
options.enterCreateTab();
|
||||
options.setSelectionStage(options.platformStage);
|
||||
return null;
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const submitMessage = useCallback(
|
||||
async (payload: TMessagePayload) => {
|
||||
if (!session || isStreamingReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optimisticMessage = buildOptimisticMessage(payload);
|
||||
|
||||
setError(null);
|
||||
setStreamingReplyText('');
|
||||
setIsStreamingReply(true);
|
||||
setSession((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
messages: [...current.messages, optimisticMessage],
|
||||
updatedAt: optimisticMessage.createdAt,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
|
||||
try {
|
||||
const nextSession = await options.client.streamMessage(
|
||||
session.sessionId,
|
||||
payload,
|
||||
{
|
||||
onUpdate: setStreamingReplyText,
|
||||
},
|
||||
);
|
||||
setSession(nextSession);
|
||||
setStreamingReplyText('');
|
||||
} catch (caughtError) {
|
||||
setError(
|
||||
options.resolveErrorMessage(caughtError, options.errorMessages.submit),
|
||||
);
|
||||
} finally {
|
||||
setIsStreamingReply(false);
|
||||
}
|
||||
},
|
||||
[isStreamingReply, options, session],
|
||||
);
|
||||
|
||||
const executeAction = useCallback(
|
||||
async (payload: TActionPayload) => {
|
||||
if (!session || isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBusy(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
options.beforeExecuteAction?.({ payload, session });
|
||||
const response = await options.client.executeAction(
|
||||
session.sessionId,
|
||||
payload,
|
||||
);
|
||||
await options.onActionComplete?.({
|
||||
payload,
|
||||
response,
|
||||
session,
|
||||
setSession,
|
||||
});
|
||||
if (options.isCompileAction(payload)) {
|
||||
options.setSelectionStage(options.resultStage);
|
||||
}
|
||||
} catch (caughtError) {
|
||||
const errorMessage = options.resolveErrorMessage(
|
||||
caughtError,
|
||||
options.errorMessages.execute,
|
||||
);
|
||||
setError(errorMessage);
|
||||
options.onActionError?.({
|
||||
payload,
|
||||
error: caughtError,
|
||||
errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
},
|
||||
[isBusy, options, session],
|
||||
);
|
||||
|
||||
const leaveFlow = useCallback(() => {
|
||||
setError(null);
|
||||
setStreamingReplyText('');
|
||||
setIsStreamingReply(false);
|
||||
options.enterCreateTab();
|
||||
options.setSelectionStage(options.platformStage);
|
||||
}, [options]);
|
||||
|
||||
const resetTransientState = useCallback(() => {
|
||||
setError(null);
|
||||
setStreamingReplyText('');
|
||||
setIsStreamingReply(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
session,
|
||||
setSession,
|
||||
error,
|
||||
setError,
|
||||
isBusy,
|
||||
setIsBusy,
|
||||
streamingReplyText,
|
||||
setStreamingReplyText,
|
||||
isStreamingReply,
|
||||
setIsStreamingReply,
|
||||
openWorkspace,
|
||||
restoreDraft,
|
||||
submitMessage,
|
||||
executeAction,
|
||||
leaveFlow,
|
||||
resetTransientState,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 平台入口 bootstrap 通用封装。
|
||||
* 现阶段逻辑仍复用既有实现,但对外暴露平台语义命名。
|
||||
*/
|
||||
export { useRpgEntryBootstrap as usePlatformEntryBootstrap } from '../rpg-entry/useRpgEntryBootstrap';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 平台入口详情态编排通用封装。
|
||||
* 通过平台语义出口避免 Big Fish 直接依赖 RPG 命名 hook。
|
||||
*/
|
||||
export { useRpgEntryLibraryDetail as usePlatformEntryLibraryDetail } from '../rpg-entry/useRpgEntryLibraryDetail';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 平台入口导航通用封装。
|
||||
* 多玩法统一从 `platform-entry` 暴露,RPG 目录只保留兼容与 RPG 专属能力。
|
||||
*/
|
||||
export { useRpgEntryNavigation as usePlatformEntryNavigation } from '../rpg-entry/useRpgEntryNavigation';
|
||||
Reference in New Issue
Block a user