This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -129,7 +129,7 @@ function syncDraftFromEditState(
const primaryLevel = levels[0] ?? buildFallbackLevelFromDraft(draft);
return {
...draft,
workTitle: editState.workTitle.trim() || draft.workTitle,
workTitle: editState.workTitle.trim(),
workDescription: editState.workDescription.trim(),
levelName: primaryLevel.levelName,
summary: editState.workDescription.trim(),
@@ -145,8 +145,8 @@ function syncDraftFromEditState(
function createDraftEditState(draft: PuzzleResultDraft): DraftEditState {
return {
workTitle: draft.workTitle || draft.levelName,
workDescription: draft.workDescription || '',
workTitle: draft.workTitle ?? '',
workDescription: draft.workDescription ?? '',
themeTags: normalizeThemeTagInput(draft.themeTags.join('')),
levels: normalizeDraftLevels(draft),
};
@@ -219,16 +219,7 @@ function buildPublishReady(
return {
blockers: [...new Set(blockers.filter(Boolean))],
publishReady:
Boolean(session.resultPreview?.publishReady) &&
Boolean(editState.workTitle.trim()) &&
Boolean(editState.workDescription.trim()) &&
editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT &&
editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT &&
levels.length > 0 &&
levels.every(
(level) => level.levelName.trim() && resolveLevelFormalImageSrc(level),
),
publishReady: blockers.filter(Boolean).length === 0,
};
}
@@ -308,11 +299,15 @@ function PuzzleResultTabs({
function PuzzleThemeTagEditor({
editState,
isBusy,
error,
onChange,
onGenerateTags,
}: {
editState: DraftEditState;
isBusy: boolean;
error: string | null;
onChange: (nextState: DraftEditState) => void;
onGenerateTags: () => void;
}) {
const [newTagText, setNewTagText] = useState('');
const [isAddingTag, setIsAddingTag] = useState(false);
@@ -339,18 +334,34 @@ function PuzzleThemeTagEditor({
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
{!isAddingTag ? (
<div className="flex items-center gap-2">
<button
type="button"
disabled={isBusy}
onClick={() => setIsAddingTag(true)}
onClick={onGenerateTags}
className="platform-icon-button h-9 w-9"
aria-label="新增作品标签"
title="新增作品标签"
aria-label="AI生成作品标签"
title="AI生成作品标签"
>
<Plus className="h-4 w-4" />
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
</button>
) : null}
{!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>
<div className="mt-3 flex flex-wrap gap-2">
@@ -430,6 +441,11 @@ function PuzzleThemeTagEditor({
</div>
</div>
) : null}
{error ? (
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
{error}
</div>
) : null}
</section>
);
}
@@ -1191,12 +1207,16 @@ function PuzzleLevelListTab({
function PuzzleWorkInfoTab({
editState,
tagGenerationError,
isBusy,
onChange,
onGenerateTags,
}: {
editState: DraftEditState;
tagGenerationError: string | null;
isBusy: boolean;
onChange: (nextState: DraftEditState) => void;
onGenerateTags: () => void;
}) {
return (
<div className="space-y-3">
@@ -1233,8 +1253,10 @@ function PuzzleWorkInfoTab({
<PuzzleThemeTagEditor
editState={editState}
error={tagGenerationError}
isBusy={isBusy}
onChange={onChange}
onGenerateTags={onGenerateTags}
/>
</div>
);
@@ -1304,6 +1326,9 @@ export function PuzzleResultView({
const [autoSaveState, setAutoSaveState] =
useState<PuzzleAutoSaveState>('idle');
const [autoSaveError, setAutoSaveError] = useState<string | null>(null);
const [tagGenerationError, setTagGenerationError] = useState<string | null>(
null,
);
const savedEditStateRef = useRef<DraftEditState | null>(
draft ? createDraftEditState(draft) : null,
);
@@ -1314,6 +1339,7 @@ export function PuzzleResultView({
setActiveLevelId(null);
setAutoSaveState('idle');
setAutoSaveError(null);
setTagGenerationError(null);
return;
}
const nextState = createDraftEditState(draft);
@@ -1327,6 +1353,7 @@ export function PuzzleResultView({
);
setAutoSaveState('idle');
setAutoSaveError(null);
setTagGenerationError(null);
}, [draft]);
const syncedDraft = useMemo(() => {
@@ -1445,7 +1472,7 @@ export function PuzzleResultView({
const buildLevelDraft = (level: PuzzleDraftLevel): PuzzleResultDraft => ({
...syncedDraft,
levelName: level.levelName,
summary: level.pictureDescription,
summary: editState.workDescription.trim(),
candidates: level.candidates,
selectedCandidateId: level.selectedCandidateId,
coverImageSrc: resolveLevelFormalImageSrc(level) || level.coverImageSrc,
@@ -1498,8 +1525,28 @@ export function PuzzleResultView({
) : (
<PuzzleWorkInfoTab
editState={editState}
tagGenerationError={tagGenerationError}
isBusy={isBusy}
onChange={setEditState}
onGenerateTags={() => {
const workTitle = editState.workTitle.trim();
const workDescription = editState.workDescription.trim();
if (!workTitle || !workDescription) {
setTagGenerationError('请先填写作品名称和作品描述。');
return;
}
setTagGenerationError(null);
const firstLevel = editState.levels[0] ?? null;
onExecuteAction({
action: 'generate_puzzle_tags',
workTitle,
workDescription,
levelName: firstLevel?.levelName.trim(),
summary: workDescription,
themeTags: editState.themeTags,
levelsJson: JSON.stringify(editState.levels),
});
}}
/>
)}
</div>