新增图片画布项目页

新增 /project 项目页和我的页项目入口

补齐图片画布工程列表、重命名和删除 API

支持 /editor/canvas 按 projectid 加载指定工程

更新图片画布文档、TRACKING 和对应测试
This commit is contained in:
2026-06-14 00:11:36 +08:00
parent b2122481ff
commit 85834a423d
32 changed files with 1800 additions and 20 deletions

View File

@@ -94,6 +94,25 @@ pub struct EditorProjectGetRecentInput {
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorProjectListInput {
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorProjectRenameInput {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorProjectDeleteInput {
pub project_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorProjectLayoutSaveInput {
pub project_id: String,
@@ -172,6 +191,20 @@ pub struct EditorProjectProcedureResult {
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorProjectListProcedureResult {
pub ok: bool,
pub projects: Vec<EditorProjectSnapshot>,
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorProjectDeleteProcedureResult {
pub ok: bool,
pub deleted_project_id: Option<String>,
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorProjectResourceProcedureResult {
pub ok: bool,
@@ -201,6 +234,25 @@ pub fn get_recent_editor_project_and_return(
}
}
#[spacetimedb::procedure]
pub fn list_editor_projects_and_return(
ctx: &mut ProcedureContext,
input: EditorProjectListInput,
) -> EditorProjectListProcedureResult {
match ctx.try_with_tx(|tx| list_editor_projects(tx, input.clone())) {
Ok(projects) => EditorProjectListProcedureResult {
ok: true,
projects,
error_message: None,
},
Err(message) => EditorProjectListProcedureResult {
ok: false,
projects: Vec::new(),
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn get_editor_project_and_return(
ctx: &mut ProcedureContext,
@@ -223,6 +275,36 @@ pub fn save_editor_project_layout_and_return(
}
}
#[spacetimedb::procedure]
pub fn rename_editor_project_and_return(
ctx: &mut ProcedureContext,
input: EditorProjectRenameInput,
) -> EditorProjectProcedureResult {
match ctx.try_with_tx(|tx| rename_editor_project(tx, input.clone())) {
Ok(project) => editor_project_ok(Some(project)),
Err(message) => editor_project_error(message),
}
}
#[spacetimedb::procedure]
pub fn delete_editor_project_and_return(
ctx: &mut ProcedureContext,
input: EditorProjectDeleteInput,
) -> EditorProjectDeleteProcedureResult {
match ctx.try_with_tx(|tx| delete_editor_project(tx, input.clone())) {
Ok(project_id) => EditorProjectDeleteProcedureResult {
ok: true,
deleted_project_id: Some(project_id),
error_message: None,
},
Err(message) => EditorProjectDeleteProcedureResult {
ok: false,
deleted_project_id: None,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn create_editor_project_resource_and_return(
ctx: &mut ProcedureContext,
@@ -309,6 +391,32 @@ fn get_recent_editor_project(
.transpose()
}
fn list_editor_projects(
ctx: &ReducerContext,
input: EditorProjectListInput,
) -> Result<Vec<EditorProjectSnapshot>, String> {
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
let mut projects = ctx
.db
.editor_project()
.by_editor_project_owner_user_id()
.filter(&owner_user_id)
.collect::<Vec<_>>();
projects.sort_by(|left, right| {
right
.updated_at
.to_micros_since_unix_epoch()
.cmp(&left.updated_at.to_micros_since_unix_epoch())
.then_with(|| right.project_id.cmp(&left.project_id))
});
projects
.into_iter()
.map(|project| build_project_snapshot(ctx, project.project_id.as_str()))
.collect()
}
fn get_editor_project(
ctx: &ReducerContext,
input: EditorProjectGetInput,
@@ -319,6 +427,68 @@ fn get_editor_project(
build_project_snapshot(ctx, project.project_id.as_str())
}
fn rename_editor_project(
ctx: &ReducerContext,
input: EditorProjectRenameInput,
) -> Result<EditorProjectSnapshot, String> {
let project_id = normalize_required(&input.project_id, "editor_project.project_id")?;
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
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);
ctx.db.editor_project().project_id().delete(&project_id);
ctx.db.editor_project().insert(EditorProject {
project_id: project.project_id.clone(),
owner_user_id: project.owner_user_id,
title: normalize_title(&input.title),
viewport_x: project.viewport_x,
viewport_y: project.viewport_y,
viewport_scale: project.viewport_scale,
layers_json: project.layers_json,
created_at: project.created_at,
updated_at: now,
});
build_project_snapshot(ctx, project_id.as_str())
}
fn delete_editor_project(
ctx: &ReducerContext,
input: EditorProjectDeleteInput,
) -> Result<String, String> {
let project_id = normalize_required(&input.project_id, "editor_project.project_id")?;
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
require_owned_project(ctx, project_id.as_str(), owner_user_id.as_str())?;
let project_key = project_id.clone();
let canvas_ids = ctx
.db
.editor_canvas()
.by_editor_canvas_project_id()
.filter(&project_key)
.map(|canvas| canvas.canvas_id)
.collect::<Vec<_>>();
let resource_ids = ctx
.db
.editor_project_resource()
.by_editor_project_resource_project_id()
.filter(&project_key)
.map(|resource| resource.resource_id)
.collect::<Vec<_>>();
for canvas_id in canvas_ids {
ctx.db.editor_canvas().canvas_id().delete(&canvas_id);
}
for resource_id in resource_ids {
ctx.db
.editor_project_resource()
.resource_id()
.delete(&resource_id);
}
ctx.db.editor_project().project_id().delete(&project_id);
Ok(project_id)
}
fn save_editor_project_layout(
ctx: &ReducerContext,
input: EditorProjectLayoutSaveInput,