feat: 首页素材画廊增加分类筛选标签
- 后端 SQL 增加 json_extract 提取素材 metaType (frame/spritesheet/preview) - 前端 AssetGallery 新增分类标签栏:全部/精灵表/帧/场景瓦片/背景/UI - 按 metaType + assetType 组合过滤展示
This commit is contained in:
@@ -313,13 +313,14 @@ type RecentAssetItem struct {
|
|||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
AssetType string `json:"assetType"`
|
AssetType string `json:"assetType"`
|
||||||
|
MetaType string `json:"metaType"` // frame / spritesheet / preview
|
||||||
TaskID string `json:"taskId"`
|
TaskID string `json:"taskId"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecentAssets 获取最近生成的素材列表(已完成任务的全部素材)。
|
// GetRecentAssets 获取最近生成的素材列表(已完成任务的全部素材)。
|
||||||
func GetRecentAssets(c *gin.Context) {
|
func GetRecentAssets(c *gin.Context) {
|
||||||
limit := 20
|
limit := 60
|
||||||
|
|
||||||
var dbRows []struct {
|
var dbRows []struct {
|
||||||
Key string
|
Key string
|
||||||
@@ -327,6 +328,7 @@ func GetRecentAssets(c *gin.Context) {
|
|||||||
Format string
|
Format string
|
||||||
Prompt string
|
Prompt string
|
||||||
AssetType string
|
AssetType string
|
||||||
|
MetaType string
|
||||||
TaskID string
|
TaskID string
|
||||||
ProjectID uint
|
ProjectID uint
|
||||||
CreatedAt string
|
CreatedAt string
|
||||||
@@ -334,6 +336,7 @@ func GetRecentAssets(c *gin.Context) {
|
|||||||
|
|
||||||
db.GetDB().WithContext(c.Request.Context()).
|
db.GetDB().WithContext(c.Request.Context()).
|
||||||
Raw(`SELECT a.key, a.url, a.format, t.prompt, t.asset_type as asset_type,
|
Raw(`SELECT a.key, a.url, a.format, t.prompt, t.asset_type as asset_type,
|
||||||
|
COALESCE(json_extract(a.metadata, '$.type'), 'frame') as meta_type,
|
||||||
t.external_id as task_id, t.project_id, t.created_at as created_at
|
t.external_id as task_id, t.project_id, t.created_at as created_at
|
||||||
FROM assets a
|
FROM assets a
|
||||||
INNER JOIN tasks t ON a.task_id = t.id
|
INNER JOIN tasks t ON a.task_id = t.id
|
||||||
@@ -350,6 +353,7 @@ func GetRecentAssets(c *gin.Context) {
|
|||||||
Format: r.Format,
|
Format: r.Format,
|
||||||
Prompt: r.Prompt,
|
Prompt: r.Prompt,
|
||||||
AssetType: r.AssetType,
|
AssetType: r.AssetType,
|
||||||
|
MetaType: r.MetaType,
|
||||||
TaskID: r.TaskID,
|
TaskID: r.TaskID,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export interface RecentAssetItem {
|
|||||||
format: string
|
format: string
|
||||||
prompt: string
|
prompt: string
|
||||||
assetType: string
|
assetType: string
|
||||||
|
metaType: string
|
||||||
taskId: string
|
taskId: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import { getRecentAssets } from '../api/generate'
|
import { getRecentAssets } from '../api/generate'
|
||||||
import type { RecentAssetItem } from '../api/types'
|
import type { RecentAssetItem } from '../api/types'
|
||||||
|
|
||||||
@@ -9,9 +9,38 @@ const ASSET_TYPE_LABELS: Record<string, string> = {
|
|||||||
animation: '动画',
|
animation: '动画',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'spritesheet', label: '精灵表' },
|
||||||
|
{ key: 'frame', label: '帧' },
|
||||||
|
{ key: 'tileset', label: '场景瓦片' },
|
||||||
|
{ key: 'background', label: '背景' },
|
||||||
|
{ key: 'ui', label: 'UI' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function matchCategory(item: RecentAssetItem, cat: string): boolean {
|
||||||
|
switch (cat) {
|
||||||
|
case 'all':
|
||||||
|
return true
|
||||||
|
case 'spritesheet':
|
||||||
|
return item.metaType === 'spritesheet'
|
||||||
|
case 'frame':
|
||||||
|
return item.metaType === 'frame' || item.metaType === 'preview' || item.format === 'gif'
|
||||||
|
case 'tileset':
|
||||||
|
return item.assetType === 'sprite' && item.metaType === 'spritesheet'
|
||||||
|
case 'background':
|
||||||
|
return item.assetType === 'background'
|
||||||
|
case 'ui':
|
||||||
|
return item.assetType === 'ui'
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function AssetGallery() {
|
export default function AssetGallery() {
|
||||||
const [assets, setAssets] = useState<RecentAssetItem[]>([])
|
const [assets, setAssets] = useState<RecentAssetItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [activeCat, setActiveCat] = useState('all')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getRecentAssets()
|
getRecentAssets()
|
||||||
@@ -20,13 +49,45 @@ export default function AssetGallery() {
|
|||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (loading) return null
|
const filtered = useMemo(
|
||||||
|
() => assets.filter(item => matchCategory(item, activeCat)),
|
||||||
|
[assets, activeCat]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (loading) return null
|
||||||
if (assets.length === 0) return null
|
if (assets.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ marginBottom: 32 }}>
|
<section style={{ marginBottom: 32 }}>
|
||||||
<h2 style={{ fontSize: 18, marginBottom: 16 }}>最近生成</h2>
|
<h2 style={{ fontSize: 18, marginBottom: 12 }}>最近生成</h2>
|
||||||
|
|
||||||
|
{/* 分类标签 */}
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCat(cat.key)}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
padding: '4px 14px',
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${activeCat === cat.key ? 'var(--accent)' : 'var(--border)'}`,
|
||||||
|
background: activeCat === cat.key ? 'var(--accent-dim)' : 'transparent',
|
||||||
|
color: activeCat === cat.key ? 'var(--accent)' : 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: activeCat === cat.key ? 600 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 素材网格 */}
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>暂无此类素材</p>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -34,15 +95,11 @@ export default function AssetGallery() {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{assets.map((item, i) => (
|
{filtered.map((item, i) => (
|
||||||
<div
|
<div
|
||||||
key={`${item.taskId}-${i}`}
|
key={`${item.taskId}-${i}`}
|
||||||
className="card"
|
className="card"
|
||||||
style={{ padding: 8, cursor: 'pointer' }}
|
style={{ padding: 8 }}
|
||||||
onClick={() => {
|
|
||||||
// navigate to result page — need projectId for URL
|
|
||||||
// For now just open the image
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -95,6 +152,7 @@ export default function AssetGallery() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user