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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user