补齐自动分离恢复状态胶水
新增 separation 恢复探测、完成清理命令与 DTO。 持久化项目相对 cut 图片路径并保留 state sidecar 到最终提交。
This commit is contained in:
@@ -337,6 +337,30 @@ async fn separate_ui(
|
||||
ui_editor::commands::separate_ui_impl(project_path, asset_id, state).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn inspect_separation_recovery(
|
||||
project_path: String,
|
||||
asset_id: String,
|
||||
) -> Result<ui_editor::commands::SeparationRecoveryDTO, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.list")?;
|
||||
ui_editor::commands::separation::inspect_separation_recovery(root, &asset_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn finalize_separation(project_path: String, asset_id: String) -> Result<(), String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.register")?;
|
||||
ui_editor::commands::separation::finalize_separation(root, &asset_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn discard_separation_recovery(project_path: String, asset_id: String) -> Result<(), String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.register")?;
|
||||
ui_editor::commands::separation::finalize_separation(root, &asset_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn merge_ui(state: ui_editor::state::State) -> Result<ui_editor::commands::MergeDTO, String> {
|
||||
ui_editor::commands::merge_ui_impl(state).await
|
||||
@@ -2569,6 +2593,9 @@ fn main() {
|
||||
suggest_ui_design_semantic,
|
||||
recognize_ui,
|
||||
separate_ui,
|
||||
inspect_separation_recovery,
|
||||
finalize_separation,
|
||||
discard_separation_recovery,
|
||||
merge_ui,
|
||||
bind_components,
|
||||
load_ui_design_state,
|
||||
|
||||
@@ -12,6 +12,6 @@ pub(crate) use merge::{merge_ui_impl, merge_ui_impl_with_provider};
|
||||
pub use recognition::RecognitionDTO;
|
||||
pub(crate) use recognition::{recognize_ui_impl, recognize_ui_impl_with_provider};
|
||||
pub(crate) use separation::separate_ui_impl;
|
||||
pub use separation::SeparationDTO;
|
||||
pub use separation::{SeparationDTO, SeparationRecoveryDTO};
|
||||
pub(crate) use ui_design_suggestion::suggest_ui_design_semantic_impl;
|
||||
pub use ui_design_suggestion::UIDesignSuggestionTreeNode;
|
||||
|
||||
@@ -84,6 +84,15 @@ pub struct SeparationDTO {
|
||||
pub problematic_nodes: Vec<ProblematicNode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))]
|
||||
pub struct SeparationRecoveryDTO {
|
||||
pub exists: bool,
|
||||
pub bound_node_count: usize,
|
||||
pub problematic_node_count: usize,
|
||||
pub has_pending_tree: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct BindingArea {
|
||||
|
||||
@@ -25,6 +25,21 @@ pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result<PathBuf, St
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
pub fn separation_state_path(root: &Path, asset_id: &str) -> Result<PathBuf, String> {
|
||||
Ok(separation_sidecar_dir(root, asset_id)?.join("state.json"))
|
||||
}
|
||||
|
||||
pub fn project_relative_path(root: &Path, path: &Path) -> Result<String, String> {
|
||||
let relative = path
|
||||
.strip_prefix(root)
|
||||
.map_err(|_| "separation 产物必须位于项目目录内".to_string())?;
|
||||
let value = relative.to_string_lossy().replace('\\', "/");
|
||||
if value.is_empty() || value.starts_with('/') || value.split('/').any(|part| part == "..") {
|
||||
return Err("separation 产物相对路径无效".to_string());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn write_separation_state(path: &Path, state: &SeparationState) -> Result<(), String> {
|
||||
if state.schema_version != SEPARATION_STATE_SCHEMA_VERSION {
|
||||
app_log!("ui_separation.error stage=state_write reason=schema_mismatch");
|
||||
@@ -116,3 +131,36 @@ pub fn separation_dto(state: &SeparationState) -> SeparationDTO {
|
||||
problematic_nodes: state.problematic_nodes.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inspect_separation_recovery(
|
||||
root: &Path,
|
||||
asset_id: &str,
|
||||
) -> Result<SeparationRecoveryDTO, String> {
|
||||
let state_path = separation_state_path(root, asset_id)?;
|
||||
if !state_path.exists() {
|
||||
return Ok(SeparationRecoveryDTO {
|
||||
exists: false,
|
||||
bound_node_count: 0,
|
||||
problematic_node_count: 0,
|
||||
has_pending_tree: false,
|
||||
});
|
||||
}
|
||||
let state = read_separation_state(&state_path)?;
|
||||
Ok(SeparationRecoveryDTO {
|
||||
exists: true,
|
||||
bound_node_count: state.bound.len(),
|
||||
problematic_node_count: state.problematic_nodes.len(),
|
||||
has_pending_tree: state.trees.iter().any(|tree| {
|
||||
!tree.root.children.is_empty() || tree.root_extractable
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finalize_separation(root: &Path, asset_id: &str) -> Result<(), String> {
|
||||
let state_path = separation_state_path(root, asset_id)?;
|
||||
match fs::remove_file(&state_path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(format!("删除 separation state 失败:{error}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ pub(crate) async fn separate_ui_impl(
|
||||
);
|
||||
format!("创建 separation sidecar 失败:{e}")
|
||||
})?;
|
||||
let state_path = sidecar.join("state.json");
|
||||
let state_path = separation_state_path(root, &asset_id)?;
|
||||
let restored = state_path.exists();
|
||||
let mut separation = if restored {
|
||||
app_log!("ui_separation.state_restore.start asset_id={}", asset_id);
|
||||
@@ -538,8 +538,10 @@ pub(crate) async fn separate_ui_impl(
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
cut_paths
|
||||
.insert(to_node.clone(), cut_path.to_string_lossy().to_string());
|
||||
cut_paths.insert(
|
||||
to_node.clone(),
|
||||
project_relative_path(root, &cut_path)?,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
app_log!(
|
||||
@@ -577,19 +579,6 @@ pub(crate) async fn separate_ui_impl(
|
||||
batch_index += 1;
|
||||
}
|
||||
}
|
||||
match fs::remove_file(&state_path) {
|
||||
Ok(()) => app_log!("ui_separation.state_removed asset_id={}", asset_id),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
app_log!(
|
||||
"ui_separation.state_remove_skipped asset_id={} reason=not_found",
|
||||
asset_id
|
||||
)
|
||||
}
|
||||
Err(error) => app_log!(
|
||||
"ui_separation.error stage=state_remove asset_id={} error={error}",
|
||||
asset_id
|
||||
),
|
||||
}
|
||||
app_log!(
|
||||
"ui_separation.completed asset_id={} bound_nodes={} problematic_nodes={}",
|
||||
asset_id,
|
||||
|
||||
Reference in New Issue
Block a user