feat: 无限画布 — infinite canvas with generation integration

Backend:
- canvases table + owner-scoped CRUD under /admin/api/canvas
  (list/create/get/save/delete, 50 docs/user, 4MiB cap, JSON validation)

Frontend:
- /canvas full-screen workspace: DOM+CSS-transform infinite pan/zoom
  (GPU-composited world layer, viewport culling, rAF-batched pointer moves)
- nodes: image (thumb display, natural-size correction), note
  (uncontrolled contenteditable), generation placeholder with edges
- generate on canvas: prompt from panel or selected notes, selected image
  nodes as references, results auto-placed + bezier edges to sources
- multi-canvas management, debounced autosave + keepalive flush,
  snapshot undo/redo (60 steps), marquee/keyboard interactions
This commit is contained in:
dokploy-bot
2026-09-19 22:38:41 +08:00
parent 42208c0753
commit 571ee2c801
12 changed files with 2078 additions and 1 deletions
+84
View File
@@ -0,0 +1,84 @@
package repo
import (
"context"
"gorm.io/gorm"
"backend/internal/model"
)
// CanvasRepository persists infinite-canvas documents. All queries are
// userID-scoped — a canvas is only ever read or written by its owner.
type CanvasRepository struct {
db *gorm.DB
}
func NewCanvasRepository(db *gorm.DB) *CanvasRepository {
return &CanvasRepository{db: db}
}
// List returns the user's canvases newest-first, without the (potentially
// large) Data blob — the picker only needs id/title/updatedAt.
func (r *CanvasRepository) List(ctx context.Context, userID string) ([]model.Canvas, error) {
var items []model.Canvas
err := r.db.WithContext(ctx).
Select("id", "user_id", "title", "updated_at").
Where("user_id = ?", userID).
Order("updated_at DESC").
Find(&items).Error
return items, err
}
func (r *CanvasRepository) Get(ctx context.Context, userID, id string) (*model.Canvas, error) {
var c model.Canvas
if err := r.db.WithContext(ctx).
Where("id = ? AND user_id = ?", id, userID).
First(&c).Error; err != nil {
return nil, err
}
return &c, nil
}
func (r *CanvasRepository) Create(ctx context.Context, c *model.Canvas) error {
return r.db.WithContext(ctx).Create(c).Error
}
// Update replaces title and/or the document blob. A no-match update reports
// ErrRecordNotFound (gorm's Updates is silent about missing rows).
func (r *CanvasRepository) Update(ctx context.Context, userID, id string, fields map[string]any) error {
res := r.db.WithContext(ctx).
Model(&model.Canvas{}).
Where("id = ? AND user_id = ?", id, userID).
Updates(fields)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *CanvasRepository) Delete(ctx context.Context, userID, id string) error {
res := r.db.WithContext(ctx).
Where("id = ? AND user_id = ?", id, userID).
Delete(&model.Canvas{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// CountByUser backs the per-user document cap.
func (r *CanvasRepository) CountByUser(ctx context.Context, userID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).
Model(&model.Canvas{}).
Where("user_id = ?", userID).
Count(&n).Error
return n, err
}