创作数据流程收束

This commit is contained in:
2026-04-21 09:44:17 +08:00
parent effe0355bd
commit 3614e1f5a2
93 changed files with 1794 additions and 8651 deletions

View File

@@ -21,8 +21,10 @@ type CustomWorldCreationHubProps = {
function EmptyState({ title }: { title: string }) {
return (
<div className="platform-remap-surface flex min-h-[16rem] flex-col items-center justify-center rounded-[1.8rem] border border-white/8 bg-white/5 px-6 py-8 text-center">
<div className="text-lg font-semibold text-white">{title}</div>
<div className="platform-subpanel flex min-h-[14rem] flex-col items-center justify-center rounded-[1.6rem] px-6 py-8 text-center">
<div className="text-lg font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</div>
);
}
@@ -52,35 +54,26 @@ export function CustomWorldCreationHub({
);
return (
<div
className="flex h-full min-h-0 flex-col overflow-y-auto overscroll-y-contain pr-1 pb-[max(1rem,env(safe-area-inset-bottom))]"
style={{ WebkitOverflowScrolling: 'touch' }}
>
<div
className="platform-remap-surface sticky top-0 z-20 -mx-3 px-3 pb-4 pt-1 sm:static sm:mx-0 sm:bg-none sm:px-0 sm:pb-5 sm:pt-0"
style={{
background:
'linear-gradient(180deg, var(--platform-modal-fill), transparent)',
}}
>
<div className="platform-page-stage platform-remap-surface space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4">
<div className="pb-1">
<div className="flex items-start justify-between gap-3">
<div>
<button
type="button"
onClick={onBack}
className="rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white"
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-[11px]"
>
</button>
<div className="mt-4 text-[1.8rem] font-black leading-tight text-white sm:text-[2.3rem]">
<div className="mt-4 text-[1.8rem] font-black leading-tight text-[var(--platform-text-strong)] sm:text-[2.3rem]">
</div>
</div>
<div className="hidden shrink-0 gap-2 sm:flex">
<span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-100">
<span className="platform-pill platform-pill--warm px-3 py-1 text-[11px]">
稿 {draftCount}
</span>
<span className="rounded-full border border-emerald-300/20 bg-emerald-500/10 px-3 py-1 text-[11px] text-emerald-100">
<span className="platform-pill platform-pill--success px-3 py-1 text-[11px]">
{publishedCount}
</span>
</div>
@@ -98,12 +91,12 @@ export function CustomWorldCreationHub({
/>
{error ? (
<div className="rounded-3xl border border-rose-400/18 bg-rose-500/10 px-4 py-4 text-sm leading-7 text-rose-100">
<div className="platform-banner platform-banner--danger rounded-[1.4rem] px-4 py-4 text-sm leading-7">
<div>{error}</div>
<button
type="button"
onClick={onRetry}
className="mt-3 rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-zinc-200 transition hover:text-white"
className="platform-button platform-button--ghost mt-3 min-h-0 rounded-full px-4 py-2 text-sm"
>
</button>
@@ -115,15 +108,15 @@ export function CustomWorldCreationHub({
{Array.from({ length: 3 }).map((_, index) => (
<div
key={`skeleton-${index}`}
className="min-h-[12rem] rounded-[1.8rem] border border-white/8 bg-white/5 p-5"
className="platform-subpanel min-h-[12rem] rounded-[1.6rem] p-5"
>
<div className="h-4 w-20 rounded-full bg-white/10" />
<div className="mt-6 h-8 w-36 rounded-full bg-white/10" />
<div className="mt-4 h-4 w-full rounded-full bg-white/10" />
<div className="mt-2 h-4 w-4/5 rounded-full bg-white/10" />
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-6 h-8 w-36 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-4 h-4 w-full rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-8 flex gap-2">
<div className="h-7 w-20 rounded-full bg-white/10" />
<div className="h-7 w-20 rounded-full bg-white/10" />
<div className="h-7 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="h-7 w-20 rounded-full bg-[var(--platform-track-fill)]" />
</div>
</div>
))}

View File

@@ -1,146 +0,0 @@
import { X } from 'lucide-react';
import type { CustomWorldQuestion } from '../../../packages/shared/src/contracts/runtime';
type CustomWorldCreationLauncherModalProps = {
isOpen: boolean;
mode: 'create' | 'resume';
seedText: string;
seedTextLocked: boolean;
questions: CustomWorldQuestion[];
answers: Record<string, string>;
isBusy: boolean;
error: string | null;
lastError?: string | null;
primaryLabel: string;
onClose: () => void;
onSeedTextChange: (value: string) => void;
onAnswerChange: (questionId: string, value: string) => void;
onPrimaryAction: () => void;
};
export function CustomWorldCreationLauncherModal({
isOpen,
mode,
seedText,
seedTextLocked,
questions,
answers,
isBusy,
error,
lastError = null,
primaryLabel,
onClose,
onSeedTextChange,
onAnswerChange,
onPrimaryAction,
}: CustomWorldCreationLauncherModalProps) {
if (!isOpen) {
return null;
}
const unansweredQuestions = questions.filter((question) => !question.answer?.trim());
return (
<div className="platform-overlay fixed inset-0 z-[90] flex items-center justify-center p-4 backdrop-blur-sm">
<div className="platform-modal-shell platform-remap-surface flex max-h-[92vh] w-full max-w-2xl flex-col overflow-hidden rounded-[2rem] shadow-[0_30px_90px_rgba(0,0,0,0.6)]">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div>
<div className="text-base font-semibold text-white">
{mode === 'create' ? '新建作品' : '继续创作'}
</div>
<div className="mt-1 text-xs text-zinc-400">
</div>
</div>
<button
type="button"
onClick={onClose}
disabled={isBusy}
className="rounded-full border border-white/10 bg-white/5 p-2 text-zinc-300 transition hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
<div className="space-y-4">
<label className="block">
<div className="mb-2 text-sm font-medium text-zinc-200">
</div>
<textarea
value={seedText}
onChange={(event) => onSeedTextChange(event.target.value)}
rows={seedTextLocked ? 4 : 6}
readOnly={seedTextLocked}
placeholder="例:一个被潮雾切碎的列岛世界,灯塔守望者、沉船秘术与旧盟约残片正在重新苏醒。"
className={`w-full rounded-3xl border border-white/10 bg-black/30 px-4 py-3 text-sm leading-7 text-white outline-none transition focus:border-sky-400/40 ${
seedTextLocked ? 'cursor-not-allowed opacity-75' : ''
}`}
/>
</label>
{unansweredQuestions.length > 0 ? (
<div className="space-y-3">
<div className="rounded-3xl border border-white/10 bg-black/20 px-4 py-3 text-sm leading-7 text-zinc-300">
</div>
{unansweredQuestions.map((question) => (
<label key={question.id} className="block">
<div className="mb-2 text-sm font-medium text-zinc-200">
{question.label}
</div>
<div className="mb-2 text-xs leading-6 text-zinc-400">
{question.question}
</div>
<textarea
value={answers[question.id] ?? question.answer ?? ''}
onChange={(event) =>
onAnswerChange(question.id, event.target.value)
}
rows={3}
placeholder="补充一句就可以。"
className="w-full rounded-3xl border border-white/10 bg-black/30 px-4 py-3 text-sm leading-7 text-white outline-none transition focus:border-sky-400/40"
/>
</label>
))}
</div>
) : null}
{lastError ? (
<div className="rounded-3xl border border-amber-400/25 bg-amber-500/10 px-4 py-3 text-sm leading-6 text-amber-100">
{lastError}
</div>
) : null}
{error ? (
<div className="rounded-3xl border border-rose-400/25 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{error}
</div>
) : null}
</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
<button
type="button"
onClick={onClose}
disabled={isBusy}
className="rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-zinc-300 transition hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
>
</button>
<button
type="button"
onClick={onPrimaryAction}
disabled={isBusy}
className="rounded-2xl bg-sky-400 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:bg-sky-300 disabled:cursor-not-allowed disabled:opacity-45"
>
{isBusy ? '处理中...' : primaryLabel}
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,3 @@
import { getNineSliceStyle, UI_CHROME } from '../../uiAssets';
type CustomWorldCreationStartCardProps = {
onCreateNew: () => void;
};
@@ -8,35 +6,22 @@ export function CustomWorldCreationStartCard({
onCreateNew,
}: CustomWorldCreationStartCardProps) {
return (
<div
className="pixel-nine-slice pixel-panel overflow-hidden"
style={getNineSliceStyle(UI_CHROME.storyPanel, {
paddingX: 18,
paddingY: 16,
})}
>
<div className="relative overflow-hidden rounded-[1.75rem] border border-white/8 bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.18),transparent_36%),linear-gradient(180deg,rgba(8,10,14,0.28),rgba(8,10,14,0.82))] px-5 py-5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="text-2xl font-black text-white sm:text-3xl">
</div>
<div className="platform-surface platform-surface--hero relative overflow-hidden px-5 py-5">
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<div className="relative z-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="text-2xl font-black text-white sm:text-3xl">
</div>
<button
type="button"
onClick={onCreateNew}
className="pixel-nine-slice pixel-pressable w-full text-left sm:w-auto"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 18,
paddingY: 11,
})}
>
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-semibold text-white"></span>
<span className="text-white/60"></span>
</div>
</button>
</div>
<button
type="button"
onClick={onCreateNew}
className="platform-button platform-button--primary w-full justify-between rounded-[1.1rem] text-left sm:w-auto"
>
<span className="text-sm font-semibold"></span>
<span aria-hidden="true"></span>
</button>
</div>
</div>
);

View File

@@ -1,6 +1,5 @@
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
import { getNineSliceStyle, UI_CHROME } from '../../uiAssets';
function formatUpdatedAt(value: string) {
const date = new Date(value);
@@ -31,76 +30,71 @@ export function CustomWorldWorkCard({
const roleCountLabel = isDraft ? '角色' : '可扮演角色';
return (
<div
className="pixel-nine-slice pixel-panel relative overflow-hidden"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 15,
})}
>
<div className="platform-surface platform-interactive-card relative min-h-[13.5rem] overflow-hidden px-4 py-4 sm:min-h-[14rem]">
<CustomWorldCoverArtwork
imageSrc={item.coverImageSrc}
title={item.title}
fallbackLabel={item.title.slice(0, 4) || '封面'}
fallbackLabel="封面"
renderMode={item.coverRenderMode}
characterImageSrcs={item.coverCharacterImageSrcs}
className="absolute inset-0"
className="platform-cover-artwork absolute inset-0"
/>
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="relative z-10 flex h-full min-h-[12rem] flex-col">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-wrap gap-2">
<span
className={`rounded-full border px-3 py-1 text-[10px] tracking-[0.18em] ${
className={`platform-pill px-3 py-1 text-[10px] ${
isDraft
? 'border-amber-300/20 bg-amber-500/10 text-amber-100'
: 'border-emerald-300/20 bg-emerald-500/10 text-emerald-100'
? 'platform-pill--warm'
: 'platform-pill--success'
}`}
>
{isDraft ? '草稿' : '已发布'}
</span>
{item.stageLabel ? (
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] text-zinc-200">
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.stageLabel}
</span>
) : null}
</div>
<div className="text-[11px] text-zinc-400">
<div className="shrink-0 text-[11px] text-[var(--platform-text-soft)]">
{formatUpdatedAt(item.updatedAt)}
</div>
</div>
<div className="mt-4">
<div className="text-2xl font-black text-white">
<div className="text-2xl font-black text-[var(--platform-text-strong)]">
{item.title}
</div>
<div className="mt-1 text-xs tracking-[0.12em] text-zinc-400">
<div className="mt-1 text-xs tracking-[0.12em] text-[var(--platform-text-soft)]">
{item.subtitle}
</div>
<div className="mt-3 line-clamp-3 text-sm leading-7 text-zinc-200/90">
<div className="mt-3 line-clamp-3 text-sm leading-7 text-[color:color-mix(in_srgb,var(--platform-text-base)_92%,transparent)]">
{item.summary}
</div>
</div>
<div className="mt-auto flex items-center justify-between gap-3 pt-4">
<div className="mt-auto flex flex-col gap-3 pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] text-zinc-100">
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{roleCountLabel} {item.playableNpcCount}
</span>
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] text-zinc-100">
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.landmarkCount}
</span>
{item.roleVisualReadyCount ? (
<span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-3 py-1 text-[10px] text-amber-100">
<span className="platform-pill platform-pill--warm px-3 py-1 text-[10px]">
{item.roleVisualReadyCount}
</span>
) : null}
{item.roleAnimationReadyCount ? (
<span className="rounded-full border border-emerald-300/20 bg-emerald-500/10 px-3 py-1 text-[10px] text-emerald-100">
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
{item.roleAnimationReadyCount}
</span>
) : null}
{item.roleAssetSummaryLabel ? (
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] text-zinc-200">
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.roleAssetSummaryLabel}
</span>
) : null}
@@ -108,7 +102,7 @@ export function CustomWorldWorkCard({
<button
type="button"
onClick={onClick}
className="rounded-full border border-sky-300/20 bg-sky-500/10 px-4 py-2 text-sm font-medium text-sky-100 transition hover:text-white"
className="platform-button platform-button--primary min-h-0 shrink-0 rounded-full px-4 py-2 text-sm"
>
{isDraft ? (hasFoundationDraft ? '继续精修' : '继续创作') : '进入世界'}
</button>

View File

@@ -23,7 +23,7 @@ export function CustomWorldWorkTabs({
onChange,
}: CustomWorldWorkTabsProps) {
return (
<div className="platform-remap-surface flex items-center gap-2 overflow-x-auto pb-1">
<div className="platform-remap-surface flex items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
{FILTER_OPTIONS.map((option) => {
const count =
option.id === 'draft'
@@ -37,10 +37,8 @@ export function CustomWorldWorkTabs({
key={option.id}
type="button"
onClick={() => onChange(option.id)}
className={`shrink-0 rounded-full border px-4 py-2 text-sm transition ${
activeFilter === option.id
? 'border-sky-300/20 bg-sky-500/10 text-sky-100'
: 'border-white/10 bg-black/18 text-zinc-300 hover:text-white'
className={`platform-tab shrink-0 px-4 py-2 text-sm ${
activeFilter === option.id ? 'platform-tab--active' : ''
}`}
>
{option.label} {count}