fix: 工程类型与素材风格标签分离,支持双栏自定义标签
- 工程层面:新增 PROJECT_TYPES(2D平台跳跃/角色扮演等8种),替代原先与素材生成重复的风格标签栏 - 素材生成层面:StyleSelector 作为素材风格主配置项,新增 CustomTagsEditor 支持任务级自定义标签 - extractTags 合并工程类型、风格分类、工程自定义标签、任务自定义标签 - StyleSelector 支持 categories 参数复用 - 后端:spritesheet 拆分失败回退为单帧、GIF 上传为独立素材、AssetResponse 增加 metadata 字段
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gen2d/internal/db"
|
"gen2d/internal/db"
|
||||||
@@ -135,6 +136,7 @@ func runPipelineBg(ctx context.Context, projectID, taskID string, req GenerateRe
|
|||||||
updateTaskInDB(ctx, taskID, "saving", "format_adapter", "", 90)
|
updateTaskInDB(ctx, taskID, "saving", "format_adapter", "", 90)
|
||||||
|
|
||||||
// 上传素材并保存到数据库
|
// 上传素材并保存到数据库
|
||||||
|
var lastCDNURL string
|
||||||
for i, a := range output.Assets {
|
for i, a := range output.Assets {
|
||||||
key := fmt.Sprintf("generation/%s/%s/%d.%s", projectID, taskID, i, a.Format)
|
key := fmt.Sprintf("generation/%s/%s/%d.%s", projectID, taskID, i, a.Format)
|
||||||
cdnURL, err := storageSvc.Upload(ctx, key, a.Data)
|
cdnURL, err := storageSvc.Upload(ctx, key, a.Data)
|
||||||
@@ -143,15 +145,15 @@ func runPipelineBg(ctx context.Context, projectID, taskID string, req GenerateRe
|
|||||||
updateTaskInDB(ctx, taskID, "failed", "", "上传素材失败: "+err.Error(), 0)
|
updateTaskInDB(ctx, taskID, "failed", "", "上传素材失败: "+err.Error(), 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
lastCDNURL = cdnURL
|
||||||
|
|
||||||
// 序列化单个素材的元数据
|
var assetMeta map[string]interface{}
|
||||||
var metadata map[string]interface{}
|
if a.Format == "gif" {
|
||||||
if i < len(output.Assets) {
|
assetMeta = map[string]interface{}{"index": i, "type": "preview"}
|
||||||
metadata = map[string]interface{}{
|
} else {
|
||||||
"index": i,
|
assetMeta = map[string]interface{}{"index": i}
|
||||||
}
|
}
|
||||||
}
|
metadataJSON, _ := json.Marshal(assetMeta)
|
||||||
metadataJSON, _ := json.Marshal(metadata)
|
|
||||||
|
|
||||||
asset := &model.Asset{
|
asset := &model.Asset{
|
||||||
TaskID: getTaskDBID(ctx, taskID),
|
TaskID: getTaskDBID(ctx, taskID),
|
||||||
@@ -166,7 +168,11 @@ func runPipelineBg(ctx context.Context, projectID, taskID string, req GenerateRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新为完成状态
|
// GIF URL 替换为实际上传后的 CDN 地址
|
||||||
|
if output.Metadata.GIFURL != "" && lastCDNURL != "" {
|
||||||
|
output.Metadata.GIFURL = lastCDNURL
|
||||||
|
}
|
||||||
|
|
||||||
var fullMetadata string
|
var fullMetadata string
|
||||||
if metadataJSON, err := json.Marshal(output.Metadata); err == nil {
|
if metadataJSON, err := json.Marshal(output.Metadata); err == nil {
|
||||||
fullMetadata = string(metadataJSON)
|
fullMetadata = string(metadataJSON)
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ type AssetResponse struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
|
Metadata string `json:"metadata"` // 素材元数据(JSON: {"index":0,"type":"frame"|"preview"|"spritesheet"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -159,6 +160,7 @@ var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input
|
|||||||
})
|
})
|
||||||
|
|
||||||
// processSpriteSheet 将单张精灵表拆分为独立帧并生成 GIF 预览。
|
// processSpriteSheet 将单张精灵表拆分为独立帧并生成 GIF 预览。
|
||||||
|
// 拆分失败时回退为单帧,不阻塞管线。
|
||||||
func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetParams, resolution int) (PipelineOutput, error) {
|
func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetParams, resolution int) (PipelineOutput, error) {
|
||||||
l := logger.FromCtx(ctx)
|
l := logger.FromCtx(ctx)
|
||||||
src, err := png.Decode(bytes.NewReader(img.Data))
|
src, err := png.Decode(bytes.NewReader(img.Data))
|
||||||
@@ -175,13 +177,19 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
|
|||||||
|
|
||||||
frames, err := splitsprite.Process(src, opts)
|
frames, err := splitsprite.Process(src, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error("format_adapter split sprite sheet failed", "error", err)
|
l.Warn("format_adapter split sprite sheet failed, falling back to single frame", "error", err)
|
||||||
return PipelineOutput{}, fmt.Errorf("split sprite sheet: %w", err)
|
// 回退:整张图作为单帧
|
||||||
|
frames = []image.Image{src}
|
||||||
}
|
}
|
||||||
l.Info("format_adapter split sprite sheet", "frame_count", len(frames))
|
l.Info("format_adapter split sprite sheet", "frame_count", len(frames))
|
||||||
|
|
||||||
// 帧 → Asset
|
// 帧 → Asset(先保留原始精灵表,再追加拆分后的帧)
|
||||||
assets := make([]Asset, 0, len(frames))
|
assets := make([]Asset, 0, len(frames)+2)
|
||||||
|
assets = append(assets, Asset{
|
||||||
|
Data: img.Data,
|
||||||
|
Format: img.Format,
|
||||||
|
URL: "output/spritesheet.png",
|
||||||
|
})
|
||||||
for i, f := range frames {
|
for i, f := range frames {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := png.Encode(&buf, f); err != nil {
|
if err := png.Encode(&buf, f); err != nil {
|
||||||
@@ -195,12 +203,19 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GIF 预览
|
// GIF 预览 → Asset(排在帧之后)
|
||||||
|
var gifURL string
|
||||||
var gifBuf bytes.Buffer
|
var gifBuf bytes.Buffer
|
||||||
if err := gifmaker.Encode(&gifBuf, frames, nil); err != nil {
|
if err := gifmaker.Encode(&gifBuf, frames, nil); err != nil {
|
||||||
l.Warn("format_adapter generate GIF preview failed", "error", err)
|
l.Warn("format_adapter generate GIF preview failed", "error", err)
|
||||||
} else {
|
} else {
|
||||||
l.Info("format_adapter generated GIF preview", "size_bytes", gifBuf.Len())
|
l.Info("format_adapter generated GIF preview", "size_bytes", gifBuf.Len())
|
||||||
|
gifURL = "output/preview.gif"
|
||||||
|
assets = append(assets, Asset{
|
||||||
|
Data: gifBuf.Bytes(),
|
||||||
|
Format: "gif",
|
||||||
|
URL: gifURL,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, fh := 0, 0
|
fw, fh := 0, 0
|
||||||
@@ -216,7 +231,7 @@ func processSpriteSheet(ctx context.Context, img GeneratedImage, params AssetPar
|
|||||||
FrameHeight: fh,
|
FrameHeight: fh,
|
||||||
FrameCount: len(frames),
|
FrameCount: len(frames),
|
||||||
Directions: params.Frames.Directions,
|
Directions: params.Frames.Directions,
|
||||||
GIFPreview: gifBuf.Bytes(),
|
GIFURL: gifURL,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -234,7 +249,7 @@ func buildStyleDescription(projectStyle, taskStyle map[string]string) string {
|
|||||||
return "风格约束:" + strings.Join(parts, ";")
|
return "风格约束:" + strings.Join(parts, ";")
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendTechNotes 在无标签(不走 PromptAgent)时补上技术参数段。
|
// appendTechNotes 在提示词末尾追加技术参数段。
|
||||||
func appendTechNotes(prompt, assetType string, params AssetParams) string {
|
func appendTechNotes(prompt, assetType string, params AssetParams) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
if prompt != "" {
|
if prompt != "" {
|
||||||
@@ -250,7 +265,9 @@ func appendTechNotes(prompt, assetType string, params AssetParams) string {
|
|||||||
if params.Frames.FramesPerDirection > 0 {
|
if params.Frames.FramesPerDirection > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("每方向帧数: %d", params.Frames.FramesPerDirection))
|
parts = append(parts, fmt.Sprintf("每方向帧数: %d", params.Frames.FramesPerDirection))
|
||||||
}
|
}
|
||||||
if params.Format != "" {
|
if params.Format == "spritesheet" {
|
||||||
|
parts = append(parts, "输出格式: spritesheet(将所有帧排列在一张图上,帧之间用8-16像素纯白间隙分隔,等间距网格布局)")
|
||||||
|
} else if params.Format != "" {
|
||||||
parts = append(parts, fmt.Sprintf("输出格式: %s", params.Format))
|
parts = append(parts, fmt.Sprintf("输出格式: %s", params.Format))
|
||||||
}
|
}
|
||||||
return strings.Join(parts, ";")
|
return strings.Join(parts, ";")
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ type Asset struct {
|
|||||||
|
|
||||||
// AssetMetadata 素材元数据
|
// AssetMetadata 素材元数据
|
||||||
type AssetMetadata struct {
|
type AssetMetadata struct {
|
||||||
FrameWidth int
|
FrameWidth int `json:"frameWidth"`
|
||||||
FrameHeight int
|
FrameHeight int `json:"frameHeight"`
|
||||||
FrameCount int
|
FrameCount int `json:"frameCount"`
|
||||||
Directions int
|
Directions int `json:"directions"`
|
||||||
GIFPreview []byte `json:"-"` // animated GIF preview (not serialized)
|
GIFURL string `json:"gifUrl,omitempty"` // animated GIF preview URL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { post } from './client'
|
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 {
|
interface OptimizePromptParams {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
@@ -17,14 +17,26 @@ interface OptimizePromptResponse {
|
|||||||
* 从风格键值对中提取标签的中文名称作为 tags,包含自定义标签
|
* 从风格键值对中提取标签的中文名称作为 tags,包含自定义标签
|
||||||
*/
|
*/
|
||||||
export function extractTags(style: Record<string, string>): string[] {
|
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]
|
const value = style[cat.key]
|
||||||
if (!value) return []
|
if (!value) continue
|
||||||
const option = cat.options.find(o => o.value === value)
|
const option = cat.options.find(o => o.value === value)
|
||||||
return option ? [option.label] : []
|
if (option) tags.push(option.label)
|
||||||
})
|
}
|
||||||
const customTags = getCustomTags(style)
|
|
||||||
return [...presetTags, ...customTags]
|
// 自定义标签
|
||||||
|
tags.push(...getCustomTags(style))
|
||||||
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export interface AssetsResponse {
|
|||||||
frameHeight: number
|
frameHeight: number
|
||||||
frameCount: number
|
frameCount: number
|
||||||
directions: number
|
directions: number
|
||||||
|
gifUrl?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { PROJECT_TYPES, PROJECT_TYPE_KEY } from '../utils/style'
|
||||||
import StyleSelector from './StyleSelector'
|
import StyleSelector from './StyleSelector'
|
||||||
import CustomTagsEditor from './CustomTagsEditor'
|
import CustomTagsEditor from './CustomTagsEditor'
|
||||||
|
|
||||||
@@ -91,9 +92,18 @@ export default function CreateProjectModal({
|
|||||||
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<span style={{ display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
<span style={{ display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
||||||
风格配置
|
工程类型
|
||||||
</span>
|
</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} />
|
<CustomTagsEditor style={style} onChange={setStyle} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useProjectStore } from '../stores/project'
|
|||||||
import { useTaskStore } from '../stores/task'
|
import { useTaskStore } from '../stores/task'
|
||||||
import { mergeStyles } from '../utils/style'
|
import { mergeStyles } from '../utils/style'
|
||||||
import StyleSelector from './StyleSelector'
|
import StyleSelector from './StyleSelector'
|
||||||
|
import CustomTagsEditor from './CustomTagsEditor'
|
||||||
import PromptEditor from './PromptEditor'
|
import PromptEditor from './PromptEditor'
|
||||||
|
|
||||||
const ASSET_TYPE_ICONS: Record<AssetType, JSX.Element> = {
|
const ASSET_TYPE_ICONS: Record<AssetType, JSX.Element> = {
|
||||||
@@ -53,6 +54,7 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
|||||||
setPrompt,
|
setPrompt,
|
||||||
setAssetType,
|
setAssetType,
|
||||||
toggleTaskStyle,
|
toggleTaskStyle,
|
||||||
|
setTaskStyle,
|
||||||
setParams,
|
setParams,
|
||||||
setEnableAI,
|
setEnableAI,
|
||||||
setOptimizedPrompt,
|
setOptimizedPrompt,
|
||||||
@@ -115,12 +117,13 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 任务风格覆盖 */}
|
{/* 素材风格配置 */}
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
||||||
风格覆盖(可选)
|
素材风格
|
||||||
</h3>
|
</h3>
|
||||||
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
|
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
|
||||||
|
<CustomTagsEditor style={taskStyle} onChange={setTaskStyle} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 提示词(含 AI 优化) */}
|
{/* 提示词(含 AI 优化) */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import type { Project } from '../api/types'
|
import type { Project } from '../api/types'
|
||||||
import { STYLE_CATEGORIES } from '../utils/style'
|
import { PROJECT_TYPES, PROJECT_TYPE_KEY, getCustomTags } from '../utils/style'
|
||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: Project
|
project: Project
|
||||||
@@ -23,12 +23,11 @@ function formatRelativeTime(dateStr: string): string {
|
|||||||
export default function ProjectCard({ project, onDelete }: ProjectCardProps) {
|
export default function ProjectCard({ project, onDelete }: ProjectCardProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const presetTags = STYLE_CATEGORIES.flatMap(cat => {
|
const kvPairs = project.style.kvPairs
|
||||||
const value = project.style.kvPairs[cat.key]
|
const projectTypeValue = kvPairs[PROJECT_TYPE_KEY]
|
||||||
if (!value) return []
|
const projectTypeLabel = PROJECT_TYPES.find(p => p.value === projectTypeValue)?.label
|
||||||
const option = cat.options.find(o => o.value === value)
|
const customTags = getCustomTags(kvPairs).slice(0, 4)
|
||||||
return option ? [option.label] : []
|
const tags = projectTypeLabel ? [projectTypeLabel, ...customTags].slice(0, 4) : customTags
|
||||||
}).slice(0, 4)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -59,7 +58,7 @@ export default function ProjectCard({ project, onDelete }: ProjectCardProps) {
|
|||||||
)}
|
)}
|
||||||
<h3 style={{ fontSize: 16, marginBottom: 12 }}>{project.name}</h3>
|
<h3 style={{ fontSize: 16, marginBottom: 12 }}>{project.name}</h3>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
||||||
{presetTags.map(tag => (
|
{tags.map(tag => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
|
import { PROJECT_TYPES, PROJECT_TYPE_KEY } from '../utils/style'
|
||||||
import StyleSelector from './StyleSelector'
|
import StyleSelector from './StyleSelector'
|
||||||
import CustomTagsEditor from './CustomTagsEditor'
|
import CustomTagsEditor from './CustomTagsEditor'
|
||||||
import Skeleton from './Skeleton'
|
import Skeleton from './Skeleton'
|
||||||
@@ -73,9 +74,18 @@ export default function ProjectConfigPanel() {
|
|||||||
|
|
||||||
<div style={{ flex: 1, overflow: 'auto', padding: '12px 16px' }}>
|
<div style={{ flex: 1, overflow: 'auto', padding: '12px 16px' }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--text-secondary)' }}>
|
||||||
工程风格
|
工程类型
|
||||||
</div>
|
</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} />
|
<CustomTagsEditor style={draftStyle} onChange={setDraftStyle} />
|
||||||
</div>
|
</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'
|
import styles from './GenerateForm.module.css'
|
||||||
|
|
||||||
interface PromptEditorProps {
|
interface PromptEditorProps {
|
||||||
@@ -17,14 +17,25 @@ interface PromptEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function styleToDescription(kvPairs: Record<string, string>): string {
|
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]
|
const value = kvPairs[cat.key]
|
||||||
if (!value) return null
|
if (!value) continue
|
||||||
const option = cat.options.find(o => o.value === value)
|
const option = cat.options.find(o => o.value === value)
|
||||||
return option ? `${cat.label}: ${option.label}` : null
|
if (option) parts.push(`${cat.label}: ${option.label}`)
|
||||||
})
|
}
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ')
|
const customTags = getCustomTags(kvPairs)
|
||||||
|
if (customTags.length > 0) parts.push(`标签: ${customTags.join(', ')}`)
|
||||||
|
|
||||||
|
return parts.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PromptEditor({
|
export default function PromptEditor({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { StyleCategory } from '../utils/style'
|
||||||
import { STYLE_CATEGORIES } from '../utils/style'
|
import { STYLE_CATEGORIES } from '../utils/style'
|
||||||
import styles from './StyleSelector.module.css'
|
import styles from './StyleSelector.module.css'
|
||||||
|
|
||||||
@@ -5,16 +6,19 @@ interface StyleSelectorProps {
|
|||||||
value: Record<string, string>
|
value: Record<string, string>
|
||||||
onChange: (key: string, value: string) => void
|
onChange: (key: string, value: string) => void
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
categories?: StyleCategory[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StyleSelector({
|
export default function StyleSelector({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
compact,
|
compact,
|
||||||
|
categories,
|
||||||
}: StyleSelectorProps) {
|
}: StyleSelectorProps) {
|
||||||
|
const cats = categories ?? STYLE_CATEGORIES
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.container} ${compact ? styles.compact : ''}`}>
|
<div className={`${styles.container} ${compact ? styles.compact : ''}`}>
|
||||||
{STYLE_CATEGORIES.map(cat => (
|
{cats.map(cat => (
|
||||||
<div key={cat.key} className={styles.category}>
|
<div key={cat.key} className={styles.category}>
|
||||||
<span className={styles.label}>{cat.label}</span>
|
<span className={styles.label}>{cat.label}</span>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface TaskState {
|
|||||||
setPrompt: (text: string) => void
|
setPrompt: (text: string) => void
|
||||||
setAssetType: (type: AssetType) => void
|
setAssetType: (type: AssetType) => void
|
||||||
toggleTaskStyle: (key: string, value: string) => void
|
toggleTaskStyle: (key: string, value: string) => void
|
||||||
|
setTaskStyle: (style: Record<string, string>) => void
|
||||||
setParams: (params: Partial<TaskParams>) => void
|
setParams: (params: Partial<TaskParams>) => void
|
||||||
setEnableAI: (enable: boolean) => void
|
setEnableAI: (enable: boolean) => void
|
||||||
setOptimizedPrompt: (text: string | null) => void
|
setOptimizedPrompt: (text: string | null) => void
|
||||||
@@ -56,6 +57,8 @@ export const useTaskStore = create<TaskState>((set, get) => ({
|
|||||||
return { taskStyle: next, optimizedPrompt: null }
|
return { taskStyle: next, optimizedPrompt: null }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setTaskStyle: (style) => set({ taskStyle: style, optimizedPrompt: null }),
|
||||||
|
|
||||||
setParams: (params) =>
|
setParams: (params) =>
|
||||||
set(state => ({ params: { ...state.params, ...params } })),
|
set(state => ({ params: { ...state.params, ...params } })),
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ export interface StyleCategory {
|
|||||||
options: { value: string; label: string }[]
|
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[] = [
|
export const STYLE_CATEGORIES: StyleCategory[] = [
|
||||||
{
|
{
|
||||||
key: 'artStyle',
|
key: 'artStyle',
|
||||||
|
|||||||
Reference in New Issue
Block a user