Optimize local folder file tree lazy loading

This commit is contained in:
lix-2026
2026-05-26 12:30:01 +08:00
parent 8cd8b44db3
commit 4744757883
10 changed files with 728 additions and 41 deletions
@@ -18,6 +18,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
shortMindmapFileName,
syncSidebarFileTreeSelection,
} = dependencies;
var fileTreeLazyChildrenCache = new Map();
var fileTreeExpandedRelativePaths = new Set();
var fileTreeLazyCacheRootUri = '';
function updateTitleEverywhere(documentId, title) {
if (!documentId) return;
@@ -580,12 +583,13 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
var objectIdentity = fileObjectIdentity(item);
var ownerDocumentId = fileOwnerDocumentId(item, documentId, objectIdentity);
var iconKind = iconKindOf(item);
var cachedChildren = relativePath ? (fileTreeLazyChildrenCache.get(relativePath) || []) : [];
var title = isFileTreeProjectionPageRow(rowKind, assetId)
? fileTreePageTitle(rawTitle)
: normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length || cachedChildren.length);
var expanded = expandable && (fileTreeExpandedRelativePaths.has(relativePath) || item.expandedByDefault !== false);
var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId;
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
var toggle = expandable
@@ -594,16 +598,44 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
var createAction = rowKind === 'document'
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
: '';
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
var childRowsHtml = children.length
? renderFileRows(nodeId, grouped, activeId, activeRowId)
: cachedChildren.length
? renderFileRows('', groupRowsByParent(cachedChildren), activeId, activeRowId)
: '';
var childHtml = expandable && expanded && childRowsHtml
? '<ul class="tree-children">' + childRowsHtml + '</ul>'
: '';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
}).join('');
}
function ensureFileTreeLazyCacheScope() {
var rootUri = currentRootUri();
if (rootUri === fileTreeLazyCacheRootUri) return;
fileTreeLazyCacheRootUri = rootUri;
fileTreeLazyChildrenCache.clear();
fileTreeExpandedRelativePaths.clear();
}
function rememberFileTreeExpansionState() {
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
var relativePath = localFileTreeRelativePathFromRow(row);
if (!relativePath) return;
if (row.getAttribute('aria-expanded') === 'true') {
fileTreeExpandedRelativePaths.add(relativePath);
} else {
fileTreeExpandedRelativePaths.delete(relativePath);
}
});
}
function renderFileProjection(projection) {
var tree = document.getElementById('sidebar-file-tree-root');
if (!tree) return false;
ensureFileTreeLazyCacheScope();
rememberFileTreeExpansionState();
var rows = projectionItems(projection);
var activeId = currentDocumentId();
var activeRowId = currentFileTreeActiveRowId();
@@ -763,18 +795,105 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
return true;
}
function localFileTreeRelativePathFromRow(row) {
if (!(row instanceof HTMLElement)) return '';
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
if (direct) return direct;
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath');
if (runtimeFn) {
try {
return String(runtimeFn(row, { localFilePathFromAssetId: localFilePathFromAssetId }) || '').trim();
} catch (_) {}
}
var rowId = String(row.getAttribute('data-row-id') || '').trim();
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length);
if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length);
if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length);
if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length);
return '';
}
function setTreeRowExpanded(row, button, expanded) {
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
if (button) {
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
button.textContent = expanded ? '▾' : '▸';
}
var relativePath = localFileTreeRelativePathFromRow(row);
if (!relativePath) return;
if (expanded) fileTreeExpandedRelativePaths.add(relativePath);
else fileTreeExpandedRelativePaths.delete(relativePath);
}
function renderCachedFileTreeChildren(row, button, relativePath) {
var cachedRows = fileTreeLazyChildrenCache.get(relativePath) || [];
if (!cachedRows.length) return false;
var node = row.closest('.tree-node');
if (!node) return false;
var children = node.querySelector(':scope > .tree-children');
if (!children) {
children = document.createElement('ul');
children.className = 'tree-children';
node.appendChild(children);
}
children.innerHTML = renderFileRows('', groupRowsByParent(cachedRows), currentDocumentId(), currentFileTreeActiveRowId());
children.classList.remove('tree-children--collapsed');
row.setAttribute('data-filetree-children-loaded', 'true');
setTreeRowExpanded(row, button, true);
syncSidebarFileTreeSelection();
return true;
}
async function loadFileTreeChildren(row, button) {
if (!(row instanceof HTMLElement)) return false;
if (row.getAttribute('data-shell-mode') !== 'filetree') return false;
if (currentSourceKind() !== 'local_folder') return false;
ensureFileTreeLazyCacheScope();
var relativePath = localFileTreeRelativePathFromRow(row);
if (relativePath && renderCachedFileTreeChildren(row, button, relativePath)) return true;
if (row.getAttribute('data-filetree-children-loaded') === 'true') return false;
if (row.getAttribute('data-filetree-children-loading') === 'true') return true;
var rootUri = currentRootUri();
if (!rootUri || !relativePath) return false;
row.setAttribute('data-filetree-children-loading', 'true');
try {
var url = new URL('/api/tree/projections/file/children', window.location.origin);
var workspaceId = currentWorkspaceId();
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('parentRelativePath', relativePath);
var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
var payload = await response.json().catch(function() { return null; });
if (!response.ok) throw new Error(payload && (payload.error || payload.message) || 'filetree_children_failed_' + response.status);
var projection = readProjection(payload && (payload.result || payload));
var rows = projectionItems(projection);
fileTreeLazyChildrenCache.set(relativePath, rows);
if (!rows.length) {
row.setAttribute('data-filetree-children-loaded', 'true');
setTreeRowExpanded(row, button, true);
return true;
}
return renderCachedFileTreeChildren(row, button, relativePath);
} catch (error) {
setTreeLiveApplyError(error && error.message ? error.message : '文件树子目录加载失败');
return false;
} finally {
row.removeAttribute('data-filetree-children-loading');
}
}
function toggleChildren(row, button) {
var li = row && row.parentElement;
var children = li ? li.querySelector(':scope > .tree-children') : null;
if (!children) return;
if (!children) {
void loadFileTreeChildren(row, button);
return;
}
children.classList.toggle('tree-children--collapsed');
var collapsed = children.classList.contains('tree-children--collapsed');
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
} else if (button) {
button.textContent = collapsed ? '▸' : '▾';
}
setTreeRowExpanded(row, button, !collapsed);
}