//! MNOTE 通用页面布局组件(Wolai / Notion 风格) use leptos::prelude::*; const SIDEBAR_TREE_JS: &str = r##" (function(){ function closestAction(target, selector) { return target && typeof target.closest === 'function' ? target.closest(selector) : null; } function resolveWorkspaceId(trigger) { var direct = (trigger.getAttribute('data-workspace-id') || '').trim(); if (direct) return direct; var root = trigger.closest('[data-workspace-id]'); return root ? (root.getAttribute('data-workspace-id') || '').trim() : ''; } async function dispatchTreeCommand(trigger, body) { trigger.setAttribute('data-pending', 'true'); trigger.setAttribute('disabled', 'disabled'); try { var response = await fetch('/api/tree/commands', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); var payload = await response.json().catch(function(){ return null; }); if (!response.ok || !payload || !payload.result) { throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status); } return payload.result; } catch (error) { trigger.removeAttribute('disabled'); trigger.setAttribute('data-pending', 'false'); trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error)); throw error; } } async function createPage(trigger, parentId) { var workspaceId = resolveWorkspaceId(trigger); var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim(); if (!workspaceId) return; var result = await dispatchTreeCommand(trigger, { action: 'create', workspaceId: workspaceId, parentId: effectiveParentId || null, title: '新页面' }); var nextWorkspaceId = result.workspaceId || workspaceId; window.location.href = '/documents/' + encodeURIComponent(result.documentId) + '?workspaceId=' + encodeURIComponent(nextWorkspaceId); } document.addEventListener('click', function(e) { var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]'); if (createTrigger) { e.preventDefault(); void createPage(createTrigger, null); return; } var tree = document.getElementById('sidebar-tree-root'); if (!tree || !tree.contains(e.target)) return; var btn = closestAction(e.target, '[data-rust-action]'); if (!btn) return; var nodeId = btn.getAttribute('data-node-id'); var action = btn.getAttribute('data-rust-action'); if (action === 'toggle') { var row = btn.closest('.tree-row'); if (!row) return; var li = row.parentElement; var children = li.querySelector(':scope > .tree-children'); if (children) { children.classList.toggle('tree-children--collapsed'); var isCollapsed = children.classList.contains('tree-children--collapsed'); row.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true'); btn.textContent = isCollapsed ? '▸' : '▾'; } e.preventDefault(); } else if (action === 'open') { var workspaceId = resolveWorkspaceId(btn); window.location.href = '/documents/' + encodeURIComponent(nodeId) + (workspaceId ? '?workspaceId=' + encodeURIComponent(workspaceId) : ''); e.preventDefault(); } else if (action === 'create') { e.preventDefault(); void createPage(btn, nodeId); } else if (action === 'rename') { e.preventDefault(); var title = window.prompt('重命名页面'); if (title && title.trim()) { void dispatchTreeCommand(btn, { action: 'rename', workspaceId: resolveWorkspaceId(btn), documentId: nodeId, title: title.trim() }).then(function(){ window.location.reload(); }); } } }); var tree = document.getElementById('sidebar-tree-root'); if (!tree) return; var currentPath = window.location.pathname; var match = currentPath.match(/^\/documents\/([^\/]+)/); if (match) { var activeId = match[1]; var links = tree.querySelectorAll('[data-rust-action="open"]'); for (var i = 0; i < links.length; i++) { if (links[i].getAttribute('data-node-id') === activeId) { links[i].closest('.tree-row').setAttribute('data-active', 'true'); } } } })(); "##; /// MNOTE Wolai 风格页面布局 /// /// 包含左侧栏 + 内容区的双栏布局。 /// 侧栏显示品牌、导航链接和可选的页面树。 /// /// # 用法 /// /// ```ignore /// view! { /// ///
...
///
/// } /// ``` #[component] pub fn PageLayout( children: Children, current_nav: &'static str, /// 侧栏页面树 HTML(可选),由路由 handler 渲染 #[prop(optional)] sidebar_tree_html: Option, /// 工作区名称(可选),显示在侧栏顶部 #[prop(optional)] workspace_name: Option, /// workspace shell 侧栏 sections HTML(可选),由 projection 渲染 #[prop(optional)] workspace_sidebar_html: Option, /// 顶栏当前页面标题(可选) #[prop(optional)] topbar_title: Option, ) -> impl IntoView { let sidebar_tree_html = sidebar_tree_html.unwrap_or_default(); let ws_name = workspace_name .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "开发用户 的空间".to_string()); let topbar_title = topbar_title .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "个人".to_string()); let sidebar_sections_html = workspace_sidebar_html .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| { let dataset = serde_json::json!({ "workspaces": [{ "id": "default", "name": ws_name.clone() }], "documents": [] }); let projection = crate::workspace_shell::build_workspace_shell_projection( &dataset, "default", None, &ws_name, ); crate::workspace_shell::render_workspace_shell_sidebar_html( &projection, Some(sidebar_tree_html.as_str()), None, ) }); view! {
"☰""⌂"{topbar_title}
"☆" "▻" "◴" "⌬" "♙+" "◷" "…"
{children()}
} }