f44fd56e22
- StyleSelector 新增 customTags 属性,自定义标签作为「自定义标签」行展示在原生标签栏中 - 自定义标签点击切换 true/false,不再移除 key,解决点击消失问题 - CustomTagsEditor 简化为仅输入框+添加按钮 - extractTags 仅提取激活态自定义标签(value=true) - 导出 CUSTOM_PREFIX 供组件使用
214 lines
7.2 KiB
TypeScript
Executable File
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>
|
|
)
|
|
}
|