feat: 完善敲木鱼结果页元信息补录

This commit is contained in:
2026-05-24 20:34:36 +08:00
parent 8638397faa
commit 838c74d8fe
14 changed files with 757 additions and 215 deletions

View File

@@ -3,7 +3,9 @@ import {
Loader2,
Mic,
Pause,
Plus,
Send,
X,
Upload,
} from 'lucide-react';
import { useMemo, useRef, useState } from 'react';
@@ -32,44 +34,24 @@ type WoodenFishWorkspaceProps = {
};
type WoodenFishWorkspaceFormState = {
workTitle: string;
workDescription: string;
themeTags: string;
hitObjectPrompt: string;
hitObjectReferenceImageSrc: string;
hitSoundAsset: WoodenFishAudioAsset | null;
floatingWords: string[];
};
const DEFAULT_FLOATING_WORDS = [
'幸运',
'健康',
'财富',
'姻缘',
'幸福',
'事业',
'成功',
'功德',
];
const DEFAULT_WORK_TITLE = '今日敲木鱼';
const DEFAULT_THEME_TAGS = ['敲木鱼', '解压'];
const DEFAULT_FLOATING_WORDS = ['幸运'];
const MAX_FLOATING_WORD_COUNT = 8;
const DEFAULT_FORM_STATE: WoodenFishWorkspaceFormState = {
workTitle: '今日敲木鱼',
workDescription: '',
themeTags: '敲木鱼 解压',
hitObjectPrompt: WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
hitObjectPrompt: '',
hitObjectReferenceImageSrc: '',
hitSoundAsset: null,
floatingWords: DEFAULT_FLOATING_WORDS,
};
function splitTags(value: string) {
return value
.split(/[,\s]+/u)
.map((item) => item.trim())
.filter(Boolean)
.slice(0, 6);
}
function normalizeFloatingWords(words: string[]) {
const seen = new Set<string>();
const normalized: string[] = [];
@@ -84,7 +66,7 @@ function normalizeFloatingWords(words: string[]) {
break;
}
}
return normalized.length > 0 ? normalized : DEFAULT_FLOATING_WORDS;
return normalized.length > 0 ? normalized : [...DEFAULT_FLOATING_WORDS];
}
function readAudioFileAsAsset(file: File, source: 'uploaded' | 'recorded') {
@@ -278,11 +260,42 @@ export function WoodenFishWorkspace({
() => normalizeFloatingWords(formState.floatingWords),
[formState.floatingWords],
);
const canSubmit = Boolean(
formState.workTitle.trim() &&
formState.hitObjectPrompt.trim() &&
normalizedFloatingWords.length > 0,
);
const canSubmit = normalizedFloatingWords.length > 0;
const updateFloatingWord = (index: number, value: string) => {
setFormState((current) => {
const nextWords = [...current.floatingWords];
nextWords[index] = value;
return {
...current,
floatingWords: nextWords.slice(0, MAX_FLOATING_WORD_COUNT),
};
});
};
const addFloatingWord = () => {
setFormState((current) => {
if (current.floatingWords.length >= MAX_FLOATING_WORD_COUNT) {
return current;
}
return {
...current,
floatingWords: [...current.floatingWords, ''],
};
});
};
const removeFloatingWord = (index: number) => {
if (index <= 0) {
return;
}
setFormState((current) => ({
...current,
floatingWords: current.floatingWords.filter(
(_word, currentIndex) => currentIndex !== index,
),
}));
};
const handleSubmit = async () => {
if (!canSubmit || isSubmitting || isBusy) {
@@ -296,10 +309,11 @@ export function WoodenFishWorkspace({
try {
const payload: WoodenFishWorkspaceCreateRequest = {
templateId: 'wooden-fish',
workTitle: formState.workTitle.trim(),
workDescription: formState.workDescription.trim(),
themeTags: splitTags(formState.themeTags),
hitObjectPrompt: formState.hitObjectPrompt.trim(),
workTitle: DEFAULT_WORK_TITLE,
workDescription: '',
themeTags: DEFAULT_THEME_TAGS,
hitObjectPrompt:
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
hitObjectReferenceImageSrc:
formState.hitObjectReferenceImageSrc.trim() || null,
hitSoundPrompt: null,
@@ -345,6 +359,7 @@ export function WoodenFishWorkspace({
promptRows={4}
aiRedraw={aiRedraw}
promptReferenceImages={[]}
showSubmitButton={false}
submitLabel="生成"
submitDisabled={!canSubmit || isSubmitting || isBusy}
labels={{
@@ -395,55 +410,6 @@ export function WoodenFishWorkspace({
</div>
<div className="flex min-h-0 flex-col gap-3 overflow-y-auto pr-0 lg:pr-1">
<section className="platform-subpanel rounded-[1.25rem] p-4">
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<input
value={formState.workTitle}
onChange={(event) =>
setFormState((current) => ({
...current,
workTitle: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
/>
</label>
<label className="mt-3 block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<textarea
value={formState.workDescription}
onChange={(event) =>
setFormState((current) => ({
...current,
workDescription: event.target.value,
}))
}
rows={2}
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
/>
</label>
<label className="mt-3 block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<input
value={formState.themeTags}
onChange={(event) =>
setFormState((current) => ({
...current,
themeTags: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
/>
</label>
</section>
<WoodenFishAudioInputPanel
disabled={isBusy || isSubmitting}
asset={formState.hitSoundAsset}
@@ -460,24 +426,45 @@ export function WoodenFishWorkspace({
<div className="mb-3 text-sm font-black text-[var(--platform-text-strong)]">
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="grid gap-2">
{formState.floatingWords.map((word, index) => (
<input
key={index}
value={word}
maxLength={16}
disabled={isBusy || isSubmitting}
onChange={(event) => {
const nextWords = [...formState.floatingWords];
nextWords[index] = event.target.value;
setFormState((current) => ({
...current,
floatingWords: nextWords.slice(0, 8),
}));
}}
className="w-full rounded-[0.9rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2.5 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
/>
<div key={index} className="relative">
<input
value={word}
maxLength={16}
disabled={isBusy || isSubmitting}
aria-label={`功德词条 ${index + 1}`}
onChange={(event) =>
updateFloatingWord(index, event.target.value)
}
className="h-12 w-full rounded-[0.95rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 pr-10 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
/>
{index > 0 ? (
<button
type="button"
disabled={isBusy || isSubmitting}
onClick={() => removeFloatingWord(index)}
className="absolute right-2 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-white/92 text-[var(--platform-text-soft)] shadow-sm transition hover:text-[var(--platform-accent)] disabled:opacity-45"
aria-label={`删除功德词条 ${index + 1}`}
title="删除词条"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
</div>
))}
{formState.floatingWords.length < MAX_FLOATING_WORD_COUNT ? (
<button
type="button"
disabled={isBusy || isSubmitting}
onClick={addFloatingWord}
className="grid h-12 place-items-center rounded-[0.95rem] border border-dashed border-[var(--platform-subpanel-border)] bg-white/55 text-[var(--platform-text-soft)] transition hover:border-[var(--platform-accent)] hover:bg-white/78 hover:text-[var(--platform-accent)] disabled:opacity-45"
aria-label="新增功德词条"
title="新增词条"
>
<Plus className="h-5 w-5" />
</button>
) : null}
</div>
</section>
</div>