对齐 Wolai 编辑器搜索与文档画布
This commit is contained in:
@@ -663,33 +663,34 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
'<div class="wolai-search-dialog">' +
|
||||
'<div class="wolai-search-input-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
|
||||
'<input data-testid="wolai-search-input" class="wolai-search-input" type="text" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
||||
'<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
||||
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options" aria-label="搜索选项">' +
|
||||
'<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
|
||||
'<div class="wolai-search-options-left">' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch is-on" data-search-switch="title" role="switch" aria-checked="true" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options-right">' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch is-on" data-search-switch="page" role="switch" aria-checked="true" aria-label="页面内搜索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
|
||||
'<div class="wolai-search-results" data-testid="wolai-search-results"></div>' +
|
||||
'<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
||||
if (input) input.addEventListener('input', renderSearchResults);
|
||||
if (input) input.addEventListener('input', scheduleSearchResultsRender);
|
||||
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
var isOn = button.getAttribute('aria-checked') !== 'true';
|
||||
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
|
||||
button.classList.toggle('is-on', isOn);
|
||||
scheduleSearchResultsRender();
|
||||
});
|
||||
});
|
||||
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
||||
@@ -699,26 +700,33 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return overlay;
|
||||
}
|
||||
|
||||
var activeSearchRequestId = 0;
|
||||
var searchRenderTimer = 0;
|
||||
|
||||
function searchText(value) {
|
||||
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function collectSearchCandidates() {
|
||||
var seen = Object.create(null);
|
||||
var candidates = [];
|
||||
function add(title, source) {
|
||||
title = searchText(title);
|
||||
if (!title || seen[title]) return;
|
||||
seen[title] = true;
|
||||
candidates.push({ title: title, source: source || '当前工作区' });
|
||||
}
|
||||
document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) {
|
||||
add(node.textContent, '页面');
|
||||
});
|
||||
document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) {
|
||||
add(node.textContent, '当前页面');
|
||||
});
|
||||
return candidates.slice(0, 120);
|
||||
function currentWorkspaceName() {
|
||||
var name = document.querySelector('.sidebar-workspace-name');
|
||||
return searchText(name && name.textContent) || '当前工作区';
|
||||
}
|
||||
|
||||
function currentDocumentId() {
|
||||
var shell = document.querySelector('.document-shell[data-document-id]');
|
||||
var bodyId = document.body && document.body.getAttribute('data-document-id');
|
||||
return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
|
||||
}
|
||||
|
||||
function searchSwitchValue(overlay, name) {
|
||||
var button = overlay.querySelector('[data-search-switch="' + name + '"]');
|
||||
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
|
||||
}
|
||||
|
||||
function highlightedHtml(value) {
|
||||
return escapeHtml(value)
|
||||
.replace(/<mark>/g, '<mark>')
|
||||
.replace(/<\/mark>/g, '</mark>');
|
||||
}
|
||||
|
||||
function highlightSearchTitle(title, query) {
|
||||
@@ -732,31 +740,82 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
|
||||
}
|
||||
|
||||
function renderSearchResults() {
|
||||
function renderSearchRecentState(overlay) {
|
||||
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
||||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||||
if (options instanceof HTMLElement) options.hidden = true;
|
||||
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (results) {
|
||||
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSearchResultsRender() {
|
||||
window.clearTimeout(searchRenderTimer);
|
||||
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
|
||||
}
|
||||
|
||||
async function renderSearchResults() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (!(overlay instanceof HTMLElement)) return;
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
||||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||||
if (!input || !meta || !results) return;
|
||||
var query = searchText(input.value);
|
||||
var candidates = collectSearchCandidates();
|
||||
var filtered = query
|
||||
? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; })
|
||||
: candidates.slice(0, 8);
|
||||
filtered = filtered.slice(0, 100);
|
||||
meta.innerHTML = '<span>共 ' + filtered.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!filtered.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty">暂无匹配结果</div>';
|
||||
if (!query) {
|
||||
activeSearchRequestId += 1;
|
||||
renderSearchRecentState(overlay);
|
||||
return;
|
||||
}
|
||||
results.innerHTML = filtered.map(function(item) {
|
||||
return '<button type="button" class="wolai-search-result-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="link" aria-hidden="true"></span>' +
|
||||
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(item.title, query) + '</span>' +
|
||||
'<span class="wolai-search-result-snippet">' + escapeHtml(item.source) + ' · 当前工作区</span></span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
if (options instanceof HTMLElement) options.hidden = false;
|
||||
var requestId = ++activeSearchRequestId;
|
||||
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
|
||||
try {
|
||||
var response = await fetch('/api/search/documents', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
documentId: currentDocumentId() || null,
|
||||
query: query,
|
||||
limit: 30,
|
||||
filters: {
|
||||
titleOnly: searchSwitchValue(overlay, 'title'),
|
||||
exact: searchSwitchValue(overlay, 'exact'),
|
||||
includeOcr: false,
|
||||
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
|
||||
timeRange: 'any',
|
||||
timeField: 'updated'
|
||||
}
|
||||
})
|
||||
});
|
||||
var payload = await response.json();
|
||||
if (requestId !== activeSearchRequestId) return;
|
||||
var items = Array.isArray(payload.results) ? payload.results : [];
|
||||
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!items.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = items.map(function(item) {
|
||||
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
|
||||
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
|
||||
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
|
||||
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '">' +
|
||||
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
|
||||
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
|
||||
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span></span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
} catch (error) {
|
||||
if (requestId !== activeSearchRequestId) return;
|
||||
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function isSearchModalOpen() {
|
||||
@@ -768,8 +827,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var overlay = ensureSearchModal();
|
||||
overlay.hidden = false;
|
||||
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
|
||||
renderSearchResults();
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
|
||||
void renderSearchResults();
|
||||
if (input) {
|
||||
setTimeout(function() { input.focus(); input.select(); }, 0);
|
||||
}
|
||||
@@ -793,7 +853,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
|
||||
if (searchTrigger) {
|
||||
e.preventDefault();
|
||||
openSearchModal();
|
||||
toggleSearchModal();
|
||||
return;
|
||||
}
|
||||
var searchDialog = closestAction(e.target, '.wolai-search-dialog');
|
||||
if (isSearchModalOpen() && !searchDialog) {
|
||||
closeSearchModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1305,7 +1370,7 @@ 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}</span>
|
||||
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
|
||||
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
||||
</div>
|
||||
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
||||
@@ -1326,17 +1391,19 @@ pub fn PageLayout(
|
||||
<div class="wolai-topbar-left">
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
||||
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span>
|
||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span>
|
||||
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
|
||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
|
||||
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="wolai-topbar-actions" aria-label="页面操作">
|
||||
<span class="wolai-public-pill"><span aria-hidden="true">"●"</span>"Public"</span>
|
||||
<span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
|
||||
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user