fix: relink rust web tree editor runtime
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! MNOTE 登录页面组件
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// MNOTE 登录页面
|
||||
///
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! 提供文档编辑器的服务器端渲染壳。
|
||||
//! 外部手写 `<body>` 包装和 `<script>` 数据嵌入由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use crate::ssr::pages::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// MNOTE 文档页面
|
||||
///
|
||||
@@ -17,21 +17,53 @@ use crate::ssr::pages::layout::PageLayout;
|
||||
pub fn DocumentPage(
|
||||
/// 文档标题
|
||||
title: String,
|
||||
/// 文档 id
|
||||
document_id: String,
|
||||
/// 侧栏页面树 HTML(可选)
|
||||
#[prop(optional)]
|
||||
sidebar_tree_html: Option<String>,
|
||||
/// 工作区名称(可选)
|
||||
#[prop(optional)]
|
||||
workspace_name: Option<String>,
|
||||
/// workspace shell 侧栏 sections HTML(可选)
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
/// Page Aggregate 子树 JSON(可选)
|
||||
#[prop(optional)]
|
||||
page_subtree_json: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let has_page_subtree = page_subtree_json
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && *value != "null")
|
||||
.is_some();
|
||||
view! {
|
||||
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()}>
|
||||
<main class="document-shell" data-editor-host="leptos_tiptap_island">
|
||||
<PageLayout current_nav="documents" 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()} topbar_title={title.clone()}>
|
||||
<main class="document-shell" data-editor-host="leptos_tiptap_island" data-document-id={document_id.clone()}>
|
||||
<header class="document-shell-header">
|
||||
<h1>{title}</h1>
|
||||
</header>
|
||||
<section data-page-aggregate-snapshot="mnote.page_aggregate.v1"></section>
|
||||
<section id="mnote-editor-island" data-editor-host="leptos_tiptap_island"></section>
|
||||
<section
|
||||
data-testid="mnote-page-subtree"
|
||||
data-page-tree-source="page_aggregate.tree.pageSubtree"
|
||||
data-page-subtree-present={has_page_subtree.to_string()}
|
||||
></section>
|
||||
<section id="mnote-editor-island" data-editor-host="leptos_tiptap_island">
|
||||
<div
|
||||
id="mnote-leptos-tiptap-island-editor-root"
|
||||
data-testid="mnote-leptos-tiptap-island-editor-root"
|
||||
data-editor-host-kind="leptos_tiptap_island"
|
||||
data-runtime-editor-status="booting"
|
||||
></div>
|
||||
<div
|
||||
class="sr-only"
|
||||
data-editor-host-observability="rust-web-inline-island"
|
||||
data-editor-host-active="leptos_tiptap_island"
|
||||
data-editor-host-requested="leptos_tiptap_island"
|
||||
data-editor-host-status="booting"
|
||||
></div>
|
||||
</section>
|
||||
</main>
|
||||
</PageLayout>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! MNOTE 首页组件(Wolai 风格)
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// MNOTE 首页。
|
||||
#[component]
|
||||
@@ -12,25 +12,61 @@ pub fn HomePage(
|
||||
/// 工作区名称(可选)
|
||||
#[prop(optional)]
|
||||
workspace_name: Option<String>,
|
||||
/// 工作区 id(可选)
|
||||
#[prop(optional)]
|
||||
workspace_id: Option<String>,
|
||||
/// workspace shell 侧栏 sections HTML(可选)
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
/// 当前选中的页面 id(可选)
|
||||
#[prop(optional)]
|
||||
active_page_id: Option<String>,
|
||||
/// 当前选中的页面标题(可选)
|
||||
#[prop(optional)]
|
||||
active_page_title: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let active_page_id = active_page_id.unwrap_or_default();
|
||||
let active_page_title = active_page_title
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "个人".to_string());
|
||||
let has_active_page = !active_page_id.trim().is_empty();
|
||||
let workspace_id = workspace_id
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let workspace_id_value = workspace_id.clone().unwrap_or_default();
|
||||
let active_href = workspace_id
|
||||
.as_deref()
|
||||
.map(|workspace_id| format!("/documents/{active_page_id}?workspaceId={workspace_id}"))
|
||||
.unwrap_or_else(|| format!("/documents/{active_page_id}"));
|
||||
view! {
|
||||
<PageLayout current_nav="home" 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()}>
|
||||
<div class="mnote-home">
|
||||
<div class="mnote-home-icon" aria-hidden="true">"⌂"</div>
|
||||
<h1>"个人"</h1>
|
||||
<div class="mnote-home-links">
|
||||
<a href="/documents/backup"><span>"◰"</span>"正版软件备份"</a>
|
||||
<a href="/documents/growth"><span>"◜"</span>"个人发展"</a>
|
||||
<a href="/documents/notes"><span>"▦"</span>"杂记"</a>
|
||||
<a href="/documents/passwords"><span>"▣"</span>"密码"</a>
|
||||
<a href="/documents/wolai-guide"><span>"▰"</span>"Wolai 教程"</a>
|
||||
<a href="/documents/health"><span>"⌂"</span>"个人健康"</a>
|
||||
<a href="/documents/software"><span>"▤"</span>"软件开发"</a>
|
||||
</div>
|
||||
</div>
|
||||
<PageLayout current_nav="home" 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()} topbar_title={active_page_title.clone()}>
|
||||
{move || if has_active_page {
|
||||
view! {
|
||||
<main class="document-shell" data-root-active-page-id={active_page_id.clone()} data-editor-host="leptos_tiptap_island">
|
||||
<header class="document-shell-header">
|
||||
<h1>{active_page_title.clone()}</h1>
|
||||
</header>
|
||||
<section class="mnote-workspace-active-state" data-testid="mnote-workspace-active-page-state">
|
||||
<a class="mnote-workspace-active-link" href={active_href.clone()}>"打开当前页面"</a>
|
||||
</section>
|
||||
</main>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<section class="mnote-workspace-empty-state" data-testid="mnote-workspace-empty-state">
|
||||
<h1>"暂无页面"</h1>
|
||||
<p>"当前工作区还没有可显示的页面。"</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mnote-empty-create-page"
|
||||
data-testid="mnote-empty-create-page"
|
||||
data-mnote-action="create-page"
|
||||
data-workspace-id={workspace_id_value.clone()}
|
||||
>"新建页面"</button>
|
||||
</section>
|
||||
}.into_any()
|
||||
}}
|
||||
</PageLayout>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,65 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
const SIDEBAR_TREE_JS: &str = r##"
|
||||
<script>
|
||||
(function(){
|
||||
var tree = document.getElementById('sidebar-tree-root');
|
||||
if (!tree) return;
|
||||
function closestAction(target, selector) {
|
||||
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
||||
}
|
||||
|
||||
// 高亮当前页
|
||||
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');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 展开/折叠
|
||||
tree.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-rust-action]');
|
||||
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');
|
||||
@@ -41,12 +79,40 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (action === 'open') {
|
||||
window.location.href = '/documents/' + encodeURIComponent(nodeId);
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
"##;
|
||||
|
||||
/// MNOTE Wolai 风格页面布局
|
||||
@@ -76,6 +142,9 @@ pub fn PageLayout(
|
||||
/// workspace shell 侧栏 sections HTML(可选),由 projection 渲染
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
/// 顶栏当前页面标题(可选)
|
||||
#[prop(optional)]
|
||||
topbar_title: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
|
||||
|
||||
@@ -83,6 +152,10 @@ pub fn PageLayout(
|
||||
.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())
|
||||
@@ -92,14 +165,12 @@ pub fn PageLayout(
|
||||
"documents": []
|
||||
});
|
||||
let projection = crate::workspace_shell::build_workspace_shell_projection(
|
||||
&dataset,
|
||||
"default",
|
||||
None,
|
||||
&ws_name,
|
||||
&dataset, "default", None, &ws_name,
|
||||
);
|
||||
crate::workspace_shell::render_workspace_shell_sidebar_html(
|
||||
&projection,
|
||||
Some(sidebar_tree_html.as_str()),
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -124,7 +195,7 @@ pub fn PageLayout(
|
||||
</aside>
|
||||
<div class="mnote-main">
|
||||
<header class="wolai-topbar" data-testid="wolai-topbar">
|
||||
<div class="wolai-topbar-left"><span class="wolai-menu-icon">"☰"</span><span class="wolai-home-icon">"⌂"</span><span>{"个人"}</span></div>
|
||||
<div class="wolai-topbar-left"><span class="wolai-menu-icon">"☰"</span><span class="wolai-home-icon">"⌂"</span><span>{topbar_title}</span></div>
|
||||
<div class="wolai-topbar-actions" aria-label="页面操作">
|
||||
<span title="收藏">"☆"</span>
|
||||
<span title="演示">"▻"</span>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! 提供思维导图页面的服务器端渲染壳。
|
||||
//! 外部手写 `<body>` 包装和 `<script>` 数据嵌入由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// MNOTE 思维导图页面
|
||||
///
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! 提供搜索页面的服务器端渲染壳。
|
||||
//! 由 `<body>` 外层包装、搜索契约 JSON 嵌入脚本由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use crate::ssr::pages::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// MNOTE 搜索页面
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user