feat: cut over rust web main shell
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
//! MNOTE 登录页面组件
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
|
||||
/// MNOTE 登录页面
|
||||
///
|
||||
/// 当 legacy compat 关闭时由 `auth_entry` 路由使用。
|
||||
#[component]
|
||||
pub fn AuthPage() -> impl IntoView {
|
||||
view! {
|
||||
<PageLayout current_nav="home">
|
||||
<section class="mnote-auth">
|
||||
<h1>MNOTE 登录</h1>
|
||||
<p>
|
||||
"此页面由 mnote-web gateway 提供。"
|
||||
</p>
|
||||
</section>
|
||||
</PageLayout>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! MNOTE 文档页面组件 (SSR)
|
||||
//!
|
||||
//! 提供文档编辑器的服务器端渲染壳。
|
||||
//! 外部手写 `<body>` 包装和 `<script>` 数据嵌入由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use crate::ssr::pages::layout::PageLayout;
|
||||
|
||||
/// MNOTE 文档页面
|
||||
///
|
||||
/// 渲染文档编辑器的 SSR 壳结构:
|
||||
/// - `<PageLayout current_nav="documents">`
|
||||
/// - 标题区域
|
||||
/// - 页面聚合快照标记
|
||||
/// - 编辑器占位区
|
||||
#[component]
|
||||
pub fn DocumentPage(
|
||||
/// 文档标题
|
||||
title: String,
|
||||
/// 侧栏页面树 HTML(可选)
|
||||
#[prop(optional)]
|
||||
sidebar_tree_html: Option<String>,
|
||||
/// 工作区名称(可选)
|
||||
#[prop(optional)]
|
||||
workspace_name: Option<String>,
|
||||
) -> impl IntoView {
|
||||
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">
|
||||
<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>
|
||||
</main>
|
||||
</PageLayout>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! MNOTE 首页组件(Wolai 风格)
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
|
||||
/// MNOTE 首页。
|
||||
#[component]
|
||||
pub fn HomePage(
|
||||
/// 侧栏页面树 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>,
|
||||
) -> impl IntoView {
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
||||
const SIDEBAR_TREE_JS: &str = r##"
|
||||
<script>
|
||||
(function(){
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开/折叠
|
||||
tree.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[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') {
|
||||
window.location.href = '/documents/' + encodeURIComponent(nodeId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
"##;
|
||||
|
||||
/// MNOTE Wolai 风格页面布局
|
||||
///
|
||||
/// 包含左侧栏 + 内容区的双栏布局。
|
||||
/// 侧栏显示品牌、导航链接和可选的页面树。
|
||||
///
|
||||
/// # 用法
|
||||
///
|
||||
/// ```ignore
|
||||
/// view! {
|
||||
/// <PageLayout current_nav="home" sidebar_tree_html={None}>
|
||||
/// <section>...</section>
|
||||
/// </PageLayout>
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn PageLayout(
|
||||
children: Children,
|
||||
current_nav: &'static str,
|
||||
/// 侧栏页面树 HTML(可选),由路由 handler 渲染
|
||||
#[prop(optional)]
|
||||
sidebar_tree_html: Option<String>,
|
||||
/// 工作区名称(可选),显示在侧栏顶部
|
||||
#[prop(optional)]
|
||||
workspace_name: Option<String>,
|
||||
/// workspace shell 侧栏 sections HTML(可选),由 projection 渲染
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
) -> 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 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()),
|
||||
)
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
||||
<aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
|
||||
<div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
|
||||
<a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
|
||||
<span class="sidebar-workspace-name">{ws_name}</span>
|
||||
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
||||
</div>
|
||||
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索"><span class="nav-icon">"⌕"</span></a>
|
||||
<a href="/graph" title="关系图"><span class="nav-icon">"⌬"</span></a>
|
||||
<a href="/actions" title="快捷动作"><span class="nav-icon">"↯"</span></a>
|
||||
<a href="/inbox" title="收纳"><span class="nav-icon">"▣"</span></a>
|
||||
<a href="/files" title="文件"><span class="nav-icon">"▱"</span></a>
|
||||
<a href="/more" title="更多"><span class="nav-icon">"…"</span></a>
|
||||
</nav>
|
||||
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||
</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-actions" aria-label="页面操作">
|
||||
<span title="收藏">"☆"</span>
|
||||
<span title="演示">"▻"</span>
|
||||
<span title="评论">"◴"</span>
|
||||
<span title="关系">"⌬"</span>
|
||||
<span title="成员">"♙+"</span>
|
||||
<span title="历史">"◷"</span>
|
||||
<span title="更多">"…"</span>
|
||||
</div>
|
||||
</header>
|
||||
<article class="mnote-content">
|
||||
{children()}
|
||||
</article>
|
||||
<div class="wolai-floating-actions" aria-label="浮动操作">
|
||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button">"AI"</button>
|
||||
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button">"?"</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! MNOTE 思维导图页面组件 (SSR)
|
||||
//!
|
||||
//! 提供思维导图页面的服务器端渲染壳。
|
||||
//! 外部手写 `<body>` 包装和 `<script>` 数据嵌入由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::layout::PageLayout;
|
||||
|
||||
/// MNOTE 思维导图页面
|
||||
///
|
||||
/// 渲染思维导图的 SSR 壳结构:
|
||||
/// - `<main id="mnote-mindmap-shell" data-object-shell="mindmap">`
|
||||
/// - 思维导图标题区域
|
||||
/// - React 运行时挂载占位区
|
||||
#[component]
|
||||
pub fn MindmapPage(
|
||||
/// 文档 ID
|
||||
document_id: String,
|
||||
/// 思维导图 ID
|
||||
mindmap_id: String,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<PageLayout current_nav="documents">
|
||||
<main
|
||||
id="mnote-mindmap-shell"
|
||||
data-object-shell="mindmap"
|
||||
data-document-id={document_id}
|
||||
data-mindmap-id={mindmap_id}
|
||||
>
|
||||
<header>
|
||||
<h1>{"思维导图"}</h1>
|
||||
</header>
|
||||
<section id="mnote-mindmap-island" data-react-island="mindmap_runtime"></section>
|
||||
</main>
|
||||
</PageLayout>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! SSR 页面组件
|
||||
|
||||
pub mod auth;
|
||||
pub mod document;
|
||||
pub mod home;
|
||||
pub mod layout;
|
||||
pub mod mindmap;
|
||||
pub mod search;
|
||||
@@ -0,0 +1,80 @@
|
||||
//! MNOTE 搜索页面组件 (SSR)
|
||||
//!
|
||||
//! 提供搜索页面的服务器端渲染壳。
|
||||
//! 由 `<body>` 外层包装、搜索契约 JSON 嵌入脚本由路由 handler 处理。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use crate::ssr::pages::layout::PageLayout;
|
||||
|
||||
/// MNOTE 搜索页面
|
||||
///
|
||||
/// 渲染搜索页面的 SSR 壳结构:
|
||||
/// - `<main id="mnote-search-shell" data-island-host="react_search_palette">`
|
||||
/// - 搜索标题区域
|
||||
/// - 搜索岛占位区
|
||||
#[component]
|
||||
pub fn SearchPage(
|
||||
/// 工作区 ID
|
||||
#[prop(into)]
|
||||
workspace_id: String,
|
||||
/// 搜索查询
|
||||
#[prop(into)]
|
||||
search_query: String,
|
||||
/// 侧栏页面树 HTML(可选)
|
||||
#[prop(optional)]
|
||||
sidebar_tree_html: Option<String>,
|
||||
/// 工作区名称(可选)
|
||||
#[prop(optional)]
|
||||
workspace_name: Option<String>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<PageLayout current_nav="search" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()}>
|
||||
<main id="mnote-search-shell" data-island-host="react_search_palette">
|
||||
<header class="search-header">
|
||||
<h1>{"搜索"}</h1>
|
||||
<p class="search-meta">
|
||||
{format!("工作区: {}", workspace_id)}
|
||||
</p>
|
||||
<p class="search-query">
|
||||
{if search_query.is_empty() {
|
||||
"请输入关键词".to_string()
|
||||
} else {
|
||||
format!("查询: {}", search_query)
|
||||
}}
|
||||
</p>
|
||||
</header>
|
||||
<section id="mnote-search-island" data-react-island="search_palette"></section>
|
||||
</main>
|
||||
</PageLayout>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ssr::render_view;
|
||||
|
||||
#[test]
|
||||
fn search_page_renders_correct_structure() {
|
||||
let html = render_view(view! {
|
||||
<SearchPage workspace_id="ws_demo" search_query="Rust" />
|
||||
});
|
||||
assert!(html.contains("mnote-search-shell"));
|
||||
assert!(html.contains("data-island-host=\"react_search_palette\""));
|
||||
assert!(html.contains("mnote-search-island"));
|
||||
assert!(html.contains("data-react-island=\"search_palette\""));
|
||||
assert!(html.contains("搜索"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_page_contains_layout() {
|
||||
let html = render_view(view! {
|
||||
<SearchPage workspace_id="ws_demo" search_query="" />
|
||||
});
|
||||
assert!(html.contains("mnote-shell"));
|
||||
assert!(html.contains("mnote-sidebar"));
|
||||
assert!(html.contains("mnote-sidebar-brand"));
|
||||
assert!(html.contains("mnote-sidebar-nav"));
|
||||
assert!(html.contains("mnote-content"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user