fix: 工程类型与素材风格标签分离,支持双栏自定义标签
- 工程层面:新增 PROJECT_TYPES(2D平台跳跃/角色扮演等8种),替代原先与素材生成重复的风格标签栏 - 素材生成层面:StyleSelector 作为素材风格主配置项,新增 CustomTagsEditor 支持任务级自定义标签 - extractTags 合并工程类型、风格分类、工程自定义标签、任务自定义标签 - StyleSelector 支持 categories 参数复用 - 后端:spritesheet 拆分失败回退为单帧、GIF 上传为独立素材、AssetResponse 增加 metadata 字段
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { post } from './client'
|
||||
import { STYLE_CATEGORIES, getCustomTags } from '../utils/style'
|
||||
import { STYLE_CATEGORIES, PROJECT_TYPES, PROJECT_TYPE_KEY, getCustomTags } from '../utils/style'
|
||||
|
||||
interface OptimizePromptParams {
|
||||
tags: string[]
|
||||
@@ -17,14 +17,26 @@ interface OptimizePromptResponse {
|
||||
* 从风格键值对中提取标签的中文名称作为 tags,包含自定义标签
|
||||
*/
|
||||
export function extractTags(style: Record<string, string>): string[] {
|
||||
const presetTags = STYLE_CATEGORIES.flatMap(cat => {
|
||||
const tags: string[] = []
|
||||
|
||||
// 工程类型标签
|
||||
const projectType = style[PROJECT_TYPE_KEY]
|
||||
if (projectType) {
|
||||
const pt = PROJECT_TYPES.find(p => p.value === projectType)
|
||||
if (pt) tags.push(pt.label)
|
||||
}
|
||||
|
||||
// 风格分类标签
|
||||
for (const cat of STYLE_CATEGORIES) {
|
||||
const value = style[cat.key]
|
||||
if (!value) return []
|
||||
if (!value) continue
|
||||
const option = cat.options.find(o => o.value === value)
|
||||
return option ? [option.label] : []
|
||||
})
|
||||
const customTags = getCustomTags(style)
|
||||
return [...presetTags, ...customTags]
|
||||
if (option) tags.push(option.label)
|
||||
}
|
||||
|
||||
// 自定义标签
|
||||
tags.push(...getCustomTags(style))
|
||||
return tags
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -127,6 +127,7 @@ export interface AssetsResponse {
|
||||
frameHeight: number
|
||||
frameCount: number
|
||||
directions: number
|
||||
gifUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { PROJECT_TYPES, PROJECT_TYPE_KEY } from '../utils/style'
|
||||
import StyleSelector from './StyleSelector'
|
||||
import CustomTagsEditor from './CustomTagsEditor'
|
||||
|
||||
@@ -91,9 +92,18 @@ export default function CreateProjectModal({
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
||||
风格配置
|
||||
工程类型
|
||||
</span>
|
||||
<StyleSelector value={style} onChange={handleStyleChange} compact />
|
||||
<StyleSelector
|
||||
value={style}
|
||||
onChange={handleStyleChange}
|
||||
compact
|
||||
categories={[{
|
||||
key: PROJECT_TYPE_KEY,
|
||||
label: '工程类型',
|
||||
options: PROJECT_TYPES.map(p => ({ value: p.value, label: p.label })),
|
||||
}]}
|
||||
/>
|
||||
<CustomTagsEditor style={style} onChange={setStyle} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useProjectStore } from '../stores/project'
|
||||
import { useTaskStore } from '../stores/task'
|
||||
import { mergeStyles } from '../utils/style'
|
||||
import StyleSelector from './StyleSelector'
|
||||
import CustomTagsEditor from './CustomTagsEditor'
|
||||
import PromptEditor from './PromptEditor'
|
||||
|
||||
const ASSET_TYPE_ICONS: Record<AssetType, JSX.Element> = {
|
||||
@@ -53,6 +54,7 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
||||
setPrompt,
|
||||
setAssetType,
|
||||
toggleTaskStyle,
|
||||
setTaskStyle,
|
||||
setParams,
|
||||
setEnableAI,
|
||||
setOptimizedPrompt,
|
||||
@@ -115,12 +117,13 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 任务风格覆盖 */}
|
||||
{/* 素材风格配置 */}
|
||||
<div>
|
||||
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
||||
风格覆盖(可选)
|
||||
素材风格
|
||||
</h3>
|
||||
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
|
||||
<CustomTagsEditor style={taskStyle} onChange={setTaskStyle} />
|
||||
</div>
|
||||
|
||||
{/* 提示词(含 AI 优化) */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import type { Project } from '../api/types'
|
||||
import { STYLE_CATEGORIES } from '../utils/style'
|
||||
import { PROJECT_TYPES, PROJECT_TYPE_KEY, getCustomTags } from '../utils/style'
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: Project
|
||||
@@ -23,12 +23,11 @@ function formatRelativeTime(dateStr: string): string {
|
||||
export default function ProjectCard({ project, onDelete }: ProjectCardProps) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const presetTags = STYLE_CATEGORIES.flatMap(cat => {
|
||||
const value = project.style.kvPairs[cat.key]
|
||||
if (!value) return []
|
||||
const option = cat.options.find(o => o.value === value)
|
||||
return option ? [option.label] : []
|
||||
}).slice(0, 4)
|
||||
const kvPairs = project.style.kvPairs
|
||||
const projectTypeValue = kvPairs[PROJECT_TYPE_KEY]
|
||||
const projectTypeLabel = PROJECT_TYPES.find(p => p.value === projectTypeValue)?.label
|
||||
const customTags = getCustomTags(kvPairs).slice(0, 4)
|
||||
const tags = projectTypeLabel ? [projectTypeLabel, ...customTags].slice(0, 4) : customTags
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -59,7 +58,7 @@ export default function ProjectCard({ project, onDelete }: ProjectCardProps) {
|
||||
)}
|
||||
<h3 style={{ fontSize: 16, marginBottom: 12 }}>{project.name}</h3>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
||||
{presetTags.map(tag => (
|
||||
{tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { PROJECT_TYPES, PROJECT_TYPE_KEY } from '../utils/style'
|
||||
import StyleSelector from './StyleSelector'
|
||||
import CustomTagsEditor from './CustomTagsEditor'
|
||||
import Skeleton from './Skeleton'
|
||||
@@ -73,9 +74,18 @@ export default function ProjectConfigPanel() {
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: '12px 16px' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
||||
工程风格
|
||||
工程类型
|
||||
</div>
|
||||
<StyleSelector value={draftStyle} onChange={updateDraft} compact />
|
||||
<StyleSelector
|
||||
value={draftStyle}
|
||||
onChange={updateDraft}
|
||||
compact
|
||||
categories={[{
|
||||
key: PROJECT_TYPE_KEY,
|
||||
label: '工程类型',
|
||||
options: PROJECT_TYPES.map(p => ({ value: p.value, label: p.label })),
|
||||
}]}
|
||||
/>
|
||||
<CustomTagsEditor style={draftStyle} onChange={setDraftStyle} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STYLE_CATEGORIES } from '../utils/style'
|
||||
import { STYLE_CATEGORIES, PROJECT_TYPES, PROJECT_TYPE_KEY, getCustomTags } from '../utils/style'
|
||||
import styles from './GenerateForm.module.css'
|
||||
|
||||
interface PromptEditorProps {
|
||||
@@ -17,14 +17,25 @@ interface PromptEditorProps {
|
||||
}
|
||||
|
||||
function styleToDescription(kvPairs: Record<string, string>): string {
|
||||
return STYLE_CATEGORIES.map(cat => {
|
||||
const parts: string[] = []
|
||||
|
||||
const projectType = kvPairs[PROJECT_TYPE_KEY]
|
||||
if (projectType) {
|
||||
const pt = PROJECT_TYPES.find(p => p.value === projectType)
|
||||
if (pt) parts.push(`工程类型: ${pt.label}`)
|
||||
}
|
||||
|
||||
for (const cat of STYLE_CATEGORIES) {
|
||||
const value = kvPairs[cat.key]
|
||||
if (!value) return null
|
||||
if (!value) continue
|
||||
const option = cat.options.find(o => o.value === value)
|
||||
return option ? `${cat.label}: ${option.label}` : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
if (option) parts.push(`${cat.label}: ${option.label}`)
|
||||
}
|
||||
|
||||
const customTags = getCustomTags(kvPairs)
|
||||
if (customTags.length > 0) parts.push(`标签: ${customTags.join(', ')}`)
|
||||
|
||||
return parts.join(', ')
|
||||
}
|
||||
|
||||
export default function PromptEditor({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { StyleCategory } from '../utils/style'
|
||||
import { STYLE_CATEGORIES } from '../utils/style'
|
||||
import styles from './StyleSelector.module.css'
|
||||
|
||||
@@ -5,16 +6,19 @@ interface StyleSelectorProps {
|
||||
value: Record<string, string>
|
||||
onChange: (key: string, value: string) => void
|
||||
compact?: boolean
|
||||
categories?: StyleCategory[]
|
||||
}
|
||||
|
||||
export default function StyleSelector({
|
||||
value,
|
||||
onChange,
|
||||
compact,
|
||||
categories,
|
||||
}: StyleSelectorProps) {
|
||||
const cats = categories ?? STYLE_CATEGORIES
|
||||
return (
|
||||
<div className={`${styles.container} ${compact ? styles.compact : ''}`}>
|
||||
{STYLE_CATEGORIES.map(cat => (
|
||||
{cats.map(cat => (
|
||||
<div key={cat.key} className={styles.category}>
|
||||
<span className={styles.label}>{cat.label}</span>
|
||||
<div className={styles.options}>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface TaskState {
|
||||
setPrompt: (text: string) => void
|
||||
setAssetType: (type: AssetType) => void
|
||||
toggleTaskStyle: (key: string, value: string) => void
|
||||
setTaskStyle: (style: Record<string, string>) => void
|
||||
setParams: (params: Partial<TaskParams>) => void
|
||||
setEnableAI: (enable: boolean) => void
|
||||
setOptimizedPrompt: (text: string | null) => void
|
||||
@@ -56,6 +57,8 @@ export const useTaskStore = create<TaskState>((set, get) => ({
|
||||
return { taskStyle: next, optimizedPrompt: null }
|
||||
}),
|
||||
|
||||
setTaskStyle: (style) => set({ taskStyle: style, optimizedPrompt: null }),
|
||||
|
||||
setParams: (params) =>
|
||||
set(state => ({ params: { ...state.params, ...params } })),
|
||||
|
||||
|
||||
@@ -44,6 +44,21 @@ export interface StyleCategory {
|
||||
options: { value: string; label: string }[]
|
||||
}
|
||||
|
||||
/** 工程类型键名 */
|
||||
export const PROJECT_TYPE_KEY = 'projectType'
|
||||
|
||||
/** 工程类型选项(决定项目整体类型,区别于素材生成的风格标签) */
|
||||
export const PROJECT_TYPES: { value: string; label: string }[] = [
|
||||
{ value: '2d-platformer', label: '2D 平台跳跃' },
|
||||
{ value: 'rpg', label: '角色扮演' },
|
||||
{ value: 'top-down', label: '俯视视角' },
|
||||
{ value: 'visual-novel', label: '视觉小说' },
|
||||
{ value: 'tower-defense', label: '塔防' },
|
||||
{ value: 'puzzle', label: '益智解谜' },
|
||||
{ value: 'shooter', label: '射击' },
|
||||
{ value: 'roguelike', label: '肉鸽' },
|
||||
]
|
||||
|
||||
export const STYLE_CATEGORIES: StyleCategory[] = [
|
||||
{
|
||||
key: 'artStyle',
|
||||
|
||||
Reference in New Issue
Block a user