对齐 Wolai 侧栏体验并收拢设计入库

This commit is contained in:
lix-2026
2026-04-30 16:18:54 +08:00
parent 8c895b3dc0
commit afb2a5b8a0
89 changed files with 23188 additions and 84 deletions
+63 -5
View File
@@ -2,7 +2,9 @@ use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::web_shell::{
build_editor_bootstrap_json, build_page_aggregate_snapshot, escape_html, escape_script_json,
load_file_tree_html, load_sidebar_tree_html, load_workspace_shell_projection,
render_document_title_controller_script, render_editor_island_adapter_script,
};
use crate::transport::convex::execute_convex_mutation_by_name;
use crate::workspace_shell::render_workspace_shell_sidebar_html;
@@ -140,23 +142,79 @@ pub async fn root_entry(
.active_page_title
.clone()
.unwrap_or_default();
let content = crate::ssr::render_view(leptos::view! {
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_id={workspace_id.clone()} workspace_sidebar_html={workspace_sidebar_html} active_page_id={active_page_id} active_page_title={active_page_title} />
});
let render_workspace_entry = || {
crate::ssr::render_view(leptos::view! {
<crate::ssr::pages::home::HomePage
sidebar_tree_html={sidebar_tree_html.clone()}
workspace_name={workspace_name.clone()}
workspace_id={workspace_id.clone()}
workspace_sidebar_html={workspace_sidebar_html.clone()}
active_page_id={active_page_id.clone()}
active_page_title={active_page_title.clone()}
/>
})
};
let (html_title, content, body_extra) = if active_page_id.trim().is_empty() {
("MNOTE".to_string(), render_workspace_entry(), String::new())
} else {
match build_page_aggregate_snapshot(
&state,
&context,
&active_page_id,
Some(workspace_id.as_str()),
)
.await
{
Ok(aggregate) => {
let title = aggregate.head.title.as_str();
let page_subtree_json = serde_json::to_string(&aggregate.tree.page_subtree)
.unwrap_or_else(|_| "null".to_string());
let snapshot_json =
serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
let bootstrap_json = build_editor_bootstrap_json(&aggregate, &context);
let content = crate::ssr::render_view(leptos::view! {
<crate::ssr::pages::document::DocumentPage
title={title.to_string()}
document_id={active_page_id.clone()}
workspace_id={workspace_id.clone()}
sidebar_tree_html={sidebar_tree_html.clone()}
workspace_name={workspace_name.clone()}
workspace_sidebar_html={workspace_sidebar_html.clone()}
page_subtree_json={page_subtree_json}
/>
});
let body_extra = format!(
r#"<script id="__MNOTE_PAGE_AGGREGATE__" type="application/json">{}</script>
<script id="__MNOTE_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
{}
{}"#,
escape_script_json(&snapshot_json),
escape_script_json(&bootstrap_json),
render_document_title_controller_script(),
render_editor_island_adapter_script(),
);
(title.to_string(), content, body_extra)
}
Err(_) => ("MNOTE".to_string(), render_workspace_entry(), String::new()),
}
};
let mut response = Html(format!(
r#"<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>MNOTE</title>
<title>{}</title>
<style>{}</style>
</head>
<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
{}
{}
</body>
</html>"#,
escape_html(&html_title),
crate::ssr::MNOTE_CSS,
content
content,
body_extra
))
.into_response();
stamp_gateway_headers(response.headers_mut(), false);
+27 -3
View File
@@ -36,7 +36,7 @@ use bridge_runtime::RuntimeCommandEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -209,7 +209,7 @@ fn collect_expanded_ids(projection: &Value) -> BTreeSet<String> {
}
pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeRenderRow> {
projection
let mut rows: Vec<PageTreeRenderRow> = projection
.get("items")
.and_then(Value::as_array)
.map(|items| {
@@ -232,6 +232,7 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
node_id: node_id.to_string(),
parent_node_id: item
.get("parentNodeId")
.or_else(|| item.get("parentId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
@@ -261,7 +262,30 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
})
.collect()
})
.unwrap_or_default()
.unwrap_or_default();
let parent_by_id = rows
.iter()
.map(|row| (row.node_id.clone(), row.parent_node_id.clone()))
.collect::<BTreeMap<_, _>>();
for row in &mut rows {
if row.depth == 0 && row.parent_node_id.is_some() {
let mut depth = 0_u32;
let mut cursor = row.parent_node_id.as_deref();
while let Some(parent_id) = cursor {
depth += 1;
cursor = parent_by_id
.get(parent_id)
.and_then(|parent| parent.as_deref());
if depth > 32 {
break;
}
}
row.depth = depth;
}
}
rows
}
pub(crate) fn collect_filetree_render_rows(
@@ -126,7 +126,10 @@ pub async fn document_page_shell(
Ok(response)
}
fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestContext) -> String {
pub(crate) fn build_editor_bootstrap_json(
aggregate: &PageAggregate,
context: &RequestContext,
) -> String {
serde_json::to_string(&json!({
"schema": "mnote.editor_bootstrap.v1",
"documentId": aggregate.identity.document_id,
@@ -142,7 +145,7 @@ fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestConte
.unwrap_or_else(|_| "{}".to_string())
}
fn render_document_title_controller_script() -> &'static str {
pub(crate) fn render_document_title_controller_script() -> &'static str {
r#"<script>
(() => {
const CONTRACT = 'mnote.document_title_controller.v1';
@@ -259,7 +262,7 @@ fn render_document_title_controller_script() -> &'static str {
</script>"#
}
fn render_editor_island_adapter_script() -> &'static str {
pub(crate) fn render_editor_island_adapter_script() -> &'static str {
r#"<script type="module">
(() => {
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
@@ -586,7 +589,7 @@ pub async fn page_aggregate(
Ok(response)
}
async fn build_page_aggregate_snapshot(
pub(crate) async fn build_page_aggregate_snapshot(
state: &AppState,
context: &RequestContext,
document_id: &str,
@@ -661,7 +664,7 @@ fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
}
}
fn escape_html(value: &str) -> String {
pub(crate) fn escape_html(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
@@ -669,7 +672,7 @@ fn escape_html(value: &str) -> String {
.replace('"', "&quot;")
}
fn escape_script_json(value: &str) -> String {
pub(crate) fn escape_script_json(value: &str) -> String {
value.replace("</script", "<\\/script")
}