2026-04-29 12:24:44 +08:00
|
|
|
|
//! MNOTE 文档页面组件 (SSR)
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 提供文档编辑器的服务器端渲染壳。
|
|
|
|
|
|
//! 外部手写 `<body>` 包装和 `<script>` 数据嵌入由路由 handler 处理。
|
|
|
|
|
|
|
|
|
|
|
|
use crate::ssr::pages::layout::PageLayout;
|
2026-04-29 14:36:24 +08:00
|
|
|
|
use leptos::prelude::*;
|
2026-05-06 21:44:20 +08:00
|
|
|
|
use serde_json::Value;
|
2026-04-29 12:24:44 +08:00
|
|
|
|
|
2026-05-08 23:15:00 +08:00
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
pub struct DocumentPaneViewModel {
|
|
|
|
|
|
pub pane_role: &'static str,
|
|
|
|
|
|
pub title: String,
|
|
|
|
|
|
pub document_id: String,
|
|
|
|
|
|
pub workspace_id: String,
|
|
|
|
|
|
pub page_wide_layout: bool,
|
|
|
|
|
|
pub page_small_text: bool,
|
|
|
|
|
|
pub page_layout_density: String,
|
|
|
|
|
|
pub page_font: String,
|
|
|
|
|
|
pub page_show_heading_numbers: bool,
|
|
|
|
|
|
pub has_page_subtree: bool,
|
|
|
|
|
|
pub primary_legacy_ids: bool,
|
|
|
|
|
|
pub visible: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[component]
|
|
|
|
|
|
fn DocumentPane(model: DocumentPaneViewModel) -> impl IntoView {
|
|
|
|
|
|
let pane_hidden = !model.visible;
|
|
|
|
|
|
let pane_role = model.pane_role.to_string();
|
|
|
|
|
|
let pane_role_attr = pane_role.clone();
|
|
|
|
|
|
let pane_role_attr_two = pane_role.clone();
|
|
|
|
|
|
let pane_role_attr_three = pane_role.clone();
|
|
|
|
|
|
let pane_role_attr_four = pane_role.clone();
|
|
|
|
|
|
let pane_role_attr_five = pane_role.clone();
|
|
|
|
|
|
let pane_label = if model.pane_role == "secondary" {
|
|
|
|
|
|
"右侧文档"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"主文档"
|
|
|
|
|
|
};
|
|
|
|
|
|
let title_input_id = if model.primary_legacy_ids {
|
|
|
|
|
|
Some("mnote-page-title-input".to_string())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let editor_island_id = if model.primary_legacy_ids {
|
|
|
|
|
|
Some("mnote-editor-island".to_string())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let editor_root_id = if model.primary_legacy_ids {
|
|
|
|
|
|
Some("mnote-leptos-tiptap-island-editor-root".to_string())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
view! {
|
|
|
|
|
|
<section
|
|
|
|
|
|
class="document-pane"
|
|
|
|
|
|
data-document-pane="true"
|
|
|
|
|
|
data-pane-role={pane_role_attr}
|
|
|
|
|
|
data-pane-document-id={model.document_id.clone()}
|
|
|
|
|
|
data-pane-workspace-id={model.workspace_id.clone()}
|
|
|
|
|
|
data-pane-visible={model.visible.to_string()}
|
|
|
|
|
|
aria-label={pane_label}
|
|
|
|
|
|
hidden={pane_hidden}
|
|
|
|
|
|
>
|
|
|
|
|
|
<main
|
|
|
|
|
|
class="document-shell"
|
|
|
|
|
|
data-editor-host="leptos_tiptap_island"
|
|
|
|
|
|
data-document-id={model.document_id.clone()}
|
|
|
|
|
|
data-workspace-id={model.workspace_id.clone()}
|
|
|
|
|
|
data-pane-role={pane_role_attr_two}
|
|
|
|
|
|
data-page-wide-layout={model.page_wide_layout.to_string()}
|
|
|
|
|
|
data-page-small-text={model.page_small_text.to_string()}
|
|
|
|
|
|
data-layout-density={model.page_layout_density.clone()}
|
|
|
|
|
|
data-page-font={model.page_font.clone()}
|
|
|
|
|
|
data-page-show-heading-numbers={model.page_show_heading_numbers.to_string()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<header class="document-shell-header">
|
|
|
|
|
|
<div class="document-page-icon" aria-hidden="true">
|
|
|
|
|
|
<span class="material-symbols-outlined material-symbols-filled mnote-material-page-icon" data-icon="home"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="document-pane-header-row">
|
|
|
|
|
|
<h1 class="document-title-heading">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
id={title_input_id}
|
|
|
|
|
|
class="document-title-input"
|
|
|
|
|
|
aria-label="页面标题"
|
|
|
|
|
|
data-page-title-input="true"
|
|
|
|
|
|
data-document-id={model.document_id.clone()}
|
|
|
|
|
|
data-workspace-id={model.workspace_id.clone()}
|
|
|
|
|
|
data-pane-role={pane_role_attr_three}
|
|
|
|
|
|
data-title-endpoint="/api/documents/title"
|
|
|
|
|
|
rows="1"
|
|
|
|
|
|
>{model.title.clone()}</textarea>
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="document-shell-meta" aria-label="页面元信息">
|
|
|
|
|
|
<span data-page-title-current="true">{model.title.clone()}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<section data-page-aggregate-snapshot="mnote.page_aggregate.v1"></section>
|
|
|
|
|
|
<section
|
|
|
|
|
|
data-testid="mnote-page-subtree"
|
|
|
|
|
|
data-page-tree-source="page_aggregate.tree.pageSubtree"
|
|
|
|
|
|
data-page-subtree-present={model.has_page_subtree.to_string()}
|
|
|
|
|
|
data-pane-role={pane_role_attr_four}
|
|
|
|
|
|
></section>
|
|
|
|
|
|
<section
|
|
|
|
|
|
id={editor_island_id}
|
|
|
|
|
|
data-editor-host="leptos_tiptap_island"
|
|
|
|
|
|
data-pane-role={pane_role_attr_five}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
id={editor_root_id}
|
|
|
|
|
|
data-testid="mnote-leptos-tiptap-island-editor-root"
|
|
|
|
|
|
data-editor-host-kind="leptos_tiptap_island"
|
|
|
|
|
|
data-runtime-editor-status="booting"
|
|
|
|
|
|
data-pane-role={pane_role.clone()}
|
|
|
|
|
|
></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"
|
|
|
|
|
|
data-pane-role={pane_role}
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 12:24:44 +08:00
|
|
|
|
/// MNOTE 文档页面
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 渲染文档编辑器的 SSR 壳结构:
|
|
|
|
|
|
/// - `<PageLayout current_nav="documents">`
|
|
|
|
|
|
/// - 标题区域
|
|
|
|
|
|
/// - 页面聚合快照标记
|
|
|
|
|
|
/// - 编辑器占位区
|
|
|
|
|
|
#[component]
|
|
|
|
|
|
pub fn DocumentPage(
|
|
|
|
|
|
/// 文档标题
|
|
|
|
|
|
title: String,
|
2026-04-29 14:36:24 +08:00
|
|
|
|
/// 文档 id
|
|
|
|
|
|
document_id: String,
|
2026-04-30 05:46:36 +08:00
|
|
|
|
/// 工作区 id
|
|
|
|
|
|
workspace_id: String,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
/// 侧栏页面树 HTML(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
sidebar_tree_html: Option<String>,
|
|
|
|
|
|
/// 工作区名称(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
workspace_name: Option<String>,
|
2026-04-29 14:36:24 +08:00
|
|
|
|
/// workspace shell 侧栏 sections HTML(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
workspace_sidebar_html: Option<String>,
|
|
|
|
|
|
/// Page Aggregate 子树 JSON(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
page_subtree_json: Option<String>,
|
2026-05-06 21:44:20 +08:00
|
|
|
|
/// Page Aggregate 页面选项 JSON(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
page_options_json: Option<String>,
|
2026-05-08 23:15:00 +08:00
|
|
|
|
/// 右侧文档标题(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
secondary_title: String,
|
|
|
|
|
|
/// 右侧文档 id(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
secondary_document_id: String,
|
|
|
|
|
|
/// 右侧 workspace id(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
secondary_workspace_id: String,
|
|
|
|
|
|
/// 右侧 Page Aggregate 子树 JSON(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
secondary_page_subtree_json: String,
|
|
|
|
|
|
/// 右侧页面选项 JSON(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
secondary_page_options_json: String,
|
2026-05-19 08:07:17 +08:00
|
|
|
|
/// 是否显示管理员授权入口
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
show_admin_access_policy: bool,
|
2026-05-20 10:43:38 +08:00
|
|
|
|
/// 是否启用树实时流
|
|
|
|
|
|
#[prop(optional, default = true)]
|
|
|
|
|
|
enable_tree_live: bool,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
) -> impl IntoView {
|
2026-04-29 14:36:24 +08:00
|
|
|
|
let has_page_subtree = page_subtree_json
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
|
.filter(|value| !value.is_empty() && *value != "null")
|
|
|
|
|
|
.is_some();
|
2026-04-29 16:23:49 +08:00
|
|
|
|
let workspace_label = workspace_name
|
|
|
|
|
|
.clone()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "个人空间".to_string());
|
2026-05-06 21:44:20 +08:00
|
|
|
|
let page_options = page_options_json
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|value| serde_json::from_str::<Value>(value).ok())
|
|
|
|
|
|
.unwrap_or(Value::Null);
|
|
|
|
|
|
let page_wide_layout = page_options
|
|
|
|
|
|
.get("wideLayout")
|
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let page_small_text = page_options
|
|
|
|
|
|
.get("smallText")
|
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let page_layout_density = page_options
|
|
|
|
|
|
.get("layoutDensity")
|
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
|
.unwrap_or("normal")
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
let page_font = page_options
|
|
|
|
|
|
.get("pageFont")
|
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
|
.unwrap_or("default")
|
|
|
|
|
|
.to_string();
|
2026-05-08 00:41:03 +08:00
|
|
|
|
let page_show_heading_numbers = false;
|
2026-05-08 23:15:00 +08:00
|
|
|
|
let secondary_has_page_subtree = Some(secondary_page_subtree_json.as_str())
|
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
|
.filter(|value| !value.is_empty() && *value != "null")
|
|
|
|
|
|
.is_some();
|
|
|
|
|
|
let secondary_page_options = Some(secondary_page_options_json.as_str())
|
|
|
|
|
|
.and_then(|value| serde_json::from_str::<Value>(value).ok())
|
|
|
|
|
|
.unwrap_or(Value::Null);
|
|
|
|
|
|
let secondary_page_wide_layout = secondary_page_options
|
|
|
|
|
|
.get("wideLayout")
|
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let secondary_page_small_text = secondary_page_options
|
|
|
|
|
|
.get("smallText")
|
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let secondary_page_layout_density = secondary_page_options
|
|
|
|
|
|
.get("layoutDensity")
|
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
|
.unwrap_or("normal")
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
let secondary_page_font = secondary_page_options
|
|
|
|
|
|
.get("pageFont")
|
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
|
.unwrap_or("default")
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
let secondary_page_show_heading_numbers = false;
|
|
|
|
|
|
let secondary_visible = Some(secondary_document_id.as_str())
|
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.is_some();
|
|
|
|
|
|
let primary_model = DocumentPaneViewModel {
|
|
|
|
|
|
pane_role: "primary",
|
|
|
|
|
|
title: title.clone(),
|
|
|
|
|
|
document_id: document_id.clone(),
|
|
|
|
|
|
workspace_id: workspace_id.clone(),
|
|
|
|
|
|
page_wide_layout,
|
|
|
|
|
|
page_small_text,
|
|
|
|
|
|
page_layout_density: page_layout_density.clone(),
|
|
|
|
|
|
page_font: page_font.clone(),
|
|
|
|
|
|
page_show_heading_numbers,
|
|
|
|
|
|
has_page_subtree,
|
|
|
|
|
|
primary_legacy_ids: true,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
let secondary_model = DocumentPaneViewModel {
|
|
|
|
|
|
pane_role: "secondary",
|
|
|
|
|
|
title: secondary_title,
|
|
|
|
|
|
document_id: secondary_document_id,
|
|
|
|
|
|
workspace_id: if secondary_workspace_id.trim().is_empty() {
|
|
|
|
|
|
workspace_id.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
secondary_workspace_id
|
|
|
|
|
|
},
|
|
|
|
|
|
page_wide_layout: secondary_page_wide_layout,
|
|
|
|
|
|
page_small_text: secondary_page_small_text,
|
|
|
|
|
|
page_layout_density: secondary_page_layout_density,
|
|
|
|
|
|
page_font: secondary_page_font,
|
|
|
|
|
|
page_show_heading_numbers: secondary_page_show_heading_numbers,
|
|
|
|
|
|
has_page_subtree: secondary_has_page_subtree,
|
|
|
|
|
|
primary_legacy_ids: false,
|
|
|
|
|
|
visible: secondary_visible,
|
|
|
|
|
|
};
|
2026-04-29 12:24:44 +08:00
|
|
|
|
view! {
|
2026-05-20 10:43:38 +08:00
|
|
|
|
<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()} show_admin_access_policy={show_admin_access_policy} enable_tree_live={enable_tree_live}>
|
2026-05-08 23:15:00 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="document-workspace"
|
|
|
|
|
|
data-testid="mnote-document-workspace"
|
|
|
|
|
|
data-has-secondary-pane={secondary_visible.to_string()}
|
2026-05-06 21:44:20 +08:00
|
|
|
|
>
|
2026-05-23 23:38:42 +08:00
|
|
|
|
<section class="document-main-editor-group" data-testid="mnote-main-editor-tab-host" data-pane-role="primary">
|
|
|
|
|
|
<div class="mnote-main-tab-strip" data-mnote-main-tab-strip="true" data-pane-role="primary" role="tablist" aria-label="主编辑区标签页">
|
2026-05-20 14:20:48 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="mnote-main-tab is-active"
|
|
|
|
|
|
data-mnote-main-tab="page"
|
|
|
|
|
|
data-mnote-tab-kind="page"
|
2026-05-23 23:38:42 +08:00
|
|
|
|
data-pane-role="primary"
|
2026-05-20 14:20:48 +08:00
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected="true"
|
2026-05-20 19:04:05 +08:00
|
|
|
|
tabindex="0"
|
2026-05-20 14:20:48 +08:00
|
|
|
|
>
|
2026-05-20 17:47:09 +08:00
|
|
|
|
<span class="mnote-main-tab-badge" aria-hidden="true"></span>
|
2026-05-20 14:20:48 +08:00
|
|
|
|
<span class="mnote-main-tab-title">{title.clone()}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mnote-main-tab-panels">
|
2026-05-23 23:38:42 +08:00
|
|
|
|
<div data-mnote-page-tab-panel="true" data-pane-role="primary">
|
2026-05-20 14:20:48 +08:00
|
|
|
|
<DocumentPane model={primary_model} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<section
|
|
|
|
|
|
class="mnote-resource-tab-host"
|
|
|
|
|
|
data-mnote-resource-tab-host="true"
|
2026-05-23 23:38:42 +08:00
|
|
|
|
data-pane-role="primary"
|
2026-05-20 14:20:48 +08:00
|
|
|
|
data-testid="mnote-resource-tab-host"
|
|
|
|
|
|
hidden=true
|
|
|
|
|
|
>
|
2026-05-23 23:38:42 +08:00
|
|
|
|
<div class="mnote-resource-tab-panel-root" data-mnote-resource-tab-panel-root="true" data-pane-role="primary"></div>
|
2026-05-20 14:20:48 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-05-08 23:15:00 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="document-pane-resizer"
|
|
|
|
|
|
data-testid="mnote-secondary-pane-resizer"
|
|
|
|
|
|
data-document-pane-resizer="true"
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
hidden={!secondary_visible}
|
|
|
|
|
|
></div>
|
2026-05-23 23:38:42 +08:00
|
|
|
|
<section class="document-main-editor-group" data-testid="mnote-secondary-editor-tab-host" data-pane-role="secondary" hidden={!secondary_visible}>
|
|
|
|
|
|
<div class="mnote-main-tab-strip" data-mnote-main-tab-strip="true" data-pane-role="secondary" role="tablist" aria-label="右侧编辑区标签页">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="mnote-main-tab is-active"
|
|
|
|
|
|
data-mnote-main-tab="page"
|
|
|
|
|
|
data-mnote-tab-kind="page"
|
|
|
|
|
|
data-pane-role="secondary"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected="true"
|
|
|
|
|
|
tabindex="0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="mnote-main-tab-badge" aria-hidden="true"></span>
|
|
|
|
|
|
<span class="mnote-main-tab-title">{secondary_model.title.clone()}</span>
|
|
|
|
|
|
<span class="mnote-main-tab-close" role="button" data-mnote-pane-close="secondary" aria-label="关闭右侧文档" title="关闭右侧文档">{"×"}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mnote-main-tab-panels">
|
|
|
|
|
|
<div data-mnote-page-tab-panel="true" data-pane-role="secondary">
|
|
|
|
|
|
<DocumentPane model={secondary_model} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<section
|
|
|
|
|
|
class="mnote-resource-tab-host"
|
|
|
|
|
|
data-mnote-resource-tab-host="true"
|
|
|
|
|
|
data-pane-role="secondary"
|
|
|
|
|
|
data-testid="mnote-secondary-resource-tab-host"
|
|
|
|
|
|
hidden=true
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="mnote-resource-tab-panel-root" data-mnote-resource-tab-panel-root="true" data-pane-role="secondary"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-05-08 23:15:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sr-only" data-mnote-workspace-label>{workspace_label}</div>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</PageLayout>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|