1
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
|
||||
function SmallButton({
|
||||
children,
|
||||
@@ -29,6 +32,85 @@ function SmallButton({
|
||||
);
|
||||
}
|
||||
|
||||
function PublishBlockersDialog({
|
||||
blockers,
|
||||
onClose,
|
||||
}: {
|
||||
blockers: string[];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[140] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="发布前检查"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(88vh,42rem)] w-full max-w-lg flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
发布前还需要补齐这些内容
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-6 text-[var(--platform-text-soft)]">
|
||||
当前还有 {blockers.length} 个阻断项,补齐后再发布并进入世界。
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
className="platform-icon-button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="space-y-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<div
|
||||
key={`publish-blocker-${index}-${blocker}`}
|
||||
className="platform-banner platform-banner--warning text-sm leading-6"
|
||||
>
|
||||
<div className="text-xs font-semibold tracking-[0.14em] text-[var(--platform-warm-text)] opacity-80">
|
||||
阻断项 {index + 1}
|
||||
</div>
|
||||
<div className="mt-1 text-[var(--platform-text-strong)]">
|
||||
{blocker}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-button platform-button--primary"
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
interface RpgCreationResultActionBarProps {
|
||||
editActionLabel: string;
|
||||
enterWorldActionLabel: string;
|
||||
@@ -40,6 +122,7 @@ interface RpgCreationResultActionBarProps {
|
||||
profile: CustomWorldProfile;
|
||||
regenerateActionLabel: string;
|
||||
publishReady: boolean;
|
||||
publishBlockers: string[];
|
||||
}
|
||||
|
||||
export function RpgCreationResultActionBar({
|
||||
@@ -53,7 +136,21 @@ export function RpgCreationResultActionBar({
|
||||
profile,
|
||||
regenerateActionLabel,
|
||||
publishReady,
|
||||
publishBlockers,
|
||||
}: RpgCreationResultActionBarProps) {
|
||||
const [showPublishBlockersDialog, setShowPublishBlockersDialog] =
|
||||
useState(false);
|
||||
|
||||
// 结果页只在用户点击发布动作时展示阻断项,不做吸底常驻提示。
|
||||
const handleEnterWorld = () => {
|
||||
if (!publishReady) {
|
||||
setShowPublishBlockersDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onEnterWorld?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
{profile.generationStatus === 'key_only' ? (
|
||||
@@ -82,14 +179,24 @@ export function RpgCreationResultActionBar({
|
||||
{onEnterWorld ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEnterWorld}
|
||||
disabled={isGenerating || !publishReady}
|
||||
onClick={handleEnterWorld}
|
||||
disabled={isGenerating}
|
||||
className={`platform-button platform-button--primary ${isGenerating ? 'opacity-55' : ''}`}
|
||||
>
|
||||
{enterWorldActionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{showPublishBlockersDialog ? (
|
||||
<PublishBlockersDialog
|
||||
blockers={
|
||||
publishBlockers.length > 0
|
||||
? publishBlockers
|
||||
: ['当前草稿还没有通过发布门槛,请先补齐必要内容。']
|
||||
}
|
||||
onClose={() => setShowPublishBlockersDialog(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import RpgCreationAssetDebugPanel, {
|
||||
import RpgCreationResultActionBar from './RpgCreationResultActionBar';
|
||||
import RpgCreationResultHeader from './RpgCreationResultHeader';
|
||||
import { useRpgCreationResultActions } from './useRpgCreationResultActions';
|
||||
import type { EntityGenerationKind } from './useRpgCreationResultActions';
|
||||
|
||||
export interface RpgCreationResultViewProps {
|
||||
profile: CustomWorldProfile;
|
||||
@@ -25,6 +26,12 @@ export interface RpgCreationResultViewProps {
|
||||
onRegenerate?: () => void;
|
||||
onContinueExpand?: () => void;
|
||||
onEnterWorld?: () => void;
|
||||
onDeleteEntities?: (kind: 'story' | 'landmark', ids: string[]) => Promise<void> | void;
|
||||
onGenerateEntity?:
|
||||
| ((kind: EntityGenerationKind) => Promise<{ profile?: CustomWorldProfile | null } | void> | { profile?: CustomWorldProfile | null } | void)
|
||||
| undefined;
|
||||
onGenerateRoleAssets?: (roleId: string) => Promise<void> | void;
|
||||
onGenerateSceneAssets?: (sceneId: string, sceneKind: 'camp' | 'landmark') => Promise<void> | void;
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
readOnly?: boolean;
|
||||
backLabel?: string;
|
||||
@@ -56,7 +63,11 @@ export function RpgCreationResultView({
|
||||
onEditSetting,
|
||||
onRegenerate: triggerRegenerate,
|
||||
onContinueExpand,
|
||||
onDeleteEntities,
|
||||
onEnterWorld,
|
||||
onGenerateEntity,
|
||||
onGenerateRoleAssets,
|
||||
onGenerateSceneAssets,
|
||||
onProfileChange,
|
||||
readOnly = false,
|
||||
backLabel = '返回',
|
||||
@@ -68,7 +79,6 @@ export function RpgCreationResultView({
|
||||
publishReady = true,
|
||||
publishBlockers = [],
|
||||
qualityFindings = [],
|
||||
previewSourceLabel = null,
|
||||
}: RpgCreationResultViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<ResultTab>('world');
|
||||
const assetDebugEnabled = useMemo(
|
||||
@@ -90,6 +100,11 @@ export function RpgCreationResultView({
|
||||
setEditorTarget,
|
||||
} = useRpgCreationResultActions({
|
||||
activeTab,
|
||||
agentEntityGenerator: onGenerateEntity
|
||||
? async (kind) => {
|
||||
return onGenerateEntity(kind);
|
||||
}
|
||||
: undefined,
|
||||
isGenerating,
|
||||
onProfileChange,
|
||||
profile,
|
||||
@@ -97,8 +112,19 @@ export function RpgCreationResultView({
|
||||
triggerRegenerate,
|
||||
});
|
||||
|
||||
const deleteStoryNpcs = onDeleteEntities
|
||||
? (ids: string[]) => {
|
||||
void onDeleteEntities('story', ids);
|
||||
}
|
||||
: handleDeleteStoryNpcs;
|
||||
const deleteLandmarks = onDeleteEntities
|
||||
? (ids: string[]) => {
|
||||
void onDeleteEntities('landmark', ids);
|
||||
}
|
||||
: handleDeleteLandmarks;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="platform-remap-surface flex h-full min-h-0 flex-col">
|
||||
<RpgCreationResultHeader
|
||||
autoSaveState={autoSaveState}
|
||||
backLabel={backLabel}
|
||||
@@ -114,13 +140,17 @@ export function RpgCreationResultView({
|
||||
onActiveTabChange={setActiveTab}
|
||||
onEditTarget={setEditorTarget}
|
||||
onProfileChange={onProfileChange}
|
||||
onDeleteStoryNpcs={handleDeleteStoryNpcs}
|
||||
onDeleteLandmarks={handleDeleteLandmarks}
|
||||
onDeleteStoryNpcs={deleteStoryNpcs}
|
||||
onDeleteLandmarks={deleteLandmarks}
|
||||
onGenerateRoleAssets={onGenerateRoleAssets ? (roleId) => { void onGenerateRoleAssets(roleId); } : undefined}
|
||||
onGenerateSceneAssets={onGenerateSceneAssets ? (sceneId, sceneKind) => { void onGenerateSceneAssets(sceneId, sceneKind); } : undefined}
|
||||
createActionLabel={
|
||||
readOnly || compactAgentResultMode ? undefined : createLabel
|
||||
readOnly || (compactAgentResultMode && !onGenerateEntity)
|
||||
? undefined
|
||||
: createLabel
|
||||
}
|
||||
onCreateAction={
|
||||
readOnly || compactAgentResultMode || !createTarget
|
||||
readOnly || (compactAgentResultMode && !onGenerateEntity) || !createTarget
|
||||
? undefined
|
||||
: () => {
|
||||
if (activeTab === 'playable') {
|
||||
@@ -171,25 +201,6 @@ export function RpgCreationResultView({
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && previewSourceLabel ? (
|
||||
<div className="platform-banner platform-banner--info mt-3 rounded-2xl text-sm leading-6">
|
||||
当前结果页数据源:{previewSourceLabel}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && publishBlockers.length > 0 ? (
|
||||
<div className="platform-banner platform-banner--warning mt-3 rounded-2xl text-sm leading-6">
|
||||
{publishReady
|
||||
? '当前世界已满足发布门槛。'
|
||||
: `当前还有 ${publishBlockers.length} 个发布阻断项,请先补齐后再进入世界。`}
|
||||
<div className="mt-2 space-y-1">
|
||||
{publishBlockers.slice(0, 4).map((entry, index) => (
|
||||
<div key={`publish-blocker-${index}-${entry}`}>
|
||||
{index + 1}. {entry}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!error &&
|
||||
compactAgentResultMode &&
|
||||
publishBlockers.length <= 0 &&
|
||||
@@ -216,6 +227,7 @@ export function RpgCreationResultView({
|
||||
profile={profile}
|
||||
regenerateActionLabel={regenerateActionLabel}
|
||||
publishReady={publishReady}
|
||||
publishBlockers={publishBlockers}
|
||||
/>
|
||||
|
||||
<RpgCreationEntityEditorModal
|
||||
|
||||
@@ -23,6 +23,10 @@ export type PendingGeneratedEntity = {
|
||||
|
||||
export type RecentGeneratedIds = Record<EntityGenerationKind, string[]>;
|
||||
|
||||
export type AgentEntityGenerationResult = {
|
||||
profile?: CustomWorldProfile | null;
|
||||
};
|
||||
|
||||
function getCreateTargetByTab(
|
||||
activeTab: ResultTab,
|
||||
): RpgCreationEditorTarget | null {
|
||||
@@ -99,6 +103,15 @@ function prependLandmark(
|
||||
} satisfies CustomWorldProfile;
|
||||
}
|
||||
|
||||
function getEntityCountByKind(
|
||||
profile: CustomWorldProfile,
|
||||
kind: EntityGenerationKind,
|
||||
) {
|
||||
if (kind === 'playable') return profile.playableNpcs.length;
|
||||
if (kind === 'story') return profile.storyNpcs.length;
|
||||
return profile.landmarks.length;
|
||||
}
|
||||
|
||||
function removeStoryNpcsFromProfile(
|
||||
profile: CustomWorldProfile,
|
||||
ids: string[],
|
||||
@@ -144,6 +157,9 @@ function removeLandmarksFromProfile(
|
||||
|
||||
export function useRpgCreationResultActions(params: {
|
||||
activeTab: ResultTab;
|
||||
agentEntityGenerator?:
|
||||
| ((kind: EntityGenerationKind) => Promise<AgentEntityGenerationResult | void>)
|
||||
| undefined;
|
||||
isGenerating: boolean;
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
profile: CustomWorldProfile;
|
||||
@@ -152,6 +168,7 @@ export function useRpgCreationResultActions(params: {
|
||||
}) {
|
||||
const {
|
||||
activeTab,
|
||||
agentEntityGenerator,
|
||||
isGenerating,
|
||||
onProfileChange,
|
||||
profile,
|
||||
@@ -242,7 +259,16 @@ export function useRpgCreationResultActions(params: {
|
||||
startPendingProgress(kind);
|
||||
|
||||
try {
|
||||
if (kind === 'playable') {
|
||||
if (agentEntityGenerator) {
|
||||
const previousCount = getEntityCountByKind(profile, kind);
|
||||
const generationResult = await agentEntityGenerator(kind);
|
||||
const currentCount = generationResult?.profile
|
||||
? getEntityCountByKind(generationResult.profile, kind)
|
||||
: previousCount;
|
||||
if (currentCount <= previousCount) {
|
||||
throw new Error('生成请求已完成,但结果页未收到新增内容,请返回创作页重新打开草稿后重试。');
|
||||
}
|
||||
} else if (kind === 'playable') {
|
||||
const nextNpc = await rpgCreationAssetClient.generatePlayableNpc({
|
||||
profile,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user