调整图片画布路由和画布数据模型

将图片画布入口改为 /editor/canvas

新增 editor_canvas 表并关联 editor_project 默认画布

更新 project API 响应中的 canvas 快照兼容层

统一图片画布侧栏列表项和图标按钮组件

同步前端测试、SpacetimeDB bindings、技术文档和 TRACKING 记录
This commit is contained in:
2026-06-13 22:09:45 +08:00
parent a1b9ac8544
commit 242860e2d3
21 changed files with 1649 additions and 295 deletions

View File

@@ -1,6 +1,7 @@
use crate::*;
const EDITOR_PROJECT_DEFAULT_TITLE: &str = "未命名画布";
const EDITOR_CANVAS_DEFAULT_TITLE: &str = "默认画布";
const EDITOR_PROJECT_MAX_TITLE_CHARS: usize = 80;
const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 256 * 1024;
const EDITOR_PROJECT_SOURCE_TYPES: [&str; 3] = ["uploaded", "generated", "mock_generated"];
@@ -22,6 +23,25 @@ pub struct EditorProject {
updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = editor_canvas,
index(accessor = by_editor_canvas_project_id, btree(columns = [project_id])),
index(accessor = by_editor_canvas_owner_user_id, btree(columns = [owner_user_id]))
)]
pub struct EditorCanvas {
#[primary_key]
canvas_id: String,
project_id: String,
owner_user_id: String,
title: String,
viewport_x: f64,
viewport_y: f64,
viewport_scale: f64,
layers_json: String,
created_at: Timestamp,
updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = editor_project_resource,
index(accessor = by_editor_project_resource_project_id, btree(columns = [project_id])),
@@ -123,13 +143,23 @@ pub struct EditorProjectResourceSnapshot {
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorCanvasSnapshot {
pub canvas_id: String,
pub project_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorProjectSnapshot {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub canvas: EditorCanvasSnapshot,
pub resources: Vec<EditorProjectResourceSnapshot>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
@@ -233,7 +263,7 @@ fn create_editor_project(
ctx.db.editor_project().insert(EditorProject {
project_id: project_id.clone(),
owner_user_id,
owner_user_id: owner_user_id.clone(),
title,
viewport_x: 0.0,
viewport_y: 0.0,
@@ -242,6 +272,13 @@ fn create_editor_project(
created_at: now,
updated_at: now,
});
ensure_default_canvas(
ctx,
project_id.as_str(),
owner_user_id.as_str(),
None,
now,
)?;
build_project_snapshot(ctx, project_id.as_str())
}
@@ -290,7 +327,28 @@ fn save_editor_project_layout(
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
let layers_json = normalize_layout_json(input.layers_json)?;
let project = require_owned_project(ctx, project_id.as_str(), owner_user_id.as_str())?;
let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
let canvas = ensure_default_canvas(
ctx,
project.project_id.as_str(),
project.owner_user_id.as_str(),
Some(&project),
now,
)?;
ctx.db.editor_canvas().canvas_id().delete(&canvas.canvas_id);
ctx.db.editor_canvas().insert(EditorCanvas {
canvas_id: canvas.canvas_id,
project_id: project.project_id.clone(),
owner_user_id: project.owner_user_id.clone(),
title: canvas.title,
viewport_x: input.viewport.x,
viewport_y: input.viewport.y,
viewport_scale: input.viewport.scale.clamp(0.01, 8.0),
layers_json: layers_json.clone(),
created_at: canvas.created_at,
updated_at: now,
});
ctx.db.editor_project().project_id().delete(&project_id);
ctx.db.editor_project().insert(EditorProject {
project_id: project.project_id.clone(),
@@ -301,7 +359,7 @@ fn save_editor_project_layout(
viewport_scale: input.viewport.scale.clamp(0.01, 8.0),
layers_json,
created_at: project.created_at,
updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros),
updated_at: now,
});
build_project_snapshot(ctx, project_id.as_str())
@@ -384,6 +442,13 @@ fn build_project_snapshot(
.project_id()
.find(&project_key)
.ok_or_else(|| "图片画布工程不存在".to_string())?;
let canvas = ensure_default_canvas(
ctx,
project.project_id.as_str(),
project.owner_user_id.as_str(),
Some(&project),
project.created_at,
)?;
let mut resources = ctx
.db
.editor_project_resource()
@@ -401,18 +466,77 @@ fn build_project_snapshot(
project_id: project.project_id,
owner_user_id: project.owner_user_id,
title: project.title,
viewport: EditorProjectViewportSnapshot {
x: project.viewport_x,
y: project.viewport_y,
scale: project.viewport_scale,
},
layers_json: project.layers_json,
canvas: canvas_snapshot_from_row(canvas),
resources,
created_at_micros: project.created_at.to_micros_since_unix_epoch(),
updated_at_micros: project.updated_at.to_micros_since_unix_epoch(),
})
}
fn ensure_default_canvas(
ctx: &ReducerContext,
project_id: &str,
owner_user_id: &str,
legacy_project: Option<&EditorProject>,
now: Timestamp,
) -> Result<EditorCanvas, String> {
let canvas_id = default_canvas_id(project_id);
if let Some(canvas) = ctx.db.editor_canvas().canvas_id().find(&canvas_id) {
return Ok(canvas);
}
let legacy_view = legacy_project
.map(|project| {
(
project.viewport_x,
project.viewport_y,
project.viewport_scale,
project.layers_json.clone(),
project.created_at,
project.updated_at,
)
})
.unwrap_or((0.0, 0.0, 1.0, "[]".to_string(), now, now));
let canvas = EditorCanvas {
canvas_id: canvas_id.clone(),
project_id: project_id.to_string(),
owner_user_id: owner_user_id.to_string(),
title: EDITOR_CANVAS_DEFAULT_TITLE.to_string(),
viewport_x: legacy_view.0,
viewport_y: legacy_view.1,
viewport_scale: legacy_view.2,
layers_json: legacy_view.3,
created_at: legacy_view.4,
updated_at: legacy_view.5,
};
ctx.db.editor_canvas().insert(canvas);
ctx.db
.editor_canvas()
.canvas_id()
.find(&canvas_id)
.ok_or_else(|| "图片画布创建失败".to_string())
}
fn default_canvas_id(project_id: &str) -> String {
format!("{project_id}:canvas:default")
}
fn canvas_snapshot_from_row(row: EditorCanvas) -> EditorCanvasSnapshot {
EditorCanvasSnapshot {
canvas_id: row.canvas_id,
project_id: row.project_id,
title: row.title,
viewport: EditorProjectViewportSnapshot {
x: row.viewport_x,
y: row.viewport_y,
scale: row.viewport_scale,
},
layers_json: row.layers_json,
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}
fn require_owned_project(
ctx: &ReducerContext,
project_id: &str,

View File

@@ -230,6 +230,7 @@ macro_rules! migration_tables {
asset_entity_binding,
asset_event,
editor_project,
editor_canvas,
editor_project_resource,
puzzle_agent_session,
puzzle_background_compile_task,