20260513 mindmap优化01

This commit is contained in:
lix-2026
2026-05-13 22:43:16 +08:00
parent 17c003976b
commit b4a452a8b7
89 changed files with 11557 additions and 707 deletions
@@ -1,7 +1,13 @@
use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::web_shell::{
build_page_aggregate_snapshot, load_file_tree_html, load_sidebar_tree_html,
load_workspace_shell_projection,
};
use crate::ssr::pages::mindmap::MindmapPage;
use axum::extract::{Extension, Path};
use crate::workspace_shell::render_workspace_shell_sidebar_html;
use axum::extract::{Extension, Path, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue};
use axum::response::{Html, IntoResponse, Response};
use serde_json::json;
@@ -10,9 +16,92 @@ const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell";
pub async fn mindmap_object_shell(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Path((doc_id, mindmap_id)): Path<(String, String)>,
) -> Result<Response, WebError> {
let default_workspace_name = format!("{} 的空间", state.config().dev_user_name);
let aggregate = build_page_aggregate_snapshot(&state, &context, &doc_id, None, None, None)
.await
.ok();
let workspace_id = aggregate
.as_ref()
.map(|value| value.identity.workspace_id.clone())
.filter(|value| !value.trim().is_empty());
let title = aggregate
.as_ref()
.map(|value| value.head.title.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "思维导图".to_string());
let (workspace_name, sidebar_tree_html, workspace_sidebar_html) =
if let Some(workspace_id) = workspace_id.as_deref() {
let workspace_projection = load_workspace_shell_projection(
state.config(),
&context,
workspace_id,
Some(&doc_id),
&default_workspace_name,
)
.await;
let sidebar_tree_html =
load_sidebar_tree_html(state.config(), &context, workspace_id, Some(&doc_id))
.await
.unwrap_or_default();
let file_tree_html =
load_file_tree_html(state.config(), &context, workspace_id, Some(&doc_id))
.await
.unwrap_or_default();
let workspace_name = workspace_projection.workspace_name.clone();
let workspace_sidebar_html = render_workspace_shell_sidebar_html(
&workspace_projection,
Some(sidebar_tree_html.as_str()),
Some(file_tree_html.as_str()),
);
(
Some(workspace_name),
Some(sidebar_tree_html),
Some(workspace_sidebar_html),
)
} else {
(None, None, None)
};
let editor_bootstrap = json!({
"documentId": format!("__mindmap_object__:{doc_id}:{mindmap_id}"),
"workspaceId": workspace_id
.as_ref()
.map(|value| format!("__mindmap_object__:{value}")),
"title": title.clone(),
"content": {
"type": "doc",
"content": [
{
"type": "paragraph",
"attrs": {
"mindmapId": mindmap_id,
"mnoteBlockType": "mindmap",
"projectionVersion": 1,
"rootNodeId": "root",
"mnoteMindmapData": serde_json::Value::Null
}
}
]
},
"readOnly": false,
"editable": true,
"standaloneObject": {
"kind": "mindmap",
"documentId": doc_id,
"mindmapId": mindmap_id
},
"revision": serde_json::Value::Null,
"conflictDetectionKey": serde_json::Value::Null,
"pageOptions": {
"pageWidth": "full",
"smallText": false,
"showHeadingNumbers": false,
"fontFamily": "sans"
}
});
let contract = json!({
"schema": "mnote.mindmap_shell.v1",
"owner": "mnote-web",
@@ -35,11 +124,17 @@ pub async fn mindmap_object_shell(
"requestId": context.trace.request_id,
"traceId": context.trace.trace_id
});
let editor_bootstrap_json =
serde_json::to_string(&editor_bootstrap).unwrap_or_else(|_| "null".to_string());
let contract_json = serde_json::to_string(&contract).unwrap_or_else(|_| "null".to_string());
let body_content = crate::ssr::render_view(leptos::view! {
<MindmapPage
document_id={doc_id.clone()}
mindmap_id={mindmap_id.clone()}
title={title.clone()}
sidebar_tree_html={sidebar_tree_html.unwrap_or_default()}
workspace_name={workspace_name.unwrap_or_default()}
workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()}
/>
});
let html = format!(
@@ -47,19 +142,24 @@ pub async fn mindmap_object_shell(
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>思维导图</title>
<title>{}</title>
<style>{}</style>
</head>
<body data-mnote-web-owner="mnote-web" data-mnote-shell="mindmap" data-document-id="{}" data-mindmap-id="{}">
{}
<script id="__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
<script id="__MNOTE_MINDMAP_SHELL__" type="application/json">{}</script>
{}
</body>
</html>"#,
escape_html(&title),
crate::ssr::MNOTE_CSS,
escape_html(&doc_id),
escape_html(&mindmap_id),
body_content,
escape_script_json(&editor_bootstrap_json),
escape_script_json(&contract_json),
render_mindmap_standalone_bootstrap_script(),
);
let mut response = Html(html).into_response();
stamp_shell_headers(response.headers_mut(), "mindmap");
@@ -87,6 +187,80 @@ fn escape_script_json(value: &str) -> String {
value.replace("</script", "<\\/script")
}
fn render_mindmap_standalone_bootstrap_script() -> &'static str {
r#"<script type="module">
(() => {
const BOOTSTRAP_ID = '__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__';
const MOUNT_ID = 'mnote-mindmap-island';
const parseJsonScript = (id) => {
const node = document.getElementById(id);
if (!node) return null;
try {
return JSON.parse(node.textContent || 'null');
} catch (error) {
console.warn(`mnote mindmap bootstrap JSON 解析失败: ${id}`, error);
return null;
}
};
const loadRuntime = async () => {
if (window.__mnoteLeptosTiptapRuntimePromise) {
return window.__mnoteLeptosTiptapRuntimePromise;
}
window.__mnoteLeptosTiptapRuntimePromise = (async () => {
const manifestResponse = await fetch('/api/leptos-tiptap-runtime/manifest.json', { cache: 'no-store' });
if (!manifestResponse.ok) throw new Error(`manifest_failed_${manifestResponse.status}`);
const manifest = await manifestResponse.json();
if (!manifest.entryAssetPath) throw new Error('island manifest 缺少 entryAssetPath');
const entryUrl = `/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`;
const wasmUrl = manifest.wasmAssetPath ? `/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}` : undefined;
const runtime = await import(entryUrl);
if (typeof runtime.default !== 'function' || typeof runtime.mount !== 'function' || typeof runtime.unmount !== 'function') {
throw new Error('island runtime 导出不完整');
}
await runtime.default(wasmUrl);
if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
window.__MNOTE_MINDMAP_RUST_SHELL__ = {
mount: runtime.mount_mindmap_shell,
unmount: runtime.unmount_mindmap_shell,
};
}
return runtime;
})();
return window.__mnoteLeptosTiptapRuntimePromise;
};
const bootstrap = parseJsonScript(BOOTSTRAP_ID);
const mountTarget = document.getElementById(MOUNT_ID);
if (!bootstrap || !(mountTarget instanceof HTMLElement)) return;
let mountId = null;
let runtimeModule = null;
const mountStandaloneMindmap = async () => {
runtimeModule = await loadRuntime();
mountId = runtimeModule.mount(mountTarget, bootstrap);
mountTarget.setAttribute('data-runtime-mount-id', String(mountId));
};
void mountStandaloneMindmap().catch((error) => {
console.error('mnote standalone mindmap mount failed', error);
mountTarget.setAttribute('data-runtime-editor-status', 'error');
mountTarget.setAttribute('data-runtime-editor-error', error instanceof Error ? error.message : 'unknown');
});
window.addEventListener('beforeunload', () => {
if (mountId != null && runtimeModule && typeof runtimeModule.unmount === 'function') {
try {
runtimeModule.unmount(mountId);
} catch (_error) {}
}
}, { once: true });
})();
</script>"#
}
#[cfg(test)]
mod tests {
use crate::app::{build_app, AppConfig, AppState};
@@ -189,9 +363,21 @@ mod tests {
.expect("body");
let html = String::from_utf8(body.to_vec()).expect("utf8");
assert!(html.contains("mnote.mindmap_shell.v1"));
assert!(html.contains("data-mnote-object-editor=\"mindmap\""));
assert!(html.contains(
"data-mnote-object-identity=\"resource:mindmap:doc_1:mind_1\""
));
assert!(html.contains("data-leptos-mindmap-island=\"standalone\""));
assert!(html.contains("mindmap.simple_mind_map_scene.get"));
assert!(html.contains("mindmap.command.apply"));
assert!(html.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
assert!(html.contains("mnoteBlockType"));
assert!(html.contains("__mindmap_object__:doc_1:mind_1"));
assert!(html.contains("\"standaloneObject\""));
assert!(html.contains("\"documentId\":\"doc_1\""));
assert!(html.contains("\"mindmapId\":\"mind_1\""));
assert!(html.contains("runtimeModule.mount(mountTarget, bootstrap)"));
assert!(html.contains("/api/leptos-tiptap-runtime/manifest.json"));
assert!(!html.contains("react_mindmap_runtime"));
assert!(!html.contains("next-app-router"));
}