diff --git a/backend/internal/handler/project.go b/backend/internal/handler/project.go index 5eaafa9db..7127781f8 100644 --- a/backend/internal/handler/project.go +++ b/backend/internal/handler/project.go @@ -1,7 +1,9 @@ package handler import ( + "fmt" "net/http" + "strconv" "infinite-canvas/backend/internal/service" @@ -29,12 +31,33 @@ func RegisterProjectRoutes(r *gin.RouterGroup, svc *service.Service) { failService(c, err) return } - projects, err := svc.ListProjects(user.ID) + pageParam, hasPage := c.GetQuery("page") + pageSizeParam, hasPageSize := c.GetQuery("page_size") + if !hasPage && !hasPageSize { + projects, err := svc.ListProjects(user.ID) + if err != nil { + failService(c, err) + return + } + ok(c, gin.H{"projects": projects}) + return + } + page, err := parsePositiveQueryInt(pageParam, 1) + if err != nil { + fail(c, http.StatusBadRequest, err) + return + } + pageSize, err := parsePositiveQueryInt(pageSizeParam, 50) + if err != nil { + fail(c, http.StatusBadRequest, err) + return + } + projects, err := svc.ListProjectsPage(user.ID, page, pageSize) if err != nil { failService(c, err) return } - ok(c, gin.H{"projects": projects}) + ok(c, projects) }) r.POST("/projects", func(c *gin.Context) { user, err := currentUser(c, svc) @@ -713,3 +736,14 @@ func RegisterProjectRoutes(r *gin.RouterGroup, svc *service.Service) { ok(c, gin.H{"asset": asset}) }) } + +func parsePositiveQueryInt(value string, fallback int) (int, error) { + if value == "" { + return fallback, nil + } + parsed, err := strconv.Atoi(value) + if err != nil || parsed < 1 { + return 0, fmt.Errorf("query parameter must be a positive integer") + } + return parsed, nil +} diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go index 7a94ab450..e0e9f9cb0 100644 --- a/backend/internal/repository/repository.go +++ b/backend/internal/repository/repository.go @@ -982,6 +982,19 @@ func (r *Repository) Projects(userID string) ([]model.Project, error) { return projects, err } +func (r *Repository) ProjectsPage(userID string, page int, pageSize int) ([]model.Project, int64, error) { + var projects []model.Project + var total int64 + query := r.db.Model(&model.Project{}).Where("user_id = ?", userID) + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + if err := query.Order("updated_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&projects).Error; err != nil { + return nil, 0, err + } + return projects, total, nil +} + func (r *Repository) ProjectForUser(userID string, id string) (*model.Project, error) { var project model.Project if err := r.db.First(&project, "id = ? AND user_id = ?", id, userID).Error; err != nil { diff --git a/backend/internal/service/project.go b/backend/internal/service/project.go index 81f307c14..b4201f893 100644 --- a/backend/internal/service/project.go +++ b/backend/internal/service/project.go @@ -69,6 +69,14 @@ type ProjectSummary struct { CompletedUnitCount int `json:"completedUnitCount"` } +type ProjectListPage struct { + Projects []ProjectSummary `json:"projects"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + Total int64 `json:"total"` + HasMore bool `json:"hasMore"` +} + type ProjectDetail struct { Project model.Project `json:"project"` Units []model.ProjectUnit `json:"units"` @@ -87,6 +95,22 @@ func (s *Service) ListProjects(userID string) ([]ProjectSummary, error) { if err != nil { return nil, err } + return s.summarizeProjects(userID, projects) +} + +func (s *Service) ListProjectsPage(userID string, page int, pageSize int) (ProjectListPage, error) { + projects, total, err := s.repo.ProjectsPage(userID, page, pageSize) + if err != nil { + return ProjectListPage{}, err + } + result, err := s.summarizeProjects(userID, projects) + if err != nil { + return ProjectListPage{}, err + } + return ProjectListPage{Projects: result, Page: page, PageSize: pageSize, Total: total, HasMore: int64(page*pageSize) < total}, nil +} + +func (s *Service) summarizeProjects(userID string, projects []model.Project) ([]ProjectSummary, error) { result := make([]ProjectSummary, 0, len(projects)) for _, project := range projects { units, unitsErr := s.repo.ProjectUnitSummaries(project.ID) diff --git a/web/src/components/canvas/canvas-folder-card.tsx b/web/src/components/canvas/canvas-folder-card.tsx index e8d9b55a0..328c08a4c 100644 --- a/web/src/components/canvas/canvas-folder-card.tsx +++ b/web/src/components/canvas/canvas-folder-card.tsx @@ -2,7 +2,7 @@ import { Dropdown, Input } from "antd"; import { Download, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; import type { KeyboardEvent } from "react"; -import { ProjectPreview, formatProjectTime } from "@/components/canvas/canvas-project-card"; +import { ProjectPreview } from "@/components/canvas/canvas-project-card"; import { exportCanvasProjects } from "@/lib/canvas/canvas-export"; import { useCanvasStore, type CanvasProject } from "@/stores/canvas/use-canvas-store"; import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store"; @@ -67,17 +67,13 @@ export function CanvasFolderCard({ project, projectName, onClick }: CanvasFolder )}
- {projectName || "自由画布"} - + {projectName ? `所属项目:${projectName}` : "自由画布"} + {project.nodes.length} 节点 - - +
+
+ 创建时间 + 最后更新
@@ -119,7 +115,7 @@ export function CanvasFolderCard({ project, projectName, onClick }: CanvasFolder ], }} > - @@ -127,3 +123,16 @@ export function CanvasFolderCard({ project, projectName, onClick }: CanvasFolder ); } + +function formatCanvasDate(value: string) { + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) + ? new Date(timestamp).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) + : "时间不可用"; +} diff --git a/web/src/lib/plugins/builtin/eagle.ts b/web/src/lib/plugins/builtin/eagle.ts index 54eec1c49..5045e1521 100644 --- a/web/src/lib/plugins/builtin/eagle.ts +++ b/web/src/lib/plugins/builtin/eagle.ts @@ -15,6 +15,8 @@ export const eagleAssetPlugin: RegisteredPlugin = { id: EAGLE_PLUGIN_ID, name: "Eagle 素材库", version: "0.3.0", + publishedAt: "2026-08-21", + updatedAt: "2026-08-22", apiVersion: "1", category: "asset-source", description: "把 Eagle 作为影策的外部素材来源,直接浏览原始文件夹并读写 Eagle 文件。", diff --git a/web/src/lib/plugins/plugin-types.ts b/web/src/lib/plugins/plugin-types.ts index 193efe5e3..1e497008c 100644 --- a/web/src/lib/plugins/plugin-types.ts +++ b/web/src/lib/plugins/plugin-types.ts @@ -18,6 +18,8 @@ export type PluginManifest = { id: string; name: string; version: string; + publishedAt?: string; + updatedAt?: string; apiVersion: string; category: PluginCategory; description: string; diff --git a/web/src/pages/admin/components/admin-shell.tsx b/web/src/pages/admin/components/admin-shell.tsx index 47c4b5242..dbccd9134 100644 --- a/web/src/pages/admin/components/admin-shell.tsx +++ b/web/src/pages/admin/components/admin-shell.tsx @@ -92,7 +92,7 @@ export function AdminShell() { return (
-