1
This commit is contained in:
@@ -33,7 +33,6 @@ type PuzzleResultViewProps = {
|
||||
onStartTestRun?: (draft: PuzzleResultDraft) => void;
|
||||
};
|
||||
|
||||
type PuzzleResultTab = 'basic' | 'images';
|
||||
type PuzzleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
type DraftEditState = {
|
||||
@@ -115,7 +114,9 @@ function buildPublishReady(
|
||||
...(editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT &&
|
||||
editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT
|
||||
? []
|
||||
: [`正式标签数量必须在 ${PUZZLE_MIN_THEME_TAG_COUNT} 到 ${PUZZLE_MAX_THEME_TAG_COUNT} 之间。`]),
|
||||
: [
|
||||
`正式标签数量必须在 ${PUZZLE_MIN_THEME_TAG_COUNT} 到 ${PUZZLE_MAX_THEME_TAG_COUNT} 之间。`,
|
||||
]),
|
||||
...(formalImageSrc ? [] : ['请先选择一张正式拼图图片。']),
|
||||
];
|
||||
|
||||
@@ -143,9 +144,7 @@ function resolvePuzzleFormalImageSrc(draft: PuzzleResultDraft) {
|
||||
null;
|
||||
|
||||
return (
|
||||
selectedCandidate?.imageSrc?.trim() ||
|
||||
draft.coverImageSrc?.trim() ||
|
||||
''
|
||||
selectedCandidate?.imageSrc?.trim() || draft.coverImageSrc?.trim() || ''
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,41 +190,7 @@ function PuzzleResultHeader({
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleResultTabs({
|
||||
activeTab,
|
||||
onActiveTabChange,
|
||||
}: {
|
||||
activeTab: PuzzleResultTab;
|
||||
onActiveTabChange: (tab: PuzzleResultTab) => void;
|
||||
}) {
|
||||
const tabs: Array<{ key: PuzzleResultTab; label: string }> = [
|
||||
{ key: 'basic', label: '基本信息' },
|
||||
{ key: 'images', label: '拼图图片' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-3 overflow-x-auto">
|
||||
<div className="inline-flex min-w-full gap-2 rounded-full border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] p-1 sm:min-w-0">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => onActiveTabChange(tab.key)}
|
||||
className={`min-h-10 flex-1 rounded-full px-4 text-sm font-bold transition sm:flex-none ${
|
||||
activeTab === tab.key
|
||||
? 'bg-[var(--platform-accent-strong)] text-white shadow-[0_12px_30px_rgba(255,79,139,0.18)]'
|
||||
: 'text-[var(--platform-text-base)] hover:bg-white/62'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleBasicInfoTab({
|
||||
function PuzzleThemeTagEditor({
|
||||
editState,
|
||||
isBusy,
|
||||
onChange,
|
||||
@@ -254,126 +219,103 @@ function PuzzleBasicInfoTab({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 overflow-y-auto pr-1">
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
关卡名
|
||||
</div>
|
||||
<input
|
||||
value={editState.levelName}
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
题材标签
|
||||
</div>
|
||||
{!isAddingTag ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsAddingTag(true)}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="新增题材标签"
|
||||
title="新增题材标签"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{editState.themeTags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-amber-300/35 bg-amber-100/68 px-3 py-1.5 text-xs font-semibold text-amber-700"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...editState,
|
||||
levelName: event.target.value,
|
||||
themeTags: editState.themeTags.filter(
|
||||
(currentTag) => currentTag !== tag,
|
||||
),
|
||||
});
|
||||
}}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
className="rounded-full text-amber-800/70 transition hover:text-amber-950 disabled:opacity-45"
|
||||
aria-label={`删除标签 ${tag}`}
|
||||
title="删除标签"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{editState.themeTags.length <= 0 ? (
|
||||
<span className="text-sm text-[var(--platform-text-soft)]">
|
||||
暂无标签
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
题材标签
|
||||
</div>
|
||||
{!isAddingTag ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsAddingTag(true)}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="新增题材标签"
|
||||
title="新增题材标签"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{editState.themeTags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-amber-300/35 bg-amber-100/68 px-3 py-1.5 text-xs font-semibold text-amber-700"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...editState,
|
||||
themeTags: editState.themeTags.filter(
|
||||
(currentTag) => currentTag !== tag,
|
||||
),
|
||||
});
|
||||
}}
|
||||
className="rounded-full text-amber-800/70 transition hover:text-amber-950 disabled:opacity-45"
|
||||
aria-label={`删除标签 ${tag}`}
|
||||
title="删除标签"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{editState.themeTags.length <= 0 ? (
|
||||
<span className="text-sm text-[var(--platform-text-soft)]">
|
||||
暂无标签
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isAddingTag ? (
|
||||
<div className="mt-3 flex flex-col gap-2 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-2 sm:flex-row">
|
||||
<input
|
||||
autoFocus
|
||||
value={newTagText}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setNewTagText(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
addTags();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
setIsAddingTag(false);
|
||||
setNewTagText('');
|
||||
}
|
||||
}}
|
||||
className="min-h-10 flex-1 rounded-[0.8rem] border border-transparent bg-white/92 px-3 text-sm text-[var(--platform-text-strong)] outline-none"
|
||||
placeholder="输入新标签"
|
||||
aria-label="新题材标签"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={addTags}
|
||||
className="platform-button platform-button--primary min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setIsAddingTag(false);
|
||||
setNewTagText('');
|
||||
}}
|
||||
className="platform-button platform-button--ghost min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isAddingTag ? (
|
||||
<div className="mt-3 flex flex-col gap-2 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-2 sm:flex-row">
|
||||
<input
|
||||
autoFocus
|
||||
value={newTagText}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setNewTagText(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
addTags();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
setIsAddingTag(false);
|
||||
setNewTagText('');
|
||||
}
|
||||
}}
|
||||
className="min-h-10 flex-1 rounded-[0.8rem] border border-transparent bg-white/92 px-3 text-sm text-[var(--platform-text-strong)] outline-none"
|
||||
placeholder="输入新标签"
|
||||
aria-label="新题材标签"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={addTags}
|
||||
className="platform-button platform-button--primary min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setIsAddingTag(false);
|
||||
setNewTagText('');
|
||||
}}
|
||||
className="platform-button platform-button--ghost min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -476,7 +418,7 @@ function PuzzleHistoryAssetPickerDialog({
|
||||
) : null}
|
||||
|
||||
{!isLoading && assets.length > 0 ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{assets.map((asset) => (
|
||||
<button
|
||||
key={asset.assetObjectId}
|
||||
@@ -485,7 +427,7 @@ function PuzzleHistoryAssetPickerDialog({
|
||||
onClick={() => onSelect(asset)}
|
||||
className={`overflow-hidden rounded-[1.35rem] border bg-white/82 text-left transition hover:border-amber-300/70 hover:bg-white ${isBusy ? 'cursor-not-allowed opacity-55' : 'border-[var(--platform-subpanel-border)]'}`}
|
||||
>
|
||||
<div className="aspect-square overflow-hidden bg-[var(--platform-subpanel-fill)]">
|
||||
<div className="aspect-[9/16] overflow-hidden bg-[var(--platform-subpanel-fill)]">
|
||||
<ResolvedAssetImage
|
||||
src={asset.imageSrc}
|
||||
alt={asset.ownerLabel || '历史拼图素材'}
|
||||
@@ -511,12 +453,13 @@ function PuzzleHistoryAssetPickerDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function PuzzleImagesTab({
|
||||
function PuzzlePictureEditor({
|
||||
draft,
|
||||
editState,
|
||||
formalImageSrc,
|
||||
imageRefreshKey,
|
||||
isBusy,
|
||||
onSummaryChange,
|
||||
onGenerate,
|
||||
}: {
|
||||
draft: PuzzleResultDraft;
|
||||
@@ -524,18 +467,19 @@ function PuzzleImagesTab({
|
||||
formalImageSrc: string;
|
||||
imageRefreshKey: string;
|
||||
isBusy: boolean;
|
||||
onGenerate: (promptText?: string | null, referenceImageSrc?: string | null) => void;
|
||||
onSummaryChange: (summary: string) => void;
|
||||
onGenerate: (
|
||||
promptText?: string | null,
|
||||
referenceImageSrc?: string | null,
|
||||
) => void;
|
||||
}) {
|
||||
const [promptText, setPromptText] = useState(draft.summary);
|
||||
const [referenceImageSrc, setReferenceImageSrc] = useState('');
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(null);
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceImageLabel, setReferenceImageLabel] = useState('');
|
||||
const [isHistoryPickerOpen, setIsHistoryPickerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPromptText(draft.summary);
|
||||
}, [draft.summary]);
|
||||
|
||||
const handleReferenceImageChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
@@ -560,133 +504,123 @@ function PuzzleImagesTab({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 overflow-y-auto pr-1">
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
当前正式图
|
||||
<>
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面预览
|
||||
</div>
|
||||
<div className="mx-auto mt-3 aspect-[9/16] w-full max-w-[24rem] overflow-hidden rounded-[1.25rem] bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.14),transparent_34%),linear-gradient(145deg,rgba(76,29,19,0.86),rgba(30,41,59,0.94))]">
|
||||
{formalImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={formalImageSrc}
|
||||
refreshKey={imageRefreshKey}
|
||||
alt={editState.levelName || draft.levelName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-white/66">
|
||||
暂无正式图
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面描述
|
||||
</div>
|
||||
<div className="relative mt-3">
|
||||
<textarea
|
||||
value={editState.summary}
|
||||
disabled={isBusy}
|
||||
rows={10}
|
||||
onChange={(event) => onSummaryChange(event.target.value)}
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
||||
<label
|
||||
className={`inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
title={referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
>
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
void handleReferenceImageChange(event);
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsHistoryPickerOpen(true)}
|
||||
className={`inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/96 text-[var(--platform-text-strong)] shadow-sm transition hover:bg-[var(--platform-subpanel-fill)] ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
aria-label="从历史拼图素材库选择"
|
||||
title="从历史拼图素材库选择"
|
||||
>
|
||||
<Images className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 aspect-square overflow-hidden rounded-[1.25rem] bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.14),transparent_34%),linear-gradient(145deg,rgba(76,29,19,0.86),rgba(30,41,59,0.94))]">
|
||||
{formalImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={formalImageSrc}
|
||||
refreshKey={imageRefreshKey}
|
||||
alt={editState.levelName || draft.levelName}
|
||||
</div>
|
||||
|
||||
{referenceImageSrc ? (
|
||||
<div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
|
||||
<div className="h-14 w-14 overflow-hidden rounded-[0.9rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<img
|
||||
src={referenceImageSrc}
|
||||
alt="拼图参考图"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-white/66">
|
||||
暂无正式图
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面描述
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={promptText}
|
||||
disabled={isBusy}
|
||||
rows={10}
|
||||
onChange={(event) => setPromptText(event.target.value)}
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
||||
<label className={`inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`} title={referenceImageSrc ? '更换参考图' : '添加参考图'}>
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
void handleReferenceImageChange(event);
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsHistoryPickerOpen(true)}
|
||||
className={`inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/96 text-[var(--platform-text-strong)] shadow-sm transition hover:bg-[var(--platform-subpanel-fill)] ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
aria-label="从历史拼图素材库选择"
|
||||
title="从历史拼图素材库选择"
|
||||
>
|
||||
<Images className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{referenceImageLabel || '已选择参考图'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{referenceImageSrc ? (
|
||||
<div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
|
||||
<div className="h-14 w-14 overflow-hidden rounded-[0.9rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<img
|
||||
src={referenceImageSrc}
|
||||
alt="拼图参考图"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{referenceImageLabel || '已选择参考图'}
|
||||
</div>
|
||||
<div className="text-xs leading-5 text-[var(--platform-text-base)]">
|
||||
下次生成将带入这张参考图
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
className="platform-button platform-button--ghost min-h-9 px-3 py-1.5 text-xs"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{referenceImageError ? (
|
||||
<div className="mt-2 text-xs leading-5 text-red-600">
|
||||
{referenceImageError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.candidates[0]?.actualPrompt || draft.candidates[0]?.prompt ? (
|
||||
<div className="mt-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-4 py-3 text-xs leading-6 text-[var(--platform-text-base)]">
|
||||
{draft.candidates[0]?.actualPrompt || draft.candidates[0]?.prompt}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onGenerate(
|
||||
promptText.trim() || undefined,
|
||||
referenceImageSrc || undefined,
|
||||
);
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
|
||||
className="platform-button platform-button--ghost min-h-9 px-3 py-1.5 text-xs"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
<Sparkles className="h-4 w-4" />
|
||||
生成并替换当前图片
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{referenceImageError ? (
|
||||
<div className="mt-2 text-xs leading-5 text-red-600">
|
||||
{referenceImageError}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onGenerate(
|
||||
editState.summary.trim() || undefined,
|
||||
referenceImageSrc || undefined,
|
||||
);
|
||||
}}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
<Sparkles className="h-4 w-4" />
|
||||
重新生成画面
|
||||
</button>
|
||||
|
||||
{isHistoryPickerOpen ? (
|
||||
<PuzzleHistoryAssetPickerDialog
|
||||
@@ -694,13 +628,15 @@ function PuzzleImagesTab({
|
||||
onClose={() => setIsHistoryPickerOpen(false)}
|
||||
onSelect={(asset) => {
|
||||
setReferenceImageSrc(asset.imageSrc);
|
||||
setReferenceImageLabel(`历史素材 · ${asset.ownerLabel || '未记录账号'}`);
|
||||
setReferenceImageLabel(
|
||||
`历史素材 · ${asset.ownerLabel || '未记录账号'}`,
|
||||
);
|
||||
setReferenceImageError(null);
|
||||
setIsHistoryPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -789,7 +725,7 @@ function PuzzlePublishDialog({
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
正式图
|
||||
</div>
|
||||
<div className="aspect-square overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<div className="aspect-[9/16] overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
|
||||
{formalImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={formalImageSrc}
|
||||
@@ -904,7 +840,7 @@ function PuzzleResultActionBar({
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼图结果页收口为两个 Tab:基本信息负责标题和标签,拼图图片负责单图编辑工作台。
|
||||
* 拼图结果页收口为单列表:关卡名称、画面预览、画面描述、重新生成画面、题材标签。
|
||||
* 发布校验只在点击发布后出现,避免结果页重新变成信息总表。
|
||||
*/
|
||||
export function PuzzleResultView({
|
||||
@@ -919,11 +855,11 @@ export function PuzzleResultView({
|
||||
const draft = session.draft;
|
||||
const formalImageSrc = draft ? resolvePuzzleFormalImageSrc(draft) : '';
|
||||
const imageRefreshKey = `${session.updatedAt}:${formalImageSrc}`;
|
||||
const [activeTab, setActiveTab] = useState<PuzzleResultTab>('basic');
|
||||
const [editState, setEditState] = useState<DraftEditState | null>(
|
||||
draft ? createDraftEditState(draft) : null,
|
||||
);
|
||||
const [autoSaveState, setAutoSaveState] = useState<PuzzleAutoSaveState>('idle');
|
||||
const [autoSaveState, setAutoSaveState] =
|
||||
useState<PuzzleAutoSaveState>('idle');
|
||||
const [autoSaveError, setAutoSaveError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -945,7 +881,9 @@ export function PuzzleResultView({
|
||||
|
||||
const normalizedLevelName = editState.levelName.trim();
|
||||
const normalizedSummary = editState.summary.trim();
|
||||
const normalizedTags = normalizeThemeTagInput(editState.themeTags.join(','));
|
||||
const normalizedTags = normalizeThemeTagInput(
|
||||
editState.themeTags.join(','),
|
||||
);
|
||||
const draftLevelName = draft.levelName.trim();
|
||||
const draftSummary = draft.summary.trim();
|
||||
const draftTags = normalizeThemeTagInput(draft.themeTags.join(','));
|
||||
@@ -1022,25 +960,38 @@ export function PuzzleResultView({
|
||||
onBack={onBack}
|
||||
/>
|
||||
|
||||
<PuzzleResultTabs
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
/>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div className="space-y-3">
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
关卡名称
|
||||
</div>
|
||||
<input
|
||||
value={editState.levelName}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
setEditState({
|
||||
...editState,
|
||||
levelName: event.target.value,
|
||||
});
|
||||
}}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="关卡名称"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{activeTab === 'basic' ? (
|
||||
<PuzzleBasicInfoTab
|
||||
editState={editState}
|
||||
isBusy={isBusy}
|
||||
onChange={setEditState}
|
||||
/>
|
||||
) : (
|
||||
<PuzzleImagesTab
|
||||
<PuzzlePictureEditor
|
||||
draft={draft}
|
||||
editState={editState}
|
||||
formalImageSrc={formalImageSrc}
|
||||
imageRefreshKey={imageRefreshKey}
|
||||
isBusy={isBusy}
|
||||
onSummaryChange={(summary) => {
|
||||
setEditState({
|
||||
...editState,
|
||||
summary,
|
||||
});
|
||||
}}
|
||||
onGenerate={(promptText, referenceImageSrc) => {
|
||||
onExecuteAction({
|
||||
action: 'generate_puzzle_images',
|
||||
@@ -1050,7 +1001,13 @@ export function PuzzleResultView({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PuzzleThemeTagEditor
|
||||
editState={editState}
|
||||
isBusy={isBusy}
|
||||
onChange={setEditState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
|
||||
Reference in New Issue
Block a user