1ad25e30f8
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
598 lines
19 KiB
TypeScript
598 lines
19 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
|
import type {
|
|
CustomWorldGalleryCard,
|
|
CustomWorldLibraryEntry,
|
|
PlatformBrowseHistoryWriteEntry,
|
|
} from '../../../packages/shared/src/contracts/runtime';
|
|
import {
|
|
buildPublicWorkDetailPath,
|
|
pushAppHistoryPath,
|
|
} from '../../routing/appPageRoutes';
|
|
import { ApiClientError } from '../../services/apiClient';
|
|
import {
|
|
deleteRpgEntryWorldProfile,
|
|
getRpgEntryWorldGalleryDetail,
|
|
getRpgEntryWorldLibraryDetail,
|
|
listRpgEntryWorldLibrary,
|
|
publishRpgEntryWorldProfile,
|
|
unpublishRpgEntryWorldProfile,
|
|
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
|
|
import type {
|
|
CustomWorldAutoSaveState,
|
|
CustomWorldGenerationViewSource,
|
|
CustomWorldResultViewSource,
|
|
SelectionStage,
|
|
} from './rpgEntryTypes';
|
|
import {
|
|
countCustomWorldProfileAssetSlots,
|
|
countCustomWorldProfileDetailSlots,
|
|
countCustomWorldProfileStructuredSlots,
|
|
} from './rpgProfileCompleteness';
|
|
|
|
type UseRpgEntryLibraryDetailParams = {
|
|
userId: string | null | undefined;
|
|
selectedDetailEntry: CustomWorldLibraryEntry<CustomWorldProfile> | null;
|
|
setSelectedDetailEntry: (
|
|
entry:
|
|
| CustomWorldLibraryEntry<CustomWorldProfile>
|
|
| null
|
|
| ((
|
|
current: CustomWorldLibraryEntry<CustomWorldProfile> | null,
|
|
) => CustomWorldLibraryEntry<CustomWorldProfile> | null),
|
|
) => void;
|
|
savedCustomWorldEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
|
|
setSavedCustomWorldEntries: (
|
|
entries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
|
) => void;
|
|
setGeneratedCustomWorldProfile: (profile: CustomWorldProfile | null) => void;
|
|
setCustomWorldError: (error: string | null) => void;
|
|
setCustomWorldAutoSaveError: (error: string | null) => void;
|
|
setCustomWorldAutoSaveState: (state: CustomWorldAutoSaveState) => void;
|
|
setCustomWorldGenerationViewSource: (
|
|
source: CustomWorldGenerationViewSource,
|
|
) => void;
|
|
setCustomWorldResultViewSource: (source: CustomWorldResultViewSource) => void;
|
|
setSelectionStage: (stage: SelectionStage) => void;
|
|
setPlatformTabToCreate: () => void;
|
|
setPlatformTabToDraft: () => void;
|
|
setPlatformError: (error: string | null) => void;
|
|
appendBrowseHistoryEntry: (
|
|
entry: PlatformBrowseHistoryWriteEntry,
|
|
) => Promise<void>;
|
|
refreshCustomWorldWorks: () => Promise<unknown>;
|
|
refreshPublishedGallery: () => Promise<unknown>;
|
|
persistAgentUiState: (
|
|
sessionId: string | null,
|
|
operationId: string | null,
|
|
generationSource?: 'agent-draft-foundation' | null,
|
|
) => void;
|
|
syncAgentCreationResultView: (
|
|
sessionId: string,
|
|
) => Promise<RpgCreationResultView | null>;
|
|
buildDraftResultProfile: (
|
|
view: RpgCreationResultView | null,
|
|
) => CustomWorldProfile | null;
|
|
suppressAgentDraftResultAutoOpen: () => void;
|
|
releaseAgentDraftResultAutoOpenSuppression: () => void;
|
|
resetAutoSaveTrackingToIdle: () => void;
|
|
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
|
|
};
|
|
|
|
function isMissingRpgEntryAgentSessionError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
error.status === 404 &&
|
|
error.code === 'NOT_FOUND'
|
|
);
|
|
}
|
|
|
|
function shouldKeepSelectedDetailProfile(
|
|
selectedEntry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
|
nextOwnedEntry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
|
) {
|
|
if (
|
|
selectedEntry.ownerUserId !== nextOwnedEntry.ownerUserId ||
|
|
selectedEntry.profileId !== nextOwnedEntry.profileId
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const selectedDetailCount = countCustomWorldProfileDetailSlots(
|
|
selectedEntry.profile,
|
|
);
|
|
const nextDetailCount = countCustomWorldProfileDetailSlots(
|
|
nextOwnedEntry.profile,
|
|
);
|
|
const selectedAssetSlotCount = countCustomWorldProfileAssetSlots(
|
|
selectedEntry.profile,
|
|
);
|
|
const nextAssetSlotCount = countCustomWorldProfileAssetSlots(
|
|
nextOwnedEntry.profile,
|
|
);
|
|
const selectedStructuredSlotCount = countCustomWorldProfileStructuredSlots(
|
|
selectedEntry.profile,
|
|
);
|
|
const nextStructuredSlotCount = countCustomWorldProfileStructuredSlots(
|
|
nextOwnedEntry.profile,
|
|
);
|
|
const expectedRuntimeCount =
|
|
nextOwnedEntry.playableNpcCount + nextOwnedEntry.landmarkCount;
|
|
|
|
// 作品架列表只保证卡片摘要,不能在详情接口已经拿到完整运行态字段后覆盖详情。
|
|
return (
|
|
(selectedDetailCount > nextDetailCount &&
|
|
expectedRuntimeCount > nextDetailCount) ||
|
|
selectedAssetSlotCount > nextAssetSlotCount ||
|
|
selectedStructuredSlotCount > nextStructuredSlotCount
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 负责平台详情、创作作品入口和结果页打开路径。
|
|
* 平台壳层只消费“打开哪个面板”的结果,不再自己拼接恢复流程细节。
|
|
*/
|
|
export function useRpgEntryLibraryDetail(
|
|
params: UseRpgEntryLibraryDetailParams,
|
|
) {
|
|
const {
|
|
userId,
|
|
selectedDetailEntry,
|
|
setSelectedDetailEntry,
|
|
savedCustomWorldEntries,
|
|
setSavedCustomWorldEntries,
|
|
setGeneratedCustomWorldProfile,
|
|
setCustomWorldError,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setSelectionStage,
|
|
setPlatformTabToCreate,
|
|
setPlatformTabToDraft,
|
|
setPlatformError,
|
|
appendBrowseHistoryEntry,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
persistAgentUiState,
|
|
syncAgentCreationResultView,
|
|
buildDraftResultProfile,
|
|
suppressAgentDraftResultAutoOpen,
|
|
releaseAgentDraftResultAutoOpenSuppression,
|
|
resetAutoSaveTrackingToIdle,
|
|
markAutoSavedProfile,
|
|
} = params;
|
|
|
|
const [detailError, setDetailError] = useState<string | null>(null);
|
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
|
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!selectedDetailEntry) {
|
|
return;
|
|
}
|
|
|
|
const nextOwnedEntry = savedCustomWorldEntries.find(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
);
|
|
if (nextOwnedEntry && nextOwnedEntry !== selectedDetailEntry) {
|
|
if (
|
|
shouldKeepSelectedDetailProfile(selectedDetailEntry, nextOwnedEntry)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setSelectedDetailEntry(nextOwnedEntry);
|
|
}
|
|
}, [savedCustomWorldEntries, selectedDetailEntry, setSelectedDetailEntry]);
|
|
|
|
const openLibraryDetail = useCallback(
|
|
async (entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
|
if (entry.visibility === 'published') {
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: entry.ownerUserId,
|
|
profileId: entry.profileId,
|
|
worldName: entry.worldName,
|
|
subtitle: entry.subtitle,
|
|
summaryText: entry.summaryText,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
themeMode: entry.themeMode,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
});
|
|
}
|
|
setSelectedDetailEntry(entry);
|
|
setDetailError(null);
|
|
setSelectionStage('detail');
|
|
if (entry.publicWorkCode?.trim()) {
|
|
pushAppHistoryPath(buildPublicWorkDetailPath(entry.publicWorkCode));
|
|
}
|
|
|
|
if (!userId || entry.ownerUserId !== userId) {
|
|
return;
|
|
}
|
|
|
|
setIsDetailLoading(true);
|
|
try {
|
|
const detailEntry = await getRpgEntryWorldLibraryDetail(
|
|
entry.profileId,
|
|
);
|
|
setSelectedDetailEntry(detailEntry);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
} finally {
|
|
setIsDetailLoading(false);
|
|
}
|
|
},
|
|
[
|
|
appendBrowseHistoryEntry,
|
|
setSelectedDetailEntry,
|
|
setSelectionStage,
|
|
userId,
|
|
],
|
|
);
|
|
|
|
const loadGalleryDetailEntry = useCallback(
|
|
async (entry: CustomWorldGalleryCard) => {
|
|
const detailEntry = await getRpgEntryWorldGalleryDetail(
|
|
entry.ownerUserId,
|
|
entry.profileId,
|
|
);
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: detailEntry.ownerUserId,
|
|
profileId: detailEntry.profileId,
|
|
worldName: detailEntry.worldName,
|
|
subtitle: detailEntry.subtitle,
|
|
summaryText: detailEntry.summaryText,
|
|
coverImageSrc: detailEntry.coverImageSrc,
|
|
themeMode: detailEntry.themeMode,
|
|
authorDisplayName: detailEntry.authorDisplayName,
|
|
});
|
|
return detailEntry;
|
|
},
|
|
[appendBrowseHistoryEntry],
|
|
);
|
|
|
|
const openGalleryDetail = useCallback(
|
|
async (entry: CustomWorldGalleryCard) => {
|
|
setSelectionStage('detail');
|
|
setIsDetailLoading(true);
|
|
setDetailError(null);
|
|
|
|
try {
|
|
const detailEntry = await loadGalleryDetailEntry(entry);
|
|
setSelectedDetailEntry(detailEntry);
|
|
if (detailEntry.publicWorkCode?.trim()) {
|
|
pushAppHistoryPath(
|
|
buildPublicWorkDetailPath(detailEntry.publicWorkCode),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
setSelectedDetailEntry(null);
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
} finally {
|
|
setIsDetailLoading(false);
|
|
}
|
|
},
|
|
[loadGalleryDetailEntry, setSelectedDetailEntry, setSelectionStage],
|
|
);
|
|
|
|
const openSavedCustomWorldEditor = useCallback(
|
|
async (entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
|
try {
|
|
const detailEntry =
|
|
userId && entry.ownerUserId === userId
|
|
? await getRpgEntryWorldLibraryDetail(entry.profileId)
|
|
: entry;
|
|
setSelectedDetailEntry(detailEntry);
|
|
resetAutoSaveTrackingToIdle();
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
setCustomWorldError(null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setGeneratedCustomWorldProfile(detailEntry.profile);
|
|
markAutoSavedProfile(detailEntry.profile);
|
|
setCustomWorldAutoSaveState('saved');
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource('saved-profile');
|
|
setSelectionStage('custom-world-result');
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
}
|
|
},
|
|
[
|
|
markAutoSavedProfile,
|
|
resetAutoSaveTrackingToIdle,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldError,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setGeneratedCustomWorldProfile,
|
|
setSelectedDetailEntry,
|
|
setSelectionStage,
|
|
userId,
|
|
],
|
|
);
|
|
|
|
const handleOpenCreationWork = useCallback(
|
|
async (work: CustomWorldWorkSummary) => {
|
|
if (work.status === 'draft' && work.sessionId) {
|
|
setCustomWorldError(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
resetAutoSaveTrackingToIdle();
|
|
|
|
try {
|
|
const resultView = await syncAgentCreationResultView(work.sessionId);
|
|
const nextProfile = buildDraftResultProfile(resultView);
|
|
|
|
if (resultView?.targetStage === 'custom-world-generating') {
|
|
// 生成过程中失败的草稿要回到生成过程页承接错误处理,避免误回 Agent 对话。
|
|
suppressAgentDraftResultAutoOpen();
|
|
persistAgentUiState(
|
|
work.sessionId,
|
|
null,
|
|
resultView.generationViewSource ?? 'agent-draft-foundation',
|
|
);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldGenerationViewSource(
|
|
resultView.generationViewSource ?? 'agent-draft-foundation',
|
|
);
|
|
setCustomWorldResultViewSource(null);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('custom-world-generating');
|
|
return;
|
|
}
|
|
|
|
if (resultView?.targetStage === 'agent-workspace') {
|
|
// 还没有服务端草稿真相源时只能恢复 Agent,对象数量等摘要字段不能决定结果页入口。
|
|
suppressAgentDraftResultAutoOpen();
|
|
persistAgentUiState(work.sessionId, null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldResultViewSource(null);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('agent-workspace');
|
|
return;
|
|
}
|
|
|
|
releaseAgentDraftResultAutoOpenSuppression();
|
|
if (!nextProfile) {
|
|
persistAgentUiState(work.sessionId, null, 'agent-draft-foundation');
|
|
setPlatformError(
|
|
'当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。',
|
|
);
|
|
setPlatformTabToCreate();
|
|
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
|
setSelectionStage('custom-world-generating');
|
|
return;
|
|
}
|
|
|
|
persistAgentUiState(work.sessionId, null);
|
|
setGeneratedCustomWorldProfile(nextProfile);
|
|
setCustomWorldResultViewSource(
|
|
resultView?.resultViewSource ?? 'agent-draft',
|
|
);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage(resultView?.targetStage ?? 'custom-world-result');
|
|
return;
|
|
} catch (error) {
|
|
if (isMissingRpgEntryAgentSessionError(error)) {
|
|
// 失效会话不能继续保留在恢复状态里,否则刷新后会重复命中同一个坏 session。
|
|
persistAgentUiState(null, null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldResultViewSource(null);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setPlatformError(
|
|
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
|
|
);
|
|
} else {
|
|
setPlatformError(
|
|
resolveRpgEntryErrorMessage(error, '读取创作草稿失败。'),
|
|
);
|
|
}
|
|
|
|
setPlatformTabToDraft();
|
|
setSelectionStage('platform');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!work.profileId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let matchedEntry = savedCustomWorldEntries.find(
|
|
(entry) => entry.profileId === work.profileId,
|
|
);
|
|
|
|
if (!matchedEntry && userId) {
|
|
const latestLibraryEntries = await listRpgEntryWorldLibrary();
|
|
setSavedCustomWorldEntries(latestLibraryEntries);
|
|
matchedEntry = latestLibraryEntries.find(
|
|
(entry) => entry.profileId === work.profileId,
|
|
);
|
|
}
|
|
|
|
if (matchedEntry) {
|
|
void openLibraryDetail(matchedEntry);
|
|
return;
|
|
}
|
|
|
|
setPlatformError('未找到对应作品,请刷新后重试。');
|
|
} catch (error) {
|
|
setPlatformError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
}
|
|
},
|
|
[
|
|
buildDraftResultProfile,
|
|
openLibraryDetail,
|
|
persistAgentUiState,
|
|
releaseAgentDraftResultAutoOpenSuppression,
|
|
resetAutoSaveTrackingToIdle,
|
|
refreshCustomWorldWorks,
|
|
savedCustomWorldEntries,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldError,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setGeneratedCustomWorldProfile,
|
|
setPlatformError,
|
|
setPlatformTabToCreate,
|
|
setPlatformTabToDraft,
|
|
setSavedCustomWorldEntries,
|
|
setSelectionStage,
|
|
suppressAgentDraftResultAutoOpen,
|
|
syncAgentCreationResultView,
|
|
userId,
|
|
],
|
|
);
|
|
|
|
const handlePublishSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await publishRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '发布自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
]);
|
|
|
|
const handleUnpublishSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await unpublishRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '下架自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
]);
|
|
|
|
const handleDeleteSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const entries = await deleteRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(null);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('platform');
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '删除自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setPlatformTabToCreate,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
setSelectionStage,
|
|
]);
|
|
|
|
const isSelectedWorldOwned = Boolean(
|
|
selectedDetailEntry &&
|
|
savedCustomWorldEntries.some(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
),
|
|
);
|
|
|
|
return {
|
|
detailError,
|
|
setDetailError,
|
|
isDetailLoading,
|
|
isMutatingDetail,
|
|
isSelectedWorldOwned,
|
|
openLibraryDetail,
|
|
openGalleryDetail,
|
|
loadGalleryDetailEntry,
|
|
openSavedCustomWorldEditor,
|
|
handleOpenCreationWork,
|
|
handlePublishSelectedWorld,
|
|
handleUnpublishSelectedWorld,
|
|
handleDeleteSelectedWorld,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
|
|
*/
|
|
export const useRpgCreationDetailNavigation = useRpgEntryLibraryDetail;
|