diff --git a/README.md b/README.md index 6121f3e..bf37293 100755 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ npm run dev ## 架构 +![全局架构](assets/global-architecture.png) + ### 生成管线 基于 Eino `compose.Graph` 的四阶段管线,支持质检重试与降级输出: @@ -161,6 +163,8 @@ graph TD C --> D["三段式规范提示词
【主题】【风格】【技术】"] ``` +![后端架构](assets/backend-architecture.png) + ### 预设风格键 | 分类 | 键名 | 可选值示例 | @@ -172,6 +176,10 @@ graph TD | 光照 | `lighting` | bright, dim, dramatic, ambient, neon | | 情绪 | `mood` | cheerful, dark, mysterious, epic, calm | +![前端架构](assets/frontend-architecture.png) + +详细架构说明见 [docs/_index.md](docs/_index.md)。 + ## API 接口统一前缀 `/api/v1/`,统一响应格式: diff --git a/assets/backend-architecture.png b/assets/backend-architecture.png new file mode 100644 index 0000000..dccff6f Binary files /dev/null and b/assets/backend-architecture.png differ diff --git a/assets/frontend-architecture.png b/assets/frontend-architecture.png new file mode 100644 index 0000000..9a02526 Binary files /dev/null and b/assets/frontend-architecture.png differ diff --git a/assets/global-architecture.png b/assets/global-architecture.png new file mode 100644 index 0000000..23aef82 Binary files /dev/null and b/assets/global-architecture.png differ diff --git a/backend/internal/handler/generate.go b/backend/internal/handler/generate.go index 0aa9bce..a47eacd 100755 --- a/backend/internal/handler/generate.go +++ b/backend/internal/handler/generate.go @@ -308,14 +308,18 @@ func getTaskDBID(ctx context.Context, taskID string) uint { // 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"` + Key string `json:"key"` + URL string `json:"url"` + Format string `json:"format"` + Prompt string `json:"prompt"` + AssetType string `json:"assetType"` + MetaType string `json:"metaType"` + TaskID string `json:"taskId"` + ProjectID string `json:"projectId"` + ProjectName string `json:"projectName"` + Width int `json:"width"` + Height int `json:"height"` + CreatedAt string `json:"createdAt"` } // GetRecentAssets 获取最近生成的素材列表(已完成任务的全部素材)。 @@ -323,23 +327,31 @@ 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 + Key string + URL string + Format string + Prompt string + AssetType string + MetaType string + TaskID string + ProjectID uint + ProjectName string + Width int + Height int + 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 + t.external_id as task_id, t.project_id, + p.name as project_name, + CAST(COALESCE(json_extract(t.metadata, '$.frameWidth'), '0') AS INTEGER) as width, + CAST(COALESCE(json_extract(t.metadata, '$.frameHeight'), '0') AS INTEGER) as height, + t.created_at as created_at FROM assets a INNER JOIN tasks t ON a.task_id = t.id + INNER JOIN projects p ON t.project_id = p.id WHERE t.status = 'completed' ORDER BY a.created_at DESC LIMIT ?`, limit). @@ -348,14 +360,18 @@ func GetRecentAssets(c *gin.Context) { 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, + Key: r.Key, + URL: storageSvc.GetSignedURL(r.Key), + Format: r.Format, + Prompt: r.Prompt, + AssetType: r.AssetType, + MetaType: r.MetaType, + TaskID: r.TaskID, + ProjectID: fmt.Sprintf("%d", r.ProjectID), + ProjectName: r.ProjectName, + Width: r.Width, + Height: r.Height, + CreatedAt: r.CreatedAt, } } diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index d4ee998..3bf0c14 100755 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -107,6 +107,10 @@ export interface RecentAssetItem { assetType: string metaType: string taskId: string + projectId: string + projectName: string + width: number + height: number createdAt: string } diff --git a/frontend/src/components/AssetGallery.tsx b/frontend/src/components/AssetGallery.tsx index 621a489..695e1ec 100644 --- a/frontend/src/components/AssetGallery.tsx +++ b/frontend/src/components/AssetGallery.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom' import { getRecentAssets } from '../api/generate' import type { RecentAssetItem } from '../api/types' @@ -37,10 +38,118 @@ function matchCategory(item: RecentAssetItem, cat: string): boolean { } } +function DetailModal({ item, onClose }: { item: RecentAssetItem; onClose: () => void }) { + const downloadUrl = `/api/v1/assets/download?key=${encodeURIComponent(item.key)}` + + return createPortal( +
+
+
e.stopPropagation()} + > + + +
+ {item.prompt} +
+ + + + + + + 0 ? `${item.width} x ${item.height}` : '—'} /> + + +
+ +
+ + 下载 + + +
+
+
, + document.body + ) +} + +function Row({ label, value }: { label: string; value: string }) { + return ( + + + {label} + + {value} + + ) +} + export default function AssetGallery() { const [assets, setAssets] = useState([]) const [loading, setLoading] = useState(true) const [activeCat, setActiveCat] = useState('all') + const [detailItem, setDetailItem] = useState(null) useEffect(() => { getRecentAssets() @@ -99,7 +208,8 @@ export default function AssetGallery() {
setDetailItem(item)} >
)} + + {/* 详情弹窗 */} + {detailItem && ( + setDetailItem(null)} /> + )} ) }