diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index 5d06703..71fea0a 100755
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -78,6 +78,7 @@ func main() {
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
+ v1Auth.GET("/assets", handler.GetRecentAssets) // 首页最近素材
// 图片编辑
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
}
diff --git a/backend/internal/handler/generate.go b/backend/internal/handler/generate.go
index db6c8c0..0aa9bce 100755
--- a/backend/internal/handler/generate.go
+++ b/backend/internal/handler/generate.go
@@ -306,6 +306,62 @@ func getTaskDBID(ctx context.Context, taskID string) uint {
return task.ID
}
+// RecentAssetItem 首页素材展示项。
+type RecentAssetItem struct {
+ Key string `json:"key"`
+ URL string `json:"url"`
+ Format string `json:"format"`
+ Prompt string `json:"prompt"`
+ AssetType string `json:"assetType"`
+ MetaType string `json:"metaType"` // frame / spritesheet / preview
+ TaskID string `json:"taskId"`
+ CreatedAt string `json:"createdAt"`
+}
+
+// GetRecentAssets 获取最近生成的素材列表(已完成任务的全部素材)。
+func GetRecentAssets(c *gin.Context) {
+ limit := 60
+
+ var dbRows []struct {
+ Key string
+ URL string
+ Format string
+ Prompt string
+ AssetType string
+ MetaType string
+ TaskID string
+ ProjectID uint
+ CreatedAt string
+ }
+
+ db.GetDB().WithContext(c.Request.Context()).
+ 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
+ FROM assets a
+ INNER JOIN tasks t ON a.task_id = t.id
+ WHERE t.status = 'completed'
+ ORDER BY a.created_at DESC
+ LIMIT ?`, limit).
+ Scan(&dbRows)
+
+ result := make([]RecentAssetItem, len(dbRows))
+ for i, r := range dbRows {
+ result[i] = RecentAssetItem{
+ Key: r.Key,
+ URL: storageSvc.GetSignedURL(r.Key),
+ Format: r.Format,
+ Prompt: r.Prompt,
+ AssetType: r.AssetType,
+ MetaType: r.MetaType,
+ TaskID: r.TaskID,
+ CreatedAt: r.CreatedAt,
+ }
+ }
+
+ c.JSON(http.StatusOK, model.OK(result))
+}
+
// toTaskResponse 转换任务响应格式。
func toTaskResponse(task *model.Task) model.TaskResponse {
return model.TaskResponse{
diff --git a/frontend/index.html b/frontend/index.html
index 50ab2b0..78629cd 100755
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,6 +3,7 @@
+
gen2d
diff --git a/frontend/public/logo.png b/frontend/public/logo.png
new file mode 100644
index 0000000..a0c17bb
Binary files /dev/null and b/frontend/public/logo.png differ
diff --git a/frontend/src/api/generate.ts b/frontend/src/api/generate.ts
index 65e2e47..cf369e0 100755
--- a/frontend/src/api/generate.ts
+++ b/frontend/src/api/generate.ts
@@ -1,4 +1,9 @@
import { post, get } from './client'
+import type { RecentAssetItem } from './types'
+
+export async function getRecentAssets(): Promise {
+ return get('/api/v1/assets')
+}
import type {
Asset,
AssetsResponse,
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index daad4c4..d4ee998 100755
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -98,6 +98,18 @@ export interface Asset {
index: number
}
+// 首页最近素材
+export interface RecentAssetItem {
+ key: string
+ url: string
+ format: string
+ prompt: string
+ assetType: string
+ metaType: string
+ taskId: string
+ createdAt: string
+}
+
// 生成请求 — 对应 POST /api/v1/generate
export interface GenerateRequest {
projectId: string
diff --git a/frontend/src/components/AssetGallery.tsx b/frontend/src/components/AssetGallery.tsx
new file mode 100644
index 0000000..621a489
--- /dev/null
+++ b/frontend/src/components/AssetGallery.tsx
@@ -0,0 +1,158 @@
+import { useEffect, useState, useMemo } from 'react'
+import { getRecentAssets } from '../api/generate'
+import type { RecentAssetItem } from '../api/types'
+
+const ASSET_TYPE_LABELS: Record = {
+ sprite: '精灵',
+ background: '背景',
+ ui: 'UI',
+ 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() {
+ const [assets, setAssets] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [activeCat, setActiveCat] = useState('all')
+
+ useEffect(() => {
+ getRecentAssets()
+ .then(setAssets)
+ .catch(() => {})
+ .finally(() => setLoading(false))
+ }, [])
+
+ const filtered = useMemo(
+ () => assets.filter(item => matchCategory(item, activeCat)),
+ [assets, activeCat]
+ )
+
+ if (loading) return null
+ if (assets.length === 0) return null
+
+ return (
+
+ 最近生成
+
+ {/* 分类标签 */}
+
+ {CATEGORIES.map(cat => (
+
+ ))}
+
+
+ {/* 素材网格 */}
+ {filtered.length === 0 ? (
+ 暂无此类素材
+ ) : (
+
+ {filtered.map((item, i) => (
+
+
+

+
+
+
+ {ASSET_TYPE_LABELS[item.assetType] || item.assetType}
+
+
+
+ {item.prompt}
+
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index e459ccd..9e67c4c 100755
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -27,13 +27,16 @@ export default function Layout() {
- gen2d
+
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index 7db0daf..4c647d2 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useProjectListStore } from '../stores/projectList'
import { useToastStore } from '../stores/toast'
import ProjectCard from '../components/ProjectCard'
+import AssetGallery from '../components/AssetGallery'
import CreateProjectModal from '../components/CreateProjectModal'
import EmptyState from '../components/EmptyState'
import Skeleton from '../components/Skeleton'
@@ -39,6 +40,8 @@ export default function HomePage() {
return (
+
+
我的工程