新增图片画布项目页

新增 /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

@@ -9,7 +9,8 @@ use serde_json::{Value, json};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, SpacetimeClientError,
};
@@ -53,6 +54,12 @@ pub struct EditorProjectLayoutSaveRequest {
layers: Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectRenameRequest {
title: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectResourceCreateRequest {
@@ -95,6 +102,18 @@ pub struct EditorProjectRecentResponse {
project: Option<EditorProjectPayload>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectListResponse {
projects: Vec<EditorProjectPayload>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectDeleteResponse {
deleted_project_id: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectResourceResponse {
@@ -179,6 +198,27 @@ pub async fn load_recent_editor_project(
))
}
pub async fn list_editor_projects(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, AppError> {
let owner_user_id = authenticated.claims().user_id().to_string();
let projects = state
.spacetime_client()
.list_editor_projects(owner_user_id)
.await
.map_err(map_editor_project_error)?
.into_iter()
.map(editor_project_payload_from_record)
.collect();
Ok(json_success_body(
Some(&request_context),
EditorProjectListResponse { projects },
))
}
pub async fn create_editor_project(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -261,6 +301,53 @@ pub async fn save_editor_project_layout(
))
}
pub async fn rename_editor_project(
State(state): State<AppState>,
Path(project_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<EditorProjectRenameRequest>,
) -> Result<Json<Value>, AppError> {
let project = state
.spacetime_client()
.rename_editor_project(EditorProjectRenameRecordInput {
project_id,
owner_user_id: authenticated.claims().user_id().to_string(),
title: payload.title,
updated_at_micros: current_utc_micros(),
})
.await
.map_err(map_editor_project_error)?;
Ok(json_success_body(
Some(&request_context),
EditorProjectResponse {
project: editor_project_payload_from_record(project),
},
))
}
pub async fn delete_editor_project(
State(state): State<AppState>,
Path(project_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, AppError> {
let deleted_project_id = state
.spacetime_client()
.delete_editor_project(EditorProjectDeleteRecordInput {
project_id,
owner_user_id: authenticated.claims().user_id().to_string(),
})
.await
.map_err(map_editor_project_error)?;
Ok(json_success_body(
Some(&request_context),
EditorProjectDeleteResponse { deleted_project_id },
))
}
pub async fn create_editor_project_resource(
State(state): State<AppState>,
Path(project_id): Path<String>,

View File

@@ -1,14 +1,14 @@
use axum::{
Router, middleware,
routing::{get, post},
routing::{get, patch, post},
};
use crate::{
auth::require_bearer_auth,
editor_project::{
create_editor_project, create_editor_project_resource, edit_editor_image,
generate_editor_image, get_editor_project, load_recent_editor_project,
save_editor_project_layout,
create_editor_project, create_editor_project_resource, delete_editor_project,
edit_editor_image, generate_editor_image, get_editor_project, list_editor_projects,
load_recent_editor_project, rename_editor_project, save_editor_project_layout,
},
state::AppState,
};
@@ -24,20 +24,30 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/api/editor/projects",
post(create_editor_project).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
get(list_editor_projects)
.post(create_editor_project)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/projects/{project_id}",
get(get_editor_project)
.patch(save_editor_project_layout)
.delete(delete_editor_project)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/projects/{project_id}/metadata",
patch(rename_editor_project).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/projects/{project_id}/resources",
post(create_editor_project_resource).route_layer(middleware::from_fn_with_state(