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
+3 -1
View File
@@ -35,6 +35,7 @@ const ImagesView = () => import('./views/ImagesView.vue')
const LogsView = () => import('./views/LogsView.vue')
const ConfigView = () => import('./views/ConfigView.vue')
const ShowcaseView = () => import('./views/ShowcaseView.vue')
const CanvasView = () => import('./views/CanvasView.vue')
const routes = [
{
@@ -43,6 +44,7 @@ const routes = [
children: [
{ path: '', component: HomeView, meta: { label: '首页' } },
{ path: 'user', component: PlaygroundView, meta: { label: '画图' } },
{ path: 'canvas', component: CanvasView, meta: { label: '无限画布' } },
{ path: 'logs', component: UserLogsView, meta: { label: '图片' } },
{ path: 'mylogs', component: UserLogsTableView, meta: { label: '日志' } },
{ path: 'invite', component: InviteView, meta: { label: '邀请' } },
@@ -95,7 +97,7 @@ const router = createRouter({
// Pages that require a login. The home page (/) stays public; everything a
// signed-in user touches (画图/记录/设置) and the whole admin area is gated.
const PROTECTED = ['/user', '/logs', '/invite', '/settings', '/orders']
const PROTECTED = ['/user', '/canvas', '/logs', '/invite', '/settings', '/orders']
function isProtected(path) {
return path.startsWith('/admin') || PROTECTED.includes(path)
}