// 网盘管理系统 - 客户端逻辑 // 版本: v1.2 - 修复404错误 // console.log('[Disk] 加载 DiskManager v1.2'); class DiskManager { constructor() { this.files = []; this.uploadingFiles = []; this.dragCounter = 0; this.clipboard = null; this.shortcutKeys = null; this.container = document.querySelector('.disk-container'); // 全局文件结构缓存(用于快速验证文件夹内容) this.fileStructureCache = new Map(); // key: 文件路径, value: 文件信息(包括pngCount) this.cacheInitialized = false; // 当前登录用户名 this.currentUsername = null; this.init(); } // 获取当前登录用户名 getCurrentUsername() { // 如果已经缓存了用户名,直接返回 if (this.currentUsername) { return this.currentUsername; } // 从导航栏 iframe 中获取用户名 try { // 尝试从父窗口的 navigationFrame 获取 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { try { const navigationFrame = targetWindow.document.getElementById('navigationFrame'); if (navigationFrame && navigationFrame.contentWindow) { const navWindow = navigationFrame.contentWindow; const navDoc = navigationFrame.contentDocument || navWindow.document; // 检查用户是否已登录 const userAvatarContainer = navDoc.getElementById('userAvatarContainer'); if (userAvatarContainer) { const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer); if (computedStyle.display !== 'none') { // 用户已登录,从 userAvatar 的 alt 属性获取用户名 const userAvatar = navDoc.getElementById('userAvatar'); if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') { const username = userAvatar.alt; // 缓存用户名 this.currentUsername = username; console.log('[DiskManager] 从导航栏获取用户名:', username); return username; } } } } } catch (e) { // 跨域或访问限制,继续尝试上层窗口 console.warn('[DiskManager] 访问导航栏失败:', e); } // 尝试更上层的窗口 if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } } catch (error) { console.warn('[DiskManager] 无法获取用户名:', error); } // 如果仍然没有获取到,返回 null return this.currentUsername; } // 设置当前用户名(从外部调用,例如登录成功后) setCurrentUsername(username) { this.currentUsername = username; } // 从 localStorage 恢复登录状态 restoreLoginFromStorage() { try { const loginDataStr = localStorage.getItem('loginData'); if (!loginDataStr) { return; } const loginData = JSON.parse(loginDataStr); const now = Date.now(); // 检查是否过期 if (now >= loginData.expireTime) { localStorage.removeItem('loginData'); return; } // 未过期,恢复登录状态 if (loginData.user && loginData.user.username) { this.setCurrentUsername(loginData.user.username); console.log('[DiskManager] 从 localStorage 恢复登录状态:', loginData.user.username); } } catch (error) { console.error('[DiskManager] 恢复登录状态失败:', error); } } // 监听登录成功消息 initUserListener() { window.addEventListener('message', (event) => { if (event.data && event.data.type === 'login-success' && event.data.user) { const username = event.data.user.username; this.setCurrentUsername(username); console.log('[DiskManager] 收到登录成功消息,设置用户名:', username); // 登录成功后重新加载文件列表 this.loadFiles(); } else if (event.data && event.data.type === 'logout') { this.setCurrentUsername(null); console.log('[DiskManager] 收到登出消息,清除用户名'); // 登出后清空文件列表 this.files = []; this.renderFiles(); } }); } async init() { // 先尝试从 localStorage 恢复登录状态(同步操作,立即执行) this.restoreLoginFromStorage(); // 并行加载工具栏和右键菜单(不阻塞页面) const toolbarPromise = this.ensureToolBar(); const contextMenuPromise = this.ensureContextMenu(); // 初始化基本元素(这些不依赖工具栏和菜单的HTML) this.initElements(); this.initUploadProgress(); this.initUserListener(); // 等待工具栏加载完成后,初始化依赖它的组件 await toolbarPromise; this.initElements(); // 重新获取工具栏中的元素 this.initPath(); this.initSelection(); this.initShortcutKeys(); this.initSearchBar(); this.bindEvents(); // 工具栏加载完成后立即开始加载文件列表 const username = this.getCurrentUsername(); if (username) { console.log('[DiskManager] 初始化时检测到已登录用户:', username); // 开始加载文件(不阻塞后续初始化) this.loadFiles(); } // 等待右键菜单加载完成后初始化(并行进行,不影响文件加载) await contextMenuPromise; this.initContextMenu(); // 后台初始化文件结构缓存(不阻塞页面加载) this.initFileStructureCache(); } async ensureToolBar() { if (document.getElementById('breadcrumb')) { return; } if (!this.container) { return; } try { const response = await fetch('tool-bar.html', { cache: 'no-cache' }); if (!response.ok) { throw new Error('加载工具栏失败'); } const html = await response.text(); this.container.insertAdjacentHTML('afterbegin', html); } catch (error) { console.error('加载工具栏失败:', error); } } async ensureContextMenu() { if (document.getElementById('contextMenu')) { return; } const menuContainer = document.getElementById('contextMenuContainer'); if (!menuContainer) { return; } try { const response = await fetch('right-click-menu.html', { cache: 'no-cache' }); if (!response.ok) { throw new Error('加载右键菜单失败'); } const html = await response.text(); menuContainer.innerHTML = html; } catch (error) { console.error('加载右键菜单失败:', error); } } initElements() { this.dropZone = document.getElementById('dropZone'); this.fileList = document.getElementById('fileList'); this.breadcrumb = document.getElementById('breadcrumb'); this.emptyState = document.getElementById('emptyState'); this.loading = document.getElementById('loading'); this.btnCreateFolder = document.getElementById('btnCreateFolder'); this.fileInput = document.getElementById('fileInput'); this.searchInput = document.getElementById('searchInput'); this.searchClear = document.getElementById('searchClear'); this.selectionBar = document.getElementById('selectionBar'); this.selectionCount = document.getElementById('selectionCount'); this.selectionBox = document.getElementById('selectionBox'); this.btnDownload = document.getElementById('btnDownload'); this.btnDelete = document.getElementById('btnDelete'); this.btnUpload = document.getElementById('btnUpload'); this.uploadProgress = document.getElementById('uploadProgress'); this.uploadProgressList = document.getElementById('uploadProgressList'); this.uploadProgressTemplate = document.getElementById('uploadProgressTemplate'); this.uploadProgressClose = document.getElementById('uploadProgressClose'); this.contextMenu = document.getElementById('contextMenu'); } // 初始化路径导航 initPath() { this.pathNav = new PathNavigator({ container: this.breadcrumb, rootName: '全部文件', onNavigate: (path) => { this.loadFiles(); } }); } // 初始化框选功能 initSelection() { this.selection = new MultipleSelection({ container: this.dropZone, itemsContainer: this.fileList, selectionBox: this.selectionBox, selectionBar: this.selectionBar, selectionCount: this.selectionCount, itemSelector: '.file-item', onSelectionChange: (selectedItems) => { // 选择变化时的回调(可用于其他逻辑) } }); } initUploadProgress() { if (this.uploadProgressClose) { this.uploadProgressClose.addEventListener('click', () => this.hideUploadProgress()); } this.hideUploadProgress(); } initContextMenu() { if (!this.dropZone || !this.contextMenu) { return; } this.contextMenuManager = new RightClickMenu({ target: this.dropZone, menu: this.contextMenu, onAction: (action, event) => this.handleContextMenuAction(action, event), onBeforeShow: (event) => this.handleBeforeShowContextMenu(event) }); } handleBeforeShowContextMenu(event) { // 在显示右键菜单前,检查是否点击在文件项上 const fileItem = event.target.closest('.file-item'); // console.log('[Disk] 右键菜单即将显示'); // console.log('[Disk] 点击目标:', event.target); // console.log('[Disk] 最近的文件项:', fileItem); if (fileItem) { // 如果点击在文件项上,确保它被选中 const isSelected = this.selection.isSelected(fileItem.dataset.path); // console.log('[Disk] 文件项路径:', fileItem.dataset.path); // console.log('[Disk] 是否已选中:', isSelected); if (!isSelected) { // console.log('[Disk] → 自动选中该文件项'); this.selection.selectOnly(fileItem); // console.log('[Disk] ✓ 文件项已选中'); } } else { // console.log('[Disk] ⚠ 右键点击在空白区域'); } } showUploadProgress() { if (this.uploadProgress) { this.uploadProgress.classList.add('show'); } } hideUploadProgress() { if (this.uploadProgress) { this.uploadProgress.classList.remove('show'); } if (this.uploadProgressList) { this.uploadProgressList.innerHTML = ''; } } showGlobalLoading(text = '正在处理...') { // 直接调用父窗口的 GlobalLoading(不通过 postMessage) try { // 优先使用父窗口的 GlobalLoading if (window.parent && window.parent !== window && window.parent.GlobalLoading) { window.parent.GlobalLoading.show(text); return; } // 如果父窗口没有 GlobalLoading,尝试更上层的窗口 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { if (targetWindow.GlobalLoading) { targetWindow.GlobalLoading.show(text); return; } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } } catch (error) { console.error('[Disk] 显示 loading 失败:', error); } } updateGlobalLoadingProgress(current, total, operation = '处理') { // 更新loading进度 const text = `正在${operation} ${current}/${total} 张图片...`; this.showGlobalLoading(text); } hideGlobalLoading() { // 直接调用父窗口的 GlobalLoading(不通过 postMessage) try { // 优先使用父窗口的 GlobalLoading if (window.parent && window.parent !== window && window.parent.GlobalLoading) { window.parent.GlobalLoading.hide(); return; } // 如果父窗口没有 GlobalLoading,尝试更上层的窗口 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { if (targetWindow.GlobalLoading) { targetWindow.GlobalLoading.hide(); return; } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } } catch (error) { console.error('[Disk] 隐藏 loading 失败:', error); } } showGlobalAlert(message) { // 直接调用父窗口的 GlobalAlert(不通过 postMessage) try { // 优先使用父窗口的 GlobalAlert(disk 在 assets.html 的 iframe 中) if (window.parent && window.parent !== window && window.parent.GlobalAlert) { window.parent.GlobalAlert.show(message, 3000); return; } // 如果父窗口没有 GlobalAlert,尝试更上层的窗口(index.html) let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { if (targetWindow.GlobalAlert) { targetWindow.GlobalAlert.show(message, 3000); return; } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } // 降级处理:如果没有 GlobalAlert,使用 console console.log('[Alert]', message); } catch (error) { console.error('[Disk] 显示 alert 失败:', error); console.log('[Alert]', message); } } // 统一的错误处理函数:解析服务端错误响应并显示 async handleServerError(response, defaultMessage = '操作失败') { let errorMessage = defaultMessage; try { // 尝试解析 JSON 错误响应 const errorData = await response.json().catch(() => null); if (errorData) { // 优先使用服务端返回的 message if (errorData.message) { errorMessage = errorData.message; } else if (errorData.error) { errorMessage = errorData.error; } else if (typeof errorData === 'string') { errorMessage = errorData; } } } catch (e) { // 如果解析失败,使用默认消息或状态码 if (response.status) { errorMessage = `${defaultMessage} (状态码: ${response.status})`; } } this.showGlobalAlert(errorMessage); return errorMessage; } showGlobalConfirm(message) { // 直接调用父窗口的 GlobalConfirm(不通过 postMessage) return new Promise((resolve) => { try { // 优先使用父窗口的 GlobalConfirm if (window.parent && window.parent !== window && window.parent.GlobalConfirm) { window.parent.GlobalConfirm.show(message).then((confirmed) => { resolve(confirmed); }).catch(() => { resolve(false); }); return; } // 如果父窗口没有 GlobalConfirm,尝试更上层的窗口 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { if (targetWindow.GlobalConfirm) { targetWindow.GlobalConfirm.show(message).then((confirmed) => { resolve(confirmed); }).catch(() => { resolve(false); }); return; } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } // 如果找不到 GlobalConfirm,直接返回 false(不降级到原生 confirm) console.error('[Disk] 无法找到 GlobalConfirm,操作已取消'); resolve(false); } catch (error) { console.error('[Disk] 显示 confirm 失败:', error); // 不降级到原生 confirm,直接返回 false resolve(false); } }); } handleContextMenuAction(action) { console.log(`[Disk] 右键菜单动作: ${action}`); switch (action) { case 'new': this.createFolder(); break; case 'cut': this.cutSelected(); break; case 'copy': this.copySelected(); break; case 'paste': this.pasteClipboard(); break; case 'remove-bg': console.log('[Disk] 触发一键抠背景功能'); this.removeBackgroundFromSelected(); break; case 'crop-mini': console.log('[Disk] 触发剪裁最小区域功能'); this.cropMiniFromSelected(); break; default: console.warn('[Disk] 未知的菜单动作:', action); break; } } initShortcutKeys() { if (this.shortcutKeys) { this.shortcutKeys.destroy(); } this.shortcutKeys = new ShortcutKeys({ selection: this.selection, onDelete: () => this.deleteSelected(), onRename: () => this.renameSelected(), onCopy: () => this.copySelected(), onCut: () => this.cutSelected(), onPaste: () => this.pasteClipboard() }); } initSearchBar() { this.searchBar = new SearchBar({ input: this.searchInput, clearButton: this.searchClear, fileList: this.fileList, emptyState: this.emptyState, getFiles: () => this.files, renderAll: () => this.renderFiles(), createFileItem: (file) => this.createFileItem(file) }); } bindEvents() { // 拖拽上传事件 this.dropZone.addEventListener('dragenter', (e) => this.handleDragEnter(e)); this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e)); this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e)); this.dropZone.addEventListener('drop', (e) => this.handleDrop(e)); document.addEventListener('dragend', () => this.resetDropState()); // 按钮事件 if (this.fileInput) { this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); } if (this.btnCreateFolder) { this.btnCreateFolder.addEventListener('click', () => this.createFolder()); } // 选择操作按钮 this.btnDownload.addEventListener('click', () => this.downloadSelected()); this.btnDelete.addEventListener('click', () => this.deleteSelected()); // 上传按钮 if (this.btnUpload) { this.btnUpload.addEventListener('click', () => { if (this.fileInput) { const currentPath = this.pathNav.getPath(); const isRootDir = !currentPath || currentPath === ''; // 根目录只允许上传文件夹 if (isRootDir) { this.fileInput.setAttribute('webkitdirectory', ''); this.fileInput.removeAttribute('accept'); } else { this.fileInput.removeAttribute('webkitdirectory'); this.fileInput.setAttribute('accept', 'image/*'); } this.fileInput.click(); } }); } } // 下载选中的文件 downloadSelected() { const selectedPaths = this.selection.getSelectedItems(); selectedPaths.forEach(filePath => { const file = this.files.find(f => f.path === filePath); if (file && file.type !== 'directory') { this.downloadFile(filePath); } }); } // 删除选中的文件 async deleteSelected() { const count = this.selection.getSelectedCount(); if (count === 0) return; const confirmMsg = `确定要删除选中的 ${count} 个文件/文件夹吗?`; const confirmed = await this.showGlobalConfirm(confirmMsg); if (!confirmed) { return; } try { const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } const response = await fetch('/api/disk/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, paths: this.selection.getSelectedItems() }) }); if (!response.ok) { await this.handleServerError(response, '删除失败'); return; } const data = await response.json(); if (data.success) { this.selection.clearSelection(); this.loadFiles(); } else { this.showGlobalAlert('删除失败: ' + (data.message || '未知错误')); } } catch (error) { this.showGlobalAlert('删除失败: ' + (error.message || '网络错误')); } } renameSelected() { if (!this.selection || this.selection.getSelectedCount() !== 1) { return; } const selectedPath = this.selection.getSelectedItems()[0]; const selectedItem = this.fileList.querySelector(`[data-path="${selectedPath}"]`); if (selectedItem) { this.startRename(selectedItem); } } copySelected() { if (!this.selection || !this.selection.hasSelection()) { return false; } const selectedPaths = this.selection.getSelectedItems(); this.clearCutVisuals(); this.clipboard = { mode: 'copy', items: selectedPaths.map(path => ({ path })) }; return true; } cutSelected() { if (!this.selection || !this.selection.hasSelection()) { return false; } const selectedPaths = this.selection.getSelectedItems(); this.clipboard = { mode: 'cut', items: selectedPaths.map(path => ({ path })) }; this.applyCutVisuals(selectedPaths); return true; } async pasteClipboard() { if (!this.clipboard || !this.clipboard.items || this.clipboard.items.length === 0) { return false; } const targetFolder = this.pathNav.getPath(); const isCut = this.clipboard.mode === 'cut'; let hasSuccess = false; for (const item of this.clipboard.items) { let result = false; if (isCut) { result = await this.moveFile(item.path, targetFolder, { suppressReload: true }); } else { result = await this.copyFile(item.path, targetFolder, { suppressReload: true }); } hasSuccess = hasSuccess || result; } if (hasSuccess) { await this.loadFiles(); } if (isCut) { this.clipboard = null; this.clearCutVisuals(); } return hasSuccess; } async removeBackgroundFromSelected() { console.log('\n' + '='.repeat(70)); console.log('[Disk] 🎨 一键抠图功能被触发'); console.log('='.repeat(70)); const selectedPaths = this.selection.getSelectedItems(); console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`); if (selectedPaths.length === 0) { console.warn('[Disk] ⚠ 没有选中任何项'); this.showGlobalAlert('请先选择要抠图的文件夹'); return; } // getSelectedItems() 返回的已经是路径数组 console.log('[Disk] 选中的路径:', selectedPaths); // 确认操作 const confirmMsg = selectedPaths.length === 1 ? `一键抠背景会覆盖原文件,确定要对"${selectedPaths[0]}"进行处理吗?` : `一键抠背景会覆盖原文件,确定要对选中的 ${selectedPaths.length} 个文件夹进行处理吗?`; console.log('[Disk] → 准备显示确认对话框...'); console.log('[Disk] 确认消息:', confirmMsg); // 通过父窗口显示全局确认对话框 console.log('[Disk] → 调用 showGlobalConfirm()'); const confirmed = await this.showGlobalConfirm(confirmMsg); console.log('[Disk] ← showGlobalConfirm() 返回,结果:', confirmed); if (!confirmed) { console.log('[Disk] ✗ 用户取消操作,函数返回'); return; } console.log('[Disk] ✓ 用户确认操作,继续执行'); // 计算总图片数 let totalPngCount = 0; selectedPaths.forEach(path => { const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path); console.log('[Disk] 检查文件:', path, '信息:', fileInfo); if (fileInfo && fileInfo.pngCount) { console.log('[Disk] pngCount:', fileInfo.pngCount); totalPngCount += fileInfo.pngCount; } }); console.log('[Disk] 总PNG数量:', totalPngCount); // 显示loading(带总数信息) console.log('[Disk] → 显示全局Loading...'); const loadingText = totalPngCount > 0 ? `正在处理 0/${totalPngCount} 张图片...` : '正在处理中,请稍候...'; this.showGlobalLoading(loadingText); console.log('[Disk] ✓ Loading已显示'); // 保存总数,用于更新进度 this.currentProcessTotal = totalPngCount; try { console.log('[Disk] → 准备发送HTTP请求到服务器'); console.log('[Disk] URL: /api/disk/remove-background'); console.log('[Disk] 方法: POST'); console.log('[Disk] 数据:', { paths: selectedPaths }); const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } // 使用fetch读取SSE流 const response = await fetch('/api/disk/remove-background', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, paths: selectedPaths }) }); console.log('[Disk] ✓ HTTP响应收到'); console.log('[Disk] 状态码:', response.status); if (!response.ok) { this.hideGlobalLoading(); await this.handleServerError(response, '处理失败'); return; } // 读取SSE流 console.log('[Disk] → 开始读取SSE流...'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let finalResult = null; while (true) { const { done, value } = await reader.read(); if (done) { console.log('[Disk] ✓ SSE流读取完成'); break; } const chunk = decoder.decode(value, { stream: true }); console.log('[Disk] ← 收到数据块:', chunk.substring(0, 100)); buffer += chunk; const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.substring(6); try { const data = JSON.parse(jsonStr); console.log('[Disk] ← 解析到事件:', data); if (data.type === 'image-progress') { // 更新进度 console.log(`[Disk] → 更新进度: ${data.current}/${data.total}`); this.updateGlobalLoadingProgress(data.current, data.total, '处理'); } else if (data.type === 'complete') { console.log('[Disk] ← 收到完成事件'); finalResult = data; } else if (data.type === 'error') { console.error('[Disk] ← 收到错误事件'); throw new Error(data.message); } } catch (e) { console.error('[Disk] 解析SSE数据失败:', e, '原始数据:', jsonStr); } } } } console.log('[Disk] ✓ 最终结果:', finalResult); if (finalResult && finalResult.success) { console.log('[Disk] ✓✓✓ 服务器处理成功!'); console.log('[Disk] 处理文件夹数:', finalResult.folders || 0); console.log('[Disk] 处理图片数:', finalResult.processed || 0); console.log('[Disk] → 隐藏Loading...'); this.hideGlobalLoading(); console.log('[Disk] ✓ Loading已隐藏'); const message = `处理完成!\n共处理 ${finalResult.processed || 0} 张图片`; console.log('[Disk] → 显示成功提示'); this.showGlobalAlert(message); console.log('[Disk] → 清除图片缓存...'); await this.clearImageCache(selectedPaths); console.log('[Disk] ✓ 缓存已清除'); console.log('[Disk] → 刷新文件列表以更新预览图...'); await this.loadFiles(); console.log('[Disk] ✓ 文件列表已刷新'); console.log('='.repeat(70)); console.log('[Disk] 🎉🎉🎉 一键抠图完成!'); console.log('='.repeat(70) + '\n'); } else { throw new Error(finalResult?.message || '处理失败'); } } catch (error) { console.error('\n' + '='.repeat(70)); console.error('[Disk] ✗✗✗ 客户端处理失败'); console.error('[Disk] 错误信息:', error.message); console.error('[Disk] 错误堆栈:', error.stack); console.error('='.repeat(70) + '\n'); this.hideGlobalLoading(); this.showGlobalAlert('处理失败: ' + error.message); } } async cropMiniFromSelected() { console.log('[Disk] 剪裁最小区域功能被触发'); const selectedPaths = this.selection.getSelectedItems(); console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`); if (selectedPaths.length === 0) { this.showGlobalAlert('请先选择要剪裁的文件夹'); return; } // 计算总图片数 let totalPngCount = 0; selectedPaths.forEach(path => { const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path); if (fileInfo && fileInfo.pngCount) { totalPngCount += fileInfo.pngCount; } }); // 确认操作 const confirmMsg = selectedPaths.length === 1 ? `确定要对"${selectedPaths[0]}"进行剪裁吗?` : `确定要对选中的 ${selectedPaths.length} 个文件夹进行剪裁吗?`; const confirmed = await this.showGlobalConfirm(confirmMsg); if (!confirmed) { return; } // 显示loading const loadingText = totalPngCount > 0 ? `正在剪裁 0/${totalPngCount} 张图片...` : '正在剪裁中,请稍候...'; this.showGlobalLoading(loadingText); // 保存总数,用于更新进度 this.currentProcessTotal = totalPngCount; try { const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } // 使用fetch读取SSE流 const response = await fetch('/api/disk/crop-mini', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, paths: selectedPaths }) }); if (!response.ok) { this.hideGlobalLoading(); await this.handleServerError(response, '剪裁失败'); return; } // 读取SSE流 const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let finalResult = null; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.substring(6); try { const data = JSON.parse(jsonStr); if (data.type === 'image-progress') { this.updateGlobalLoadingProgress(data.current, data.total, '剪裁'); } else if (data.type === 'complete') { finalResult = data; } else if (data.type === 'error') { throw new Error(data.message); } } catch (e) { console.error('[Disk] 解析SSE数据失败:', e); } } } } if (finalResult && finalResult.success) { this.hideGlobalLoading(); const message = `剪裁完成!\n共处理 ${finalResult.processed || 0} 张图片`; this.showGlobalAlert(message); // 清除缓存并刷新 await this.clearImageCache(selectedPaths); await this.loadFiles(); } else { throw new Error(finalResult?.message || '剪裁失败'); } } catch (error) { console.error('[Disk] 剪裁失败:', error); this.hideGlobalLoading(); this.showGlobalAlert('剪裁失败: ' + error.message); } } applyCutVisuals(paths = []) { this.clearCutVisuals(); paths.forEach(path => { const item = this.fileList.querySelector(`[data-path="${path}"]`); if (item) { item.classList.add('cut'); } }); } clearCutVisuals() { this.fileList.querySelectorAll('.file-item.cut').forEach(item => item.classList.remove('cut')); } clearSearch(options) { if (this.searchBar) { this.searchBar.clear(options); } } // 检查是否是外部文件拖入(而非内部拖拽操作) isExternalFileDrag(e) { return e.dataTransfer.types.includes('Files') && !e.dataTransfer.types.includes('text/plain'); } // 拖拽处理 handleDragEnter(e) { e.preventDefault(); e.stopPropagation(); if (this.isExternalFileDrag(e)) { this.dropZone.classList.add('drag-over'); this.dragCounter++; } } handleDragOver(e) { e.preventDefault(); e.stopPropagation(); } handleDragLeave(e) { e.preventDefault(); e.stopPropagation(); if (this.isExternalFileDrag(e)) { this.dragCounter = Math.max(0, this.dragCounter - 1); if (this.dragCounter === 0) { this.dropZone.classList.remove('drag-over'); } } } async handleDrop(e) { e.preventDefault(); e.stopPropagation(); this.dropZone.classList.remove('drag-over'); this.dragCounter = 0; const items = e.dataTransfer.items; if (!items) return; const entries = []; for (let i = 0; i < items.length; i++) { const item = items[i].webkitGetAsEntry(); if (item) { entries.push(item); } } await this.processEntries(entries); } resetDropState() { this.dragCounter = 0; this.dropZone.classList.remove('drag-over'); } async processEntries(entries) { const currentPath = this.pathNav.getPath(); const isRootDir = !currentPath || currentPath === ''; // 检查是否在根目录上传文件(非文件夹) if (isRootDir) { const hasFiles = entries.some(entry => entry.isFile); if (hasFiles) { this.showGlobalAlert('根目录只允许上传文件夹'); return; } } const filesToUpload = []; for (const entry of entries) { await this.traverseEntry(entry, '', filesToUpload); } if (filesToUpload.length > 0) { // 显示确认对话框 const folderName = entries.length === 1 && entries[0].isDirectory ? entries[0].name : '选中的文件'; const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`; const confirmed = await this.showGlobalConfirm(confirmMsg); if (!confirmed) { return; } await this.uploadFiles(filesToUpload); } } async traverseEntry(entry, relativePath, filesToUpload) { if (entry.isFile) { const file = await new Promise((resolve) => { entry.file(resolve); }); // 只接受图片格式的文件 if (this.isImageFile(file.name)) { filesToUpload.push({ file: file, path: relativePath + file.name }); } // 非图片格式的文件直接跳过 } else if (entry.isDirectory) { const dirReader = entry.createReader(); const entries = await new Promise((resolve) => { dirReader.readEntries(resolve); }); for (const childEntry of entries) { await this.traverseEntry( childEntry, relativePath + entry.name + '/', filesToUpload ); } } } async handleFileSelect(e) { const currentPath = this.pathNav.getPath(); const isRootDir = !currentPath || currentPath === ''; const files = Array.from(e.target.files); if (files.length === 0) { this.fileInput.value = ''; return; } // 检查登录状态 const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); this.fileInput.value = ''; return; } // 只接受图片格式的文件,其他格式直接跳过 const filesToUpload = files .filter(file => this.isImageFile(file.name)) .map(file => ({ file: file, path: file.webkitRelativePath || file.name })); if (filesToUpload.length > 0) { // 显示确认对话框 const folderName = files[0].webkitRelativePath ? files[0].webkitRelativePath.split('/')[0] : '选中的文件'; const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`; const confirmed = await this.showGlobalConfirm(confirmMsg); if (!confirmed) { this.fileInput.value = ''; return; } await this.uploadFiles(filesToUpload); } this.fileInput.value = ''; } async uploadFiles(filesToUpload) { if (!filesToUpload.length) return; // 显示全局loading遮罩 this.showGlobalLoading(`正在上传 ${filesToUpload.length} 个文件...`); let hasSuccess = false; for (let i = 0; i < filesToUpload.length; i++) { const fileData = filesToUpload[i]; // 更新进度文本 this.showGlobalLoading(`正在上传... (${i + 1}/${filesToUpload.length})`); try { await this.uploadFile(fileData, null); hasSuccess = true; // 每上传成功一个文件就立即刷新显示(静默刷新,不显示加载动画) await this.loadFilesQuietly(); } catch (error) { console.error('上传失败:', error); } } // 所有文件处理完成后,隐藏loading this.hideGlobalLoading(); if (hasSuccess) { // 上传成功后刷新文件列表 await this.loadFiles(); } } async uploadFile(fileData, progressItem) { const username = this.getCurrentUsername(); if (!username) { throw new Error('请先登录'); } const formData = new FormData(); formData.append('file', fileData.file); formData.append('path', this.pathNav.getPath()); formData.append('relativePath', fileData.path); formData.append('username', username); const response = await fetch('/api/disk/upload', { method: 'POST', body: formData }); if (!response.ok) { throw new Error('上传失败'); } return response.json(); } createProgressItem(fileName) { if (!this.uploadProgressTemplate || !this.uploadProgressList) { return null; } const fragment = this.uploadProgressTemplate.content.cloneNode(true); const item = fragment.querySelector('.upload-progress-item'); if (!item) { return null; } const nameEl = item.querySelector('.upload-progress-name'); if (nameEl) { nameEl.textContent = fileName; } this.uploadProgressList.appendChild(fragment); return this.uploadProgressList.lastElementChild; } updateProgressItem(item, status, message) { if (!item) return; const statusEl = item.querySelector('.upload-progress-status'); const barFill = item.querySelector('.upload-progress-bar-fill'); if (statusEl) { statusEl.textContent = message; } if (!barFill) return; if (status === 'success') { barFill.style.width = '100%'; barFill.style.background = '#52c41a'; } else if (status === 'error') { barFill.style.background = '#ff4d4f'; } } // 初始化文件结构缓存(递归加载所有文件夹信息) async initFileStructureCache() { if (this.cacheInitialized) { return; } try { // console.log('[Disk] 开始初始化文件结构缓存...'); const username = this.getCurrentUsername(); if (!username) { console.warn('[Disk] 未登录,无法初始化缓存'); return; } const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=&recursive=true`); if (!response.ok) { await this.handleServerError(response, '加载文件列表失败'); return; } const data = await response.json(); if (data.success && data.files) { // 构建缓存 Map this.fileStructureCache.clear(); data.files.forEach(file => { this.fileStructureCache.set(file.path, file); }); this.cacheInitialized = true; // console.log(`[Disk] 文件结构缓存初始化完成,共 ${this.fileStructureCache.size} 个项目`); } else if (!data.success && data.message) { this.showGlobalAlert('加载文件列表失败: ' + data.message); } } catch (error) { this.showGlobalAlert('加载文件列表失败: ' + (error.message || '网络错误')); } } // 从缓存中获取文件信息 getFileFromCache(filePath) { return this.fileStructureCache.get(filePath); } // 更新缓存中的单个文件信息 updateCacheItem(filePath, fileInfo) { this.fileStructureCache.set(filePath, fileInfo); } // 从缓存中移除文件信息 removeCacheItem(filePath) { this.fileStructureCache.delete(filePath); } // 清除图片缓存(用于抠图后更新预览) async clearImageCache(folderPaths) { try { // 清除浏览器的图片缓存 if ('caches' in window) { const cacheNames = await caches.keys(); for (const cacheName of cacheNames) { const cache = await caches.open(cacheName); const requests = await cache.keys(); for (const request of requests) { const url = request.url; // 检查是否是处理过的文件夹的图片 for (const folderPath of folderPaths) { if (url.includes(encodeURIComponent(folderPath))) { await cache.delete(request); console.log('[Disk] 清除缓存:', url); } } } } } } catch (error) { console.error('[Disk] 清除缓存失败:', error); } } // 加载文件列表 async loadFiles() { this.showLoading(true); this.selection.clearSelection(); this.clearSearch(); try { const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); this.showLoading(false); return; } const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`); if (!response.ok) { await this.handleServerError(response, '加载文件列表失败'); return; } const data = await response.json(); if (data.success) { this.files = data.files; // 更新缓存 data.files.forEach(file => { this.updateCacheItem(file.path, file); }); this.renderFiles(); } else if (data.message) { this.showGlobalAlert('加载文件列表失败: ' + data.message); } } catch (error) { this.showGlobalAlert('加载文件列表失败: ' + (error.message || '网络错误')); } finally { this.showLoading(false); } } // 静默加载文件列表(用于上传过程中增量更新,不显示加载动画,不闪烁) async loadFilesQuietly() { try { const username = this.getCurrentUsername(); if (!username) { return; } const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`); if (!response.ok) { // 静默加载失败时不显示错误,避免干扰用户 return; } const data = await response.json(); if (data.success) { this.files = data.files; this.renderFilesSmooth(); } } catch (error) { console.error('加载文件列表失败:', error); } } renderFiles() { this.fileList.innerHTML = ''; if (this.files.length === 0) { this.emptyState.classList.add('show'); return; } this.emptyState.classList.remove('show'); this.files.forEach(file => { const fileItem = this.createFileItem(file); this.fileList.appendChild(fileItem); }); } // 平滑渲染文件列表(增量更新,避免闪烁) renderFilesSmooth() { if (this.files.length === 0) { this.emptyState.classList.add('show'); this.fileList.innerHTML = ''; return; } this.emptyState.classList.remove('show'); // 获取当前已存在的文件路径 const existingPaths = new Set(); const existingItems = this.fileList.querySelectorAll('.file-item'); existingItems.forEach(item => { existingPaths.add(item.dataset.path); }); // 创建新文件路径集合 const newPaths = new Set(this.files.map(f => f.path)); // 删除不存在的文件项 existingItems.forEach(item => { if (!newPaths.has(item.dataset.path)) { item.remove(); } }); // 只添加新文件项 this.files.forEach(file => { if (!existingPaths.has(file.path)) { const fileItem = this.createFileItem(file); this.fileList.appendChild(fileItem); } }); } createFileItem(file) { const div = document.createElement('div'); div.className = 'file-item'; div.dataset.name = file.name; div.dataset.type = file.type; div.dataset.path = file.path; div.draggable = true; const size = file.type === 'directory' ? '' : this.formatFileSize(file.size); const isImage = this.isImageFile(file.name); // 勾选标记 const checkMark = `
`; // 检查是否是包含PNG的文件夹(通过名称模式判断) const isAnimationFolder = file.type === 'directory' && this.isAnimationFolder(file.name); if (isAnimationFolder) { // 动画文件夹显示第一帧预览 // 使用服务器提供的预览信息(完全避免404错误) if (file.hasPreview && file.previewUrl) { // 服务器确认有预览图,直接使用服务器提供的URL // 确保 URL 包含用户名参数(如果服务器没有提供) const username = this.getCurrentUsername(); let previewUrl = file.previewUrl; if (username && !previewUrl.includes('username=')) { // 如果 URL 中没有用户名参数,添加它 const separator = previewUrl.includes('?') ? '&' : '?'; previewUrl = previewUrl + separator + 'username=' + encodeURIComponent(username); } // 添加时间戳防止缓存 previewUrl = previewUrl + (previewUrl.includes('?') ? '&' : '?') + 't=' + Date.now(); // console.log(`[Disk] 文件夹 ${file.name} 有预览图`); div.innerHTML = ` ${checkMark}
${file.name}
📁
${file.name}
`; } else { // 服务器确认没有预览图,直接显示文件夹图标 // console.log(`[Disk] 文件夹 ${file.name} 无预览图,显示图标`); const icon = this.getFileIcon(file); div.innerHTML = ` ${checkMark}
${icon}
${file.name}
`; } } else if (isImage && file.type !== 'directory') { // 添加时间戳防止缓存 const username = this.getCurrentUsername(); const previewUrl = username ? `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}&t=${Date.now()}` : `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`; div.innerHTML = ` ${checkMark}
${file.name}
${file.name}
${size ? `
${size}
` : ''} `; } else { const icon = this.getFileIcon(file); div.innerHTML = ` ${checkMark}
${icon}
${file.name}
${size ? `
${size}
` : ''} `; } // 用于区分单击和双击的定时器 let clickTimer = null; // 单击选中(Windows 11 行为) div.addEventListener('click', (e) => { // 如果正在重命名,不处理 if (e.target.classList.contains('rename-input')) { return; } e.stopPropagation(); const isAlreadySelected = this.selection.isSelected(file.path); const clickedOnName = e.target.classList.contains('file-name'); const ctrlKey = e.ctrlKey; // 清除之前的定时器 if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } // 延迟执行单击操作,给双击留出时间 clickTimer = setTimeout(() => { clickTimer = null; if (ctrlKey) { // Ctrl+点击:切换选中状态(多选) this.selection.toggleSelection(div); } else if (clickedOnName && isAlreadySelected) { // 点击已选中项的名称:触发重命名 this.startRename(div); } else { // 普通点击:选中当前项,取消其他选择 this.selection.selectOnly(div); } }, 200); }); // 双击进入文件夹或打开文件(整个项目都可以双击) div.addEventListener('dblclick', (e) => { // 如果正在重命名,不处理 if (e.target.classList.contains('rename-input')) { return; } e.stopPropagation(); // 取消单击的定时器,防止双击时执行单击操作 if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } if (file.type === 'directory') { this.navigateToPath(file.path); } else { this.downloadFile(file.path); } }); // 拖拽开始 div.addEventListener('dragstart', (e) => { e.stopPropagation(); div.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', JSON.stringify({ type: file.type, // 'directory' 或 'file' path: file.path, name: file.name, isDirectory: file.type === 'directory', pngCount: file.pngCount || 0, // PNG文件数量 hasPreview: file.hasPreview || false, // 是否有预览图 needsMatting: file.needsMatting || false // 是否需要抠图 })); }); div.addEventListener('dragend', () => { div.classList.remove('dragging'); document.querySelectorAll('.file-item.drag-target').forEach(el => { el.classList.remove('drag-target'); }); }); // 文件夹可以接收拖拽 if (file.type === 'directory') { div.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); const draggingEl = document.querySelector('.file-item.dragging'); if (draggingEl && draggingEl !== div) { div.classList.add('drag-target'); e.dataTransfer.dropEffect = 'move'; } }); div.addEventListener('dragleave', (e) => { e.stopPropagation(); div.classList.remove('drag-target'); }); div.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); div.classList.remove('drag-target'); try { const data = JSON.parse(e.dataTransfer.getData('text/plain')); if (data.type === 'move-file' && data.path !== file.path) { await this.moveFile(data.path, file.path); } } catch (error) { // 可能是外部文件拖拽,忽略 } }); } return div; } // 移动文件/文件夹 async moveFile(sourcePath, targetFolder, options = {}) { const { suppressReload = false, silent = false } = options; const username = this.getCurrentUsername(); if (!username) { if (!silent) { this.showGlobalAlert('请先登录'); } return; } try { const response = await fetch('/api/disk/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, sourcePath, targetFolder }) }); if (!response.ok) { if (!silent) { await this.handleServerError(response, '移动失败'); } return false; } const data = await response.json(); if (data.success) { if (!suppressReload) { await this.loadFiles(); } return true; } else { if (!silent) { this.showGlobalAlert('移动失败: ' + (data.message || '未知错误')); } return false; } } catch (error) { if (!silent) { this.showGlobalAlert('移动失败: ' + (error.message || '网络错误')); } return false; } } getFileIcon(file) { if (file.type === 'directory') { return ` `; } const ext = file.name.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) { return ` `; } if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) { return ` `; } if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) { return ` `; } if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) { return ` `; } if (['doc', 'docx', 'txt', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) { return ` `; } return ` `; } formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } isImageFile(fileName) { const ext = fileName.split('.').pop().toLowerCase(); return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext); } isAnimationFolder(folderName) { // 判断是否是动画文件夹(通过名称模式) // 放宽匹配规则,让更多文件夹能显示预览图 return /^player_\d+$/i.test(folderName) || /^(idle|walk|run|attack|死亡|站立|行走|攻击)/i.test(folderName) || /动画|animation|ani|sprite|序列|sequence/i.test(folderName) || // 新增:所有文件夹都尝试显示预览图(如果有图片就显示) true; // 默认返回true,让所有文件夹都尝试加载预览图 } // 注意:guessFirstFrameName、tryAlternativePreview 和 showFolderIcon 方法已废弃 // 现在使用服务器端提供的预览信息,完全避免客户端的404错误 // 导航到指定路径 navigateToPath(path) { this.pathNav.navigateTo(path); } // 创建文件夹 async createFolder() { let folderName = '新建文件夹'; let counter = 1; while (this.files.some(f => f.name === folderName && f.type === 'directory')) { folderName = `新建文件夹 (${counter})`; counter++; } const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } try { const response = await fetch('/api/disk/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, path: this.pathNav.getPath(), name: folderName }) }); if (!response.ok) { await this.handleServerError(response, '创建文件夹失败'); return; } const data = await response.json(); if (data.success) { await this.loadFiles(); const newFolderItem = this.fileList.querySelector(`[data-name="${folderName}"]`); if (newFolderItem) { this.startRename(newFolderItem); } } else { this.showGlobalAlert('创建文件夹失败: ' + (data.message || '未知错误')); } } catch (error) { this.showGlobalAlert('创建文件夹失败: ' + (error.message || '网络错误')); } } // 开始重命名(参考 Windows 行为) startRename(fileItem) { const nameEl = fileItem.querySelector('.file-name'); const input = fileItem.querySelector('.rename-input'); if (!nameEl || !input) return; const oldName = fileItem.dataset.name; const filePath = fileItem.dataset.path; // 标记当前正在重命名 this._isRenaming = true; // 显示输入框,隐藏文件名 input.value = oldName; nameEl.style.display = 'none'; input.style.display = ''; // 选中文件名(不含扩展名,文件夹则全选) setTimeout(() => { const dotIndex = oldName.lastIndexOf('.'); if (dotIndex > 0 && fileItem.dataset.type !== 'directory') { input.setSelectionRange(0, dotIndex); } else { input.select(); } input.focus(); }, 10); // 状态标记 let finished = false; const exitRename = () => { this._isRenaming = false; input.style.display = 'none'; nameEl.style.display = ''; }; const commitRename = async () => { if (finished) return; finished = true; const newName = input.value.trim(); exitRename(); // 名字有效且有变化才提交 if (newName && newName !== oldName) { await this.renameFile(filePath, newName); } }; const cancelRename = () => { if (finished) return; finished = true; exitRename(); }; // 使用 addEventListener 确保事件被捕获 const handleBlur = () => { input.removeEventListener('blur', handleBlur); input.removeEventListener('keydown', handleKeydown); if (!finished) { commitRename(); } }; const handleKeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); input.removeEventListener('blur', handleBlur); input.removeEventListener('keydown', handleKeydown); commitRename(); } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); input.removeEventListener('blur', handleBlur); input.removeEventListener('keydown', handleKeydown); cancelRename(); } }; input.addEventListener('blur', handleBlur); input.addEventListener('keydown', handleKeydown); } // 执行重命名 async renameFile(oldPath, newName) { const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } try { const response = await fetch('/api/disk/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, oldPath: oldPath, newName: newName }) }); const data = await response.json(); if (data.success) { this.loadFiles(); } else { this.showGlobalAlert('重命名失败: ' + data.message); this.loadFiles(); } } catch (error) { console.error('重命名失败:', error); this.showGlobalAlert('重命名失败'); this.loadFiles(); } } // 下载文件 downloadFile(filePath) { const username = this.getCurrentUsername(); if (!username) { this.showGlobalAlert('请先登录'); return; } window.open(`/api/disk/download?username=${encodeURIComponent(username)}&path=${encodeURIComponent(filePath)}`, '_blank'); } async copyFile(sourcePath, targetFolder, options = {}) { const { suppressReload = false, silent = false } = options; const username = this.getCurrentUsername(); if (!username) { if (!silent) { this.showGlobalAlert('请先登录'); } return; } try { const response = await fetch('/api/disk/copy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, sourcePath, targetFolder }) }); if (!response.ok) { if (!silent) { await this.handleServerError(response, '复制失败'); } return false; } const data = await response.json(); if (data.success) { if (!suppressReload) { await this.loadFiles(); } return true; } else { if (!silent) { this.showGlobalAlert('复制失败: ' + (data.message || '未知错误')); } return false; } } catch (error) { if (!silent) { this.showGlobalAlert('复制失败: ' + (error.message || '网络错误')); } return false; } } showLoading(show) { if (show) { this.loading.classList.add('show'); } else { this.loading.classList.remove('show'); } } } // 初始化 document.addEventListener('DOMContentLoaded', () => { window.diskManager = new DiskManager(); });