完善图片画布素材库持久化

新增账号级素材文件夹和素材表,并接入 SpacetimeDB procedure、spacetime-client facade 与 api-server BFF。

编辑器素材栏支持文件夹新建、折叠、重命名、删除、多文件上传、拖拽定向上传、框选和批量删除。

画布支持拖拽上传落点创建图层、图层打组、小地图拖拽、普通滚轮纵向滚动和 Ctrl 滚轮缩放。

更新图片画布技术方案、后端数据契约、TRACKING 和团队决策记录。
This commit is contained in:
2026-06-14 14:29:13 +08:00
parent 6bc2f11d04
commit a6025365f7
43 changed files with 4459 additions and 125 deletions

View File

@@ -51,6 +51,43 @@ pub struct EditorProjectResourceRecord {
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorAssetFolderRecord {
pub folder_id: String,
pub label: String,
pub sort_order: u32,
pub collapsed: bool,
pub system_default: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorAssetRecord {
pub asset_id: String,
pub folder_id: String,
pub label: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorAssetLibraryRecord {
pub folders: Vec<EditorAssetFolderRecord>,
pub assets: Vec<EditorAssetRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EditorProjectCreateRecordInput {
pub project_id: String,
@@ -108,6 +145,66 @@ pub struct EditorProjectResourceCreateRecordInput {
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetFolderCreateRecordInput {
pub folder_id: String,
pub owner_user_id: String,
pub label: String,
pub sort_order: u32,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetFolderUpdateRecordInput {
pub folder_id: String,
pub owner_user_id: String,
pub label: Option<String>,
pub collapsed: Option<bool>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetFolderDeleteRecordInput {
pub folder_id: String,
pub owner_user_id: String,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetCreateRecordInput {
pub asset_id: String,
pub owner_user_id: String,
pub folder_id: String,
pub label: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetUpdateRecordInput {
pub asset_id: String,
pub owner_user_id: String,
pub label: Option<String>,
pub folder_id: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetDeleteRecordInput {
pub asset_id: String,
pub owner_user_id: String,
}
impl From<EditorProjectCreateRecordInput> for crate::module_bindings::EditorProjectCreateInput {
fn from(input: EditorProjectCreateRecordInput) -> Self {
Self {
@@ -191,6 +288,90 @@ impl From<EditorProjectResourceCreateRecordInput>
}
}
impl From<EditorAssetFolderCreateRecordInput>
for crate::module_bindings::EditorAssetFolderCreateInput
{
fn from(input: EditorAssetFolderCreateRecordInput) -> Self {
Self {
folder_id: input.folder_id,
owner_user_id: input.owner_user_id,
label: input.label,
sort_order: input.sort_order,
now_micros: input.now_micros,
}
}
}
impl From<EditorAssetFolderUpdateRecordInput>
for crate::module_bindings::EditorAssetFolderUpdateInput
{
fn from(input: EditorAssetFolderUpdateRecordInput) -> Self {
Self {
folder_id: input.folder_id,
owner_user_id: input.owner_user_id,
label: input.label,
collapsed: input.collapsed,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<EditorAssetFolderDeleteRecordInput>
for crate::module_bindings::EditorAssetFolderDeleteInput
{
fn from(input: EditorAssetFolderDeleteRecordInput) -> Self {
Self {
folder_id: input.folder_id,
owner_user_id: input.owner_user_id,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<EditorAssetCreateRecordInput> for crate::module_bindings::EditorAssetCreateInput {
fn from(input: EditorAssetCreateRecordInput) -> Self {
Self {
asset_id: input.asset_id,
owner_user_id: input.owner_user_id,
folder_id: input.folder_id,
label: input.label,
asset_object_id: input.asset_object_id,
image_src: input.image_src,
object_key: input.object_key,
width: input.width,
height: input.height,
source_type: input.source_type,
prompt: input.prompt,
actual_prompt: input.actual_prompt,
model: input.model,
provider: input.provider,
task_id: input.task_id,
now_micros: input.now_micros,
}
}
}
impl From<EditorAssetUpdateRecordInput> for crate::module_bindings::EditorAssetUpdateInput {
fn from(input: EditorAssetUpdateRecordInput) -> Self {
Self {
asset_id: input.asset_id,
owner_user_id: input.owner_user_id,
label: input.label,
folder_id: input.folder_id,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<EditorAssetDeleteRecordInput> for crate::module_bindings::EditorAssetDeleteInput {
fn from(input: EditorAssetDeleteRecordInput) -> Self {
Self {
asset_id: input.asset_id,
owner_user_id: input.owner_user_id,
}
}
}
pub(crate) fn map_editor_project_optional_procedure_result(
result: EditorProjectProcedureResult,
) -> Result<Option<EditorProjectRecord>, SpacetimeClientError> {
@@ -248,6 +429,58 @@ pub(crate) fn map_editor_project_resource_procedure_result(
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照"))
}
pub(crate) fn map_editor_asset_library_procedure_result(
result: EditorAssetLibraryProcedureResult,
) -> Result<EditorAssetLibraryRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.library
.map(map_editor_asset_library_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材库快照"))
}
pub(crate) fn map_editor_asset_folder_procedure_result(
result: EditorAssetFolderProcedureResult,
) -> Result<EditorAssetFolderRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.folder
.map(map_editor_asset_folder_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材文件夹快照"))
}
pub(crate) fn map_editor_asset_folder_library_procedure_result(
result: EditorAssetFolderProcedureResult,
) -> Result<EditorAssetLibraryRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.library
.map(map_editor_asset_library_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材库快照"))
}
pub(crate) fn map_editor_asset_procedure_result(
result: EditorAssetProcedureResult,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.asset
.map(map_editor_asset_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材快照"))
}
fn map_editor_project_snapshot(
snapshot: EditorProjectSnapshot,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
@@ -309,3 +542,55 @@ fn map_editor_project_resource_snapshot(
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_editor_asset_library_snapshot(
snapshot: EditorAssetLibrarySnapshot,
) -> EditorAssetLibraryRecord {
EditorAssetLibraryRecord {
folders: snapshot
.folders
.into_iter()
.map(map_editor_asset_folder_snapshot)
.collect(),
assets: snapshot
.assets
.into_iter()
.map(map_editor_asset_snapshot)
.collect(),
}
}
fn map_editor_asset_folder_snapshot(
snapshot: EditorAssetFolderSnapshot,
) -> EditorAssetFolderRecord {
EditorAssetFolderRecord {
folder_id: snapshot.folder_id,
label: snapshot.label,
sort_order: snapshot.sort_order,
collapsed: snapshot.collapsed,
system_default: snapshot.system_default,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_editor_asset_snapshot(snapshot: EditorAssetSnapshot) -> EditorAssetRecord {
EditorAssetRecord {
asset_id: snapshot.asset_id,
folder_id: snapshot.folder_id,
label: snapshot.label,
asset_object_id: snapshot.asset_object_id,
image_src: snapshot.image_src,
object_key: snapshot.object_key,
width: snapshot.width,
height: snapshot.height,
source_type: snapshot.source_type,
prompt: snapshot.prompt,
actual_prompt: snapshot.actual_prompt,
model: snapshot.model,
provider: snapshot.provider,
task_id: snapshot.task_id,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}