feat: 首页素材画廊增加分类筛选标签

- 后端 SQL 增加 json_extract 提取素材 metaType (frame/spritesheet/preview)
- 前端 AssetGallery 新增分类标签栏:全部/精灵表/帧/场景瓦片/背景/UI
- 按 metaType + assetType 组合过滤展示
This commit is contained in:
2026-05-25 21:07:15 +08:00
parent b88f9aaf03
commit 1a78744766
3 changed files with 131 additions and 68 deletions
+5 -1
View File
@@ -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,
} }
+1
View File
@@ -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
} }
+67 -9
View File
@@ -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>
) )
} }