feat: 收口本地文件夹入口并推进 page aggregate rust-first

- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
This commit is contained in:
lix-2026
2026-05-09 19:05:06 +08:00
parent 71146de5f0
commit d148e1ccc2
20 changed files with 1635 additions and 374 deletions
+155 -10
View File
@@ -11,6 +11,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
var MNOTE_RECENT_LOCAL_ROOTS_KEY = 'mnote.localFolder.recentRoots';
var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
var mnoteNavigationInFlight = '';
var draggingPageNodeId = '';
@@ -298,6 +299,34 @@ const SIDEBAR_TREE_JS: &str = r##"
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
}
function currentSourceKind() {
return (new URLSearchParams(window.location.search).get('sourceKind') || 'convex_workspace').trim() || 'convex_workspace';
}
function rememberCloudWorkspaceId(workspaceId) {
var normalized = String(workspaceId || '').trim();
if (!normalized || normalized === 'local-folder') return;
try {
if (window.localStorage) window.localStorage.setItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY, normalized);
} catch (_) {}
}
function readLastCloudWorkspaceId() {
try {
var stored = window.localStorage ? window.localStorage.getItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY) : '';
if (stored && stored.trim()) return stored.trim();
} catch (_) {}
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('workspaceId') || '').trim();
if (fromUrl && currentSourceKind() !== 'local_folder') return fromUrl;
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (value && value !== 'local-folder') return value;
}
return '';
}
function copyWorkspaceSourceParams(targetUrl) {
var params = new URLSearchParams(window.location.search);
['sourceKind', 'rootUri', 'secondaryDocumentId', 'secondarySourceKind', 'secondaryRootUri'].forEach(function(name) {
@@ -347,7 +376,19 @@ const SIDEBAR_TREE_JS: &str = r##"
}).join('/');
}
function fileRootUriToPathInput(rootUri) {
var value = String(rootUri || '').replace(/^file:\/\//, '');
try {
return decodeURIComponent(value);
} catch (_) {
return value;
}
}
function openLocalFolderRoot(rootUri) {
if (currentSourceKind() !== 'local_folder') {
rememberCloudWorkspaceId(resolveWorkspaceId(document.body));
}
rememberLocalRoot(rootUri);
var targetUrl = new URL('/', window.location.origin);
targetUrl.searchParams.set('treeView', 'filetree');
@@ -356,6 +397,14 @@ const SIDEBAR_TREE_JS: &str = r##"
window.location.href = targetUrl.toString();
}
function switchToCloudWorkspace() {
var targetUrl = new URL('/', window.location.origin);
var workspaceId = readLastCloudWorkspaceId();
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
targetUrl.searchParams.set('sourceKind', 'convex_workspace');
window.location.href = targetUrl.toString();
}
function closeLocalFolderDialog() {
var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
@@ -461,7 +510,7 @@ const SIDEBAR_TREE_JS: &str = r##"
input.spellcheck = false;
input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
input.setAttribute('data-testid', 'mnote-local-folder-path-input');
input.value = recent.length ? recent[0].replace(/^file:\/\//, '') : '';
input.value = recent.length ? fileRootUriToPathInput(recent[0]) : '';
input.style.width = '100%';
input.style.boxSizing = 'border-box';
input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
@@ -536,6 +585,11 @@ const SIDEBAR_TREE_JS: &str = r##"
card.appendChild(status);
card.appendChild(input);
if (recent.length) {
var recentTitle = document.createElement('div');
recentTitle.style.fontSize = '12px';
recentTitle.style.color = '#6b7280';
recentTitle.textContent = '最近使用';
card.appendChild(recentTitle);
var recentList = document.createElement('div');
recentList.className = 'mnote-local-folder-dialog__recent';
recent.slice(0, 5).forEach(function(rootUri) {
@@ -551,13 +605,6 @@ const SIDEBAR_TREE_JS: &str = r##"
});
card.appendChild(recentList);
}
if (recent.length) {
var recentTitle = document.createElement('div');
recentTitle.style.fontSize = '12px';
recentTitle.style.color = '#6b7280';
recentTitle.textContent = '最近使用';
card.insertBefore(recentTitle, actions);
}
card.appendChild(actions);
dialog.appendChild(card);
dialog.addEventListener('click', function(event) {
@@ -572,6 +619,79 @@ const SIDEBAR_TREE_JS: &str = r##"
openLocalFolderDialog('');
}
function closeWorkspaceSourceMenu() {
var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.querySelectorAll('[data-testid="mnote-workspace-source-trigger"]').forEach(function(trigger) {
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
});
}
function workspaceSourceLabel(rootUri) {
var label = String(rootUri || '').replace(/^file:\/\//, '');
try {
label = decodeURIComponent(label);
} catch (_) {}
return label || '本地文件夹';
}
function openWorkspaceSourceMenu(trigger) {
closeWorkspaceSourceMenu();
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('aria-expanded', 'true');
var menu = document.createElement('div');
menu.className = 'mnote-workspace-source-menu';
menu.setAttribute('data-testid', 'mnote-workspace-source-menu');
menu.setAttribute('role', 'menu');
var cloudButton = document.createElement('button');
cloudButton.type = 'button';
cloudButton.className = 'mnote-workspace-source-menu__item';
cloudButton.setAttribute('data-testid', 'mnote-switch-cloud-workspace');
cloudButton.setAttribute('role', 'menuitem');
cloudButton.textContent = '云空间';
cloudButton.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
switchToCloudWorkspace();
});
menu.appendChild(cloudButton);
var recent = readRecentLocalRoots();
if (recent.length) {
var label = document.createElement('div');
label.className = 'mnote-workspace-source-menu__label';
label.textContent = '最近本地文件夹';
menu.appendChild(label);
recent.slice(0, 8).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'mnote-workspace-source-menu__item';
button.setAttribute('data-testid', 'mnote-recent-local-root');
button.setAttribute('data-root-uri', rootUri);
button.setAttribute('role', 'menuitem');
button.textContent = workspaceSourceLabel(rootUri);
button.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
openLocalFolderRoot(rootUri);
});
menu.appendChild(button);
});
}
var openOther = document.createElement('button');
openOther.type = 'button';
openOther.className = 'mnote-workspace-source-menu__item mnote-workspace-source-menu__item--primary';
openOther.setAttribute('data-testid', 'mnote-open-other-local-folder');
openOther.setAttribute('role', 'menuitem');
openOther.textContent = '打开其他本地文件夹';
openOther.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
requestOpenLocalFolder();
});
menu.appendChild(openOther);
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
}
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
@@ -2280,6 +2400,17 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
if (sourceMenuTrigger) {
e.preventDefault();
var existingSourceMenu = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existingSourceMenu) closeWorkspaceSourceMenu();
else openWorkspaceSourceMenu(sourceMenuTrigger);
return;
}
var sourceMenu = closestAction(e.target, '[data-testid="mnote-workspace-source-menu"]');
if (!sourceMenu) closeWorkspaceSourceMenu();
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
if (tabTrigger) {
e.preventDefault();
@@ -2871,8 +3002,18 @@ pub fn PageLayout(
<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.clone()}</span>
<span class="wolai-sidebar-chevron" aria-hidden="true">""</span>
<button
type="button"
class="mnote-workspace-source-trigger"
data-testid="mnote-workspace-source-trigger"
data-mnote-action="open-workspace-source-menu"
aria-haspopup="menu"
aria-expanded="false"
title="切换云空间或本地文件夹"
>
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
<span class="wolai-sidebar-chevron" aria-hidden="true">""</span>
</button>
</div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
@@ -2931,6 +3072,7 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
assert!(SIDEBAR_TREE_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_JS.contains("treeView"));
@@ -2947,6 +3089,9 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("requestOpenLocalFolder"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind', 'local_folder"));
assert!(SIDEBAR_TREE_JS.contains("switchToCloudWorkspace"));
assert!(SIDEBAR_TREE_JS.contains("mnote-switch-cloud-workspace"));
assert!(SIDEBAR_TREE_JS.contains("mnote-recent-local-root"));
}
#[test]
+64
View File
@@ -233,6 +233,7 @@ a:hover {
gap: 10px;
padding: 12px 16px 8px;
justify-content: flex-start;
position: relative;
}
.wolai-avatar {
@@ -256,12 +257,75 @@ a:hover {
font-weight: 650;
}
.mnote-workspace-source-trigger {
min-width: 0;
flex: 1 1 auto;
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
background: transparent;
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
}
.mnote-workspace-source-trigger:hover,
.mnote-workspace-source-trigger[aria-expanded="true"] {
background: var(--wolai-bg-hover);
}
.wolai-sidebar-chevron {
margin-left: auto;
color: var(--wolai-text-secondary);
font-size: 16px;
}
.mnote-workspace-source-menu {
position: absolute;
inset-inline-start: 0;
top: calc(100% + 6px);
z-index: 120;
width: 250px;
max-width: calc(100vw - 24px);
padding: 6px;
border: 1px solid rgba(27, 28, 28, 0.12);
border-radius: 8px;
background: #fff;
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.18);
}
.mnote-workspace-source-menu__label {
padding: 8px 8px 4px;
color: var(--wolai-text-secondary);
font-size: 12px;
}
.mnote-workspace-source-menu__item {
width: 100%;
display: block;
border: 0;
border-radius: 6px;
background: transparent;
padding: 8px;
color: var(--wolai-text-primary);
font-size: 13px;
line-height: 1.35;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.mnote-workspace-source-menu__item:hover {
background: var(--wolai-bg-hover);
}
.mnote-workspace-source-menu__item--primary {
color: #1D4ED8;
}
.wolai-quick-actions {
flex: 0 0 auto;
display: grid;