Skip to content

Commit ada099b

Browse files
committed
perf:file tree drag import and pptx loading animation
1 parent f08e7f0 commit ada099b

File tree

6 files changed

+273
-112
lines changed

6 files changed

+273
-112
lines changed

electron/renderer/modules/fileTree.js

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@
657657
'left: 0',
658658
'right: 0',
659659
'bottom: 0',
660-
'border: 1px dashed rgba(148, 163, 184, 0.9)',
660+
'border: 1px dashed rgba(96, 165, 250, 0.9)',
661661
'background: rgba(255, 255, 255, 0.75)',
662662
'pointer-events: none',
663663
'border-radius: 6px',
@@ -845,7 +845,6 @@
845845
}
846846

847847
async function unmountDocument(filePath, isFolder) {
848-
dependencies.showLoadingOverlay(isFolder ? '正在取消挂载文件夹…' : '正在取消挂载…');
849848
try {
850849
dependencies.closeAllModals();
851850
const absolutePath = await ensureProjectAbsolutePath(filePath);
@@ -906,7 +905,6 @@
906905
showCancel: false
907906
});
908907
} finally {
909-
dependencies.hideLoadingOverlay();
910908
const fileItem = document.querySelector(`[data-path="${filePath}"]`);
911909
if (fileItem) {
912910
const indicator = fileItem.querySelector('.upload-indicator');
@@ -952,7 +950,6 @@
952950
if (folderOperationKey) {
953951
backendProgressState.folderTasks.delete(folderOperationKey);
954952
}
955-
dependencies.hideLoadingOverlay();
956953
}
957954
}
958955

@@ -996,24 +993,11 @@
996993
}
997994

998995
async function unmountFolder(folderPath) {
999-
ensureBackendStatusListener();
1000-
const overlayMessage = '正在取消挂载文件夹…';
1001-
dependencies.setLoadingOverlayProgress(0.05, {
1002-
message: overlayMessage,
1003-
stage: '准备中'
1004-
});
1005996
let folderOperationKey = null;
1006997
try {
1007998
const normalizedPath = await ensureProjectAbsolutePath(folderPath);
1008999
const relativeTracking = await getTrackingRelativePath(normalizedPath);
1009-
folderOperationKey = buildFolderOperationKey('unmount_folder', relativeTracking);
1010-
if (folderOperationKey) {
1011-
backendProgressState.folderTasks.set(folderOperationKey, {
1012-
message: overlayMessage,
1013-
total: 0,
1014-
completed: 0
1015-
});
1016-
}
1000+
folderOperationKey = null;
10171001
const response = await fetch('http://localhost:8000/api/document/unmount-folder', {
10181002
method: 'POST',
10191003
headers: { 'Content-Type': 'application/json' },
@@ -1034,7 +1018,7 @@
10341018
}
10351019
}
10361020

1037-
function handleFolderOperationResult(title, data) {
1021+
function handleFolderOperationResult(title, data, options = {}) {
10381022
const failed = data.failed || 0;
10391023
const statusKey = data.status || (failed > 0 ? 'partial' : 'success');
10401024
if (data.folder && typeof dependencies.refreshFolderUploadIndicators === 'function') {
@@ -1053,6 +1037,18 @@
10531037
message = '操作完成,部分项目未成功处理。';
10541038
}
10551039

1040+
if (statusKey === 'success' && options.suppressSuccessModal) {
1041+
setTimeout(async () => {
1042+
const explorer = getExplorerModule();
1043+
if (explorer && typeof explorer.refreshFileTree === 'function') {
1044+
await explorer.refreshFileTree();
1045+
} else {
1046+
await loadFileTree();
1047+
}
1048+
}, 300);
1049+
return;
1050+
}
1051+
10561052
dependencies.showModal({
10571053
type: modalType,
10581054
title,
@@ -2014,12 +2010,12 @@
20142010
if (isExpanded) {
20152011
state.expandedFolders.delete(node.path);
20162012
childContainer.style.display = 'none';
2017-
arrow.style.transform = 'rotate(0deg)';
2013+
if (arrow) { arrow.style.transform = 'rotate(0deg)'; }
20182014
folderIcon.innerHTML = getFileIcon(node.name, true, false);
20192015
} else {
20202016
state.expandedFolders.add(node.path);
20212017
childContainer.style.display = 'block';
2022-
arrow.style.transform = 'rotate(90deg)';
2018+
if (arrow) { arrow.style.transform = 'rotate(90deg)'; }
20232019
folderIcon.innerHTML = getFileIcon(node.name, true, true);
20242020
if (typeof dependencies.updateFolderUploadStatus === 'function') {
20252021
dependencies.updateFolderUploadStatus(nodeRelativePath);
@@ -2092,18 +2088,26 @@
20922088
if (node.children) {
20932089
div.classList.add('folder-item');
20942090
const isExpanded = state.expandedFolders.has(node.path);
2095-
const arrow = document.createElement('span');
2096-
arrow.textContent = '▶';
2097-
arrow.className = 'folder-arrow';
2098-
arrow.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)';
2099-
contentDiv.appendChild(arrow);
21002091

21012092
const nameWrapper = document.createElement('span');
21022093
nameWrapper.className = 'file-name';
2094+
2095+
// 还原更小的浅灰色开放式箭头(两段线)
2096+
const arrow = document.createElement('span');
2097+
arrow.className = 'folder-arrow';
2098+
arrow.style.color = '#9aa0a6';
2099+
arrow.innerHTML = `
2100+
<svg width="8" height="8" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
2101+
<polyline points="2,1 8,5 2,9" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
2102+
</svg>
2103+
`;
2104+
nameWrapper.appendChild(arrow);
2105+
21032106
const folderIcon = document.createElement('span');
21042107
folderIcon.className = 'file-icon-wrapper';
21052108
folderIcon.innerHTML = getFileIcon(node.name, true, isExpanded);
21062109
nameWrapper.appendChild(folderIcon);
2110+
21072111
const nameSpan = document.createElement('span');
21082112
nameSpan.className = 'file-name-text';
21092113
nameSpan.textContent = node.name;
@@ -2118,10 +2122,14 @@
21182122
childContainer.dataset.parent = node.path;
21192123
childContainer.dataset.parentRelative = nodeRelativePath;
21202124
childContainer.style.display = isExpanded ? 'block' : 'none';
2121-
arrow.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)';
21222125
node.children.forEach((child) => renderTree(child, childContainer, false, depth + 1));
21232126
container.appendChild(childContainer);
21242127

2128+
// 初始化箭头方向
2129+
if (arrow) {
2130+
arrow.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)';
2131+
}
2132+
21252133
if (isExpanded && typeof dependencies.updateFolderUploadStatus === 'function') {
21262134
dependencies.updateFolderUploadStatus(nodeRelativePath);
21272135
}
@@ -2146,8 +2154,19 @@
21462154
} else {
21472155
div.classList.add('file-item-file');
21482156
div.dataset.fileName = node.name;
2157+
21492158
const nameWrapper = document.createElement('span');
21502159
nameWrapper.className = 'file-name';
2160+
// 文件圆点,颜色淡灰,与箭头垂直对齐
2161+
const fileDot = document.createElement('span');
2162+
fileDot.className = 'file-dot';
2163+
fileDot.innerHTML = `
2164+
<svg width="8" height="8" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
2165+
<circle cx="5" cy="5" r="2.5" fill="currentColor" />
2166+
</svg>
2167+
`;
2168+
nameWrapper.appendChild(fileDot);
2169+
21512170
const fileIconWrapper = document.createElement('span');
21522171
fileIconWrapper.className = 'file-icon-wrapper';
21532172
fileIconWrapper.innerHTML = getFileIcon(node.name, false);
@@ -2204,6 +2223,38 @@
22042223
if (Array.isArray(tree.children)) {
22052224
tree.children.forEach((child) => renderTree(child, state.fileTreeEl, true, 0));
22062225
}
2226+
// 空状态占位:当没有任何文件/文件夹时显示提示
2227+
if (!Array.isArray(tree.children) || tree.children.length === 0) {
2228+
const placeholder = document.createElement('div');
2229+
placeholder.className = 'file-tree-empty-placeholder';
2230+
placeholder.textContent = '你可以新建或导入文件📃';
2231+
placeholder.style.cursor = 'pointer';
2232+
placeholder.setAttribute('role', 'button');
2233+
placeholder.tabIndex = 0;
2234+
// 顶对齐:确保占位框贴顶部并覆盖可视宽度
2235+
// 具体宽度与边距由 CSS 控制,这里只确保不出现额外空隙
2236+
placeholder.style.marginTop = '-5px';
2237+
// 点击导入:调用 ExplorerModule.importFiles(打开本地文件夹)
2238+
const explorer = getExplorerModule();
2239+
const handleImport = () => {
2240+
if (explorer && typeof explorer.importFiles === 'function') {
2241+
explorer.importFiles();
2242+
}
2243+
};
2244+
placeholder.addEventListener('click', (e) => {
2245+
e.preventDefault();
2246+
e.stopPropagation();
2247+
handleImport();
2248+
});
2249+
placeholder.addEventListener('keydown', (e) => {
2250+
if (e.key === 'Enter' || e.key === ' ') {
2251+
e.preventDefault();
2252+
e.stopPropagation();
2253+
handleImport();
2254+
}
2255+
});
2256+
state.fileTreeEl.appendChild(placeholder);
2257+
}
22072258
if (typeof dependencies.updateFolderUploadStatus === 'function') {
22082259
await dependencies.updateFolderUploadStatus('data');
22092260
}
@@ -2240,14 +2291,12 @@
22402291
}
22412292
const handleRootDragEnter = (event) => {
22422293
event.preventDefault();
2243-
if (!isExternalDragEvent(event)) {
2244-
return;
2245-
}
22462294
const targetItem = event.target instanceof Element ? event.target.closest('.file-item[data-path]') : null;
22472295
if (targetItem) {
22482296
hideRootOverlay();
22492297
return;
22502298
}
2299+
// 显示根目录虚线边框(支持内部拖拽和外部拖拽)
22512300
showRootOverlay();
22522301
if (state.dropIndicator) {
22532302
state.dropIndicator.style.display = 'none';
@@ -2260,15 +2309,12 @@
22602309
if (event.dataTransfer) {
22612310
event.dataTransfer.dropEffect = external ? 'copy' : 'move';
22622311
}
2263-
if (!external) {
2264-
hideRootOverlay();
2265-
return;
2266-
}
22672312
const targetItem = event.target instanceof Element ? event.target.closest('.file-item[data-path]') : null;
22682313
if (targetItem) {
22692314
hideRootOverlay();
22702315
return;
22712316
}
2317+
// 在非目标项区域显示根目录虚线边框(内部/外部拖拽均支持)
22722318
showRootOverlay();
22732319
if (state.dropIndicator) {
22742320
state.dropIndicator.style.display = 'none';

electron/src/components/pptViewer.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,32 @@ class PptViewer {
7575
background: transparent;
7676
text-align: center;
7777
padding-left: 0;
78+
padding-top: 0;
7879
}
7980
8081
.pptx-loading,
8182
.pptx-error {
8283
display: flex;
8384
justify-content: center;
8485
align-items: center;
85-
height: 200px;
86+
height: 100%;
8687
font-size: 16px;
8788
color: #666;
8889
text-align: center;
8990
}
9091
92+
.pptx-loading {
93+
flex-direction: column;
94+
gap: 8px;
95+
}
96+
9197
.pptx-error {
9298
color: #d32f2f;
9399
}
94100
95101
/* pptx-preview库生成的幻灯片样式优化 */
96102
.pptx-content .ppt-slide {
97-
margin: 20px auto;
103+
margin: 0 auto 20px;
98104
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
99105
border-radius: 8px;
100106
overflow: hidden;
@@ -121,6 +127,13 @@ class PptViewer {
121127
text-align: left;
122128
}
123129
130+
/* 保证加载指示器不被上面规则覆盖 */
131+
.pptx-content > .pptx-loading {
132+
display: flex !important;
133+
width: 100%;
134+
height: 100%;
135+
}
136+
124137
/* PPTX容器样式 - 自定义垂直滚动条为1px粗度 */
125138
.pptx-container {
126139
overflow-x: hidden;
@@ -231,14 +244,16 @@ class PptViewer {
231244
pptxContainer.className = 'pptx-viewer pptx-container';
232245
pptxContainer.innerHTML = `
233246
<div class="pptx-content" id="pptx-content-${tabId}">
234-
<div class="pptx-loading">正在解析PPTX文件,请稍候...</div>
247+
<div class="pptx-loading" role="status" aria-live="polite">
248+
<div class="loading-spinner"></div>
249+
<p class="loading-text">加载中</p>
250+
</div>
235251
</div>
236252
`;
237253

238254
contentElement.appendChild(pptxContainer);
239255

240-
// 使用pptx-preview库解析并渲染PPTX内容
241-
await this.renderPptxWithPreview(tabId, content.buffer, content.fileName);
256+
// 保持加载指示器,渲染由 openPptxFile 调用触发
242257

243258
} catch (error) {
244259
console.error('创建PPTX查看器失败:', error);
@@ -274,9 +289,6 @@ class PptViewer {
274289
throw new Error('找不到PPTX内容容器');
275290
}
276291

277-
// 清空加载提示
278-
contentDiv.innerHTML = '';
279-
280292
// 使用pptx-preview解析并渲染PPTX
281293
// 获取容器的实际尺寸
282294
const containerRect = contentDiv.getBoundingClientRect();
@@ -294,6 +306,12 @@ class PptViewer {
294306
// 调用preview方法渲染PPTX内容
295307
await previewer.preview(buffer);
296308

309+
// 渲染完成后移除加载指示器
310+
const loadingEl = contentDiv.querySelector('.pptx-loading');
311+
if (loadingEl) {
312+
loadingEl.remove();
313+
}
314+
297315
// 返回状态信息
298316
return {
299317
fileName: fileName,

0 commit comments

Comments
 (0)