This commit is contained in:
2026-04-24 17:59:48 +08:00
parent 929febb4fe
commit 6cb3efae61
55 changed files with 2373 additions and 435 deletions

View File

@@ -2,8 +2,10 @@ import { X } from 'lucide-react';
import { type ReactNode, useState } from 'react';
import { createPortal } from 'react-dom';
import { resolveCustomWorldCoverPresentation } from '../../services/customWorldCover';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
function SmallButton({
children,
@@ -32,14 +34,25 @@ function SmallButton({
);
}
function PublishBlockersDialog({
function PublishPanelDialog({
blockers,
profile,
publishReady,
isPublishing,
onClose,
onEditCover,
onPublish,
}: {
blockers: string[];
profile: CustomWorldProfile;
publishReady: boolean;
isPublishing: boolean;
onClose: () => void;
onEditCover: () => void;
onPublish: () => void;
}) {
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
const coverPresentation = resolveCustomWorldCoverPresentation(profile);
if (typeof document === 'undefined') {
return null;
@@ -57,17 +70,17 @@ function PublishBlockersDialog({
<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]"
aria-label="发布作品"
className="platform-modal-shell platform-remap-surface flex max-h-[min(90vh,46rem)] w-full max-w-4xl 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
@@ -80,29 +93,72 @@ function PublishBlockersDialog({
</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 className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.78fr)]">
<div className="space-y-3">
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
))}
{blockers.length > 0 ? (
<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 className="platform-banner platform-banner--success text-sm leading-6">
</div>
)}
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
{coverPresentation.sourceType === 'uploaded'
? '上传封面'
: coverPresentation.sourceType === 'generated'
? 'AI封面'
: '默认封面'}
</span>
</div>
<div className="platform-subpanel rounded-[1.25rem] p-2">
<CustomWorldCoverArtwork
imageSrc={coverPresentation.imageSrc}
title={profile.name}
fallbackLabel={profile.name.slice(0, 4) || '封面'}
renderMode={coverPresentation.renderMode}
characterImageSrcs={coverPresentation.characterImageSrcs}
className="aspect-[16/9] max-h-[15rem] rounded-[1rem]"
/>
</div>
<SmallButton onClick={onEditCover} tone="sky">
</SmallButton>
</div>
</div>
</div>
<div className="flex justify-end border-t border-[var(--platform-subpanel-border)] px-5 py-4">
<div className="flex flex-col-reverse gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4 sm:flex-row sm:justify-end">
<SmallButton onClick={onClose}></SmallButton>
<button
type="button"
onClick={onClose}
className="platform-button platform-button--primary"
onClick={onPublish}
disabled={!publishReady || isPublishing}
className={`platform-button platform-button--primary ${!publishReady || isPublishing ? 'cursor-not-allowed opacity-55' : ''}`}
>
{isPublishing ? '发布中...' : '发布到广场'}
</button>
</div>
</div>
@@ -118,6 +174,9 @@ interface RpgCreationResultActionBarProps {
onContinueExpand?: () => void;
onEditSetting?: () => void;
onEnterWorld?: () => void;
onOpenCoverEditor?: () => void;
onPublishWorld?: () => Promise<void> | void;
onTestWorld?: () => void;
onRegenerate?: () => void;
profile: CustomWorldProfile;
regenerateActionLabel: string;
@@ -132,6 +191,9 @@ export function RpgCreationResultActionBar({
onContinueExpand,
onEditSetting,
onEnterWorld,
onOpenCoverEditor,
onPublishWorld,
onTestWorld,
onRegenerate,
profile,
regenerateActionLabel,
@@ -140,6 +202,7 @@ export function RpgCreationResultActionBar({
}: RpgCreationResultActionBarProps) {
const [showPublishBlockersDialog, setShowPublishBlockersDialog] =
useState(false);
const [isPublishing, setIsPublishing] = useState(false);
// 结果页只在用户点击发布动作时展示阻断项,不做吸底常驻提示。
const handleEnterWorld = () => {
@@ -151,6 +214,20 @@ export function RpgCreationResultActionBar({
onEnterWorld?.();
};
const handlePublish = async () => {
if (!publishReady || isPublishing || !onPublishWorld) {
return;
}
setIsPublishing(true);
try {
await onPublishWorld();
setShowPublishBlockersDialog(false);
} finally {
setIsPublishing(false);
}
};
return (
<div className="mt-4 flex flex-col gap-3">
{profile.generationStatus === 'key_only' ? (
@@ -176,7 +253,26 @@ export function RpgCreationResultActionBar({
</SmallButton>
) : null}
{onEnterWorld ? (
{onTestWorld ? (
<button
type="button"
onClick={onTestWorld}
disabled={isGenerating}
className={`platform-button platform-button--secondary ${isGenerating ? 'opacity-55' : ''}`}
>
</button>
) : null}
{onPublishWorld ? (
<button
type="button"
onClick={() => setShowPublishBlockersDialog(true)}
disabled={isGenerating}
className={`platform-button platform-button--primary ${isGenerating ? 'opacity-55' : ''}`}
>
</button>
) : onEnterWorld ? (
<button
type="button"
onClick={handleEnterWorld}
@@ -188,13 +284,18 @@ export function RpgCreationResultActionBar({
) : null}
</div>
{showPublishBlockersDialog ? (
<PublishBlockersDialog
blockers={
publishBlockers.length > 0
? publishBlockers
: ['当前草稿还没有通过发布门槛,请先补齐必要内容。']
}
<PublishPanelDialog
blockers={publishBlockers}
profile={profile}
publishReady={publishReady}
isPublishing={isPublishing}
onClose={() => setShowPublishBlockersDialog(false)}
onEditCover={() => {
onOpenCoverEditor?.();
}}
onPublish={() => {
void handlePublish();
}}
/>
) : null}
</div>