Files
gen2d/frontend/src/components/GenerateForm.tsx
T
Gmarker689 f44fd56e22 fix: 自定义标签融入原生标签栏,点击切换而非消失
- StyleSelector 新增 customTags 属性,自定义标签作为「自定义标签」行展示在原生标签栏中
- 自定义标签点击切换 true/false,不再移除 key,解决点击消失问题
- CustomTagsEditor 简化为仅输入框+添加按钮
- extractTags 仅提取激活态自定义标签(value=true)
- 导出 CUSTOM_PREFIX 供组件使用
2026-05-25 19:55:29 +08:00

214 lines
7.2 KiB
TypeScript
Executable File

import type { AssetType } from '../api/types'
import { useProjectStore } from '../stores/project'
import { useTaskStore } from '../stores/task'
import { mergeStyles, getCustomTags } from '../utils/style'
import StyleSelector from './StyleSelector'
import CustomTagsEditor from './CustomTagsEditor'
import PromptEditor from './PromptEditor'
const ASSET_TYPE_ICONS: Record<AssetType, JSX.Element> = {
sprite: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2a3 3 0 0 1 3 3c0 1.1-.6 2.1-1.5 2.6L17 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-1.2l-1.4 5.5a1 1 0 0 1-1 .5H10.6a1 1 0 0 1-1-.5L8.2 16H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1h2l3.5-4.4A3 3 0 0 1 12 2z" />
</svg>
),
background: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm0 2v8.6l4.3-4.3a1.5 1.5 0 0 1 2.1 0L14 13.9l2.4-2.4a1.5 1.5 0 0 1 2.1 0L20 13V6H4zm0 12h16v-1.4l-3.5-3.5-2.4 2.4a1.5 1.5 0 0 1-2.1 0L6.4 12 4 14.4V18z" />
</svg>
),
ui: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1zm0 2v12h18V6H3zm2 2h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1zm1 1v4h2V9H6zm7-1h5a1 1 0 0 1 0 2h-5a1 1 0 0 1 0-2zm0 4h3a1 1 0 0 1 0 2h-3a1 1 0 0 1 0-2z" />
</svg>
),
animation: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7L8 5zm-2 1.5L16 12 6 17.5V6.5z" />
</svg>
),
}
const ASSET_TYPES: { value: AssetType; label: string }[] = [
{ value: 'sprite', label: '精灵' },
{ value: 'background', label: '背景' },
{ value: 'ui', label: 'UI' },
{ value: 'animation', label: '动画' },
]
interface GenerateFormProps {
onSubmit: (prompt: string) => void
submitting: boolean
}
export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps) {
const { style: projectStyle } = useProjectStore()
const {
prompt,
assetType,
taskStyle,
params,
enableAI,
optimizedPrompt,
isOptimizing,
setPrompt,
setAssetType,
toggleTaskStyle,
setTaskStyle,
setParams,
setEnableAI,
setOptimizedPrompt,
runOptimize,
} = useTaskStore()
const mergedStyle = mergeStyles(projectStyle, taskStyle)
const finalPrompt = enableAI && optimizedPrompt ? optimizedPrompt : prompt
const handleOptimize = async () => {
try {
await runOptimize(projectStyle)
} catch {
// error handled in store
}
}
const handleSubmit = () => {
onSubmit(finalPrompt)
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{/* 素材类型 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
素材类型
</h3>
<div style={{ display: 'flex', gap: 12 }}>
{ASSET_TYPES.map(t => {
const isSelected = assetType === t.value
return (
<button
key={t.value}
type="button"
onClick={() => setAssetType(t.value)}
style={{
width: 80,
height: 72,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
borderRadius: 'var(--radius-lg)',
border: `2px solid ${isSelected ? 'var(--accent)' : 'var(--border)'}`,
background: isSelected ? 'var(--accent-dim)' : 'transparent',
color: isSelected ? 'var(--accent)' : 'var(--text-secondary)',
cursor: 'pointer',
transition: 'all 0.2s ease',
fontSize: 13,
fontWeight: isSelected ? 600 : 400,
}}
>
{ASSET_TYPE_ICONS[t.value]}
{t.label}
</button>
)
})}
</div>
</div>
{/* 素材风格配置 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
素材风格
</h3>
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact customTags={getCustomTags(taskStyle)} />
<CustomTagsEditor style={taskStyle} onChange={setTaskStyle} />
</div>
{/* 提示词(含 AI 优化) */}
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
}}
>
<h3 style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
提示词
</h3>
{enableAI && (
<button
type="button"
className="btn-secondary"
onClick={handleOptimize}
disabled={isOptimizing || !prompt.trim()}
style={{ padding: '6px 16px', fontSize: 13 }}
>
{isOptimizing ? '优化中...' : '优化提示词'}
</button>
)}
</div>
<PromptEditor
prompt={prompt}
onPromptChange={setPrompt}
style={mergedStyle}
params={{ resolution: params.resolution, format: params.format }}
enableAI={enableAI}
onEnableAIToggle={setEnableAI}
optimizedPrompt={optimizedPrompt}
isOptimizing={isOptimizing}
onOptimizedPromptChange={setOptimizedPrompt}
/>
</div>
{/* 参数 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
参数
</h3>
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>分辨率</span>
<input
type="number"
value={params.resolution}
onChange={e => setParams({ resolution: Number(e.target.value) })}
min={1024}
max={1536}
step={512}
style={{ width: 100 }}
/>
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>px</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>格式</span>
<select
value={params.format}
onChange={e =>
setParams({ format: e.target.value as 'spritesheet' | 'individual' })
}
>
<option value="spritesheet">Spritesheet</option>
<option value="individual">单帧</option>
</select>
</label>
</div>
</div>
{/* 提交 */}
<button
type="button"
className="btn-primary"
onClick={handleSubmit}
disabled={submitting || !finalPrompt.trim()}
style={{ alignSelf: 'flex-start', padding: '12px 32px', fontSize: 16 }}
>
{submitting ? '提交中...' : '开始生成'}
</button>
</div>
)
}