|
|
@@ -0,0 +1,1712 @@
|
|
|
+// 网盘管理系统 - 客户端逻辑
|
|
|
+// 版本: 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.init();
|
|
|
+ }
|
|
|
+
|
|
|
+ async init() {
|
|
|
+ await this.ensureToolBar();
|
|
|
+ await this.ensureContextMenu();
|
|
|
+ this.initElements();
|
|
|
+ this.initUploadProgress();
|
|
|
+ this.initContextMenu();
|
|
|
+ this.initPath();
|
|
|
+ this.initSelection();
|
|
|
+ this.initShortcutKeys();
|
|
|
+ this.initSearchBar();
|
|
|
+ this.bindEvents();
|
|
|
+ this.loadFiles();
|
|
|
+
|
|
|
+ // 后台初始化文件结构缓存(不阻塞页面加载)
|
|
|
+ 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 = '正在处理...') {
|
|
|
+ // 通过postMessage通知父页面显示loading
|
|
|
+ console.log('[Disk] 显示全局Loading:', text);
|
|
|
+ console.log('[Disk] 当前window.parent:', window.parent !== window ? '存在' : '不存在');
|
|
|
+
|
|
|
+ if (window.parent && window.parent !== window) {
|
|
|
+ console.log('[Disk] 发送global-loading消息到父窗口');
|
|
|
+ window.parent.postMessage({
|
|
|
+ type: 'global-loading',
|
|
|
+ action: 'show',
|
|
|
+ text: text
|
|
|
+ }, '*');
|
|
|
+ } else {
|
|
|
+ console.warn('[Disk] 无法找到父窗口');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updateGlobalLoadingProgress(current, total, operation = '处理') {
|
|
|
+ // 更新loading进度
|
|
|
+ const text = `正在${operation} ${current}/${total} 张图片...`;
|
|
|
+ this.showGlobalLoading(text);
|
|
|
+ }
|
|
|
+
|
|
|
+ hideGlobalLoading() {
|
|
|
+ // 通过postMessage通知父页面隐藏loading
|
|
|
+ console.log('[Disk] 隐藏全局Loading');
|
|
|
+ if (window.parent && window.parent !== window) {
|
|
|
+ window.parent.postMessage({
|
|
|
+ type: 'global-loading',
|
|
|
+ action: 'hide'
|
|
|
+ }, '*');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showGlobalAlert(message) {
|
|
|
+ // 通过postMessage通知父页面显示alert
|
|
|
+ if (window.parent && window.parent !== window) {
|
|
|
+ window.parent.postMessage({
|
|
|
+ type: 'global-alert',
|
|
|
+ message: message
|
|
|
+ }, '*');
|
|
|
+ } else {
|
|
|
+ // 降级处理:如果没有父窗口,使用全局alert(理论上不应该发生)
|
|
|
+ console.warn('[Disk] ⚠ 无父窗口,使用系统alert');
|
|
|
+ window.alert(message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showGlobalConfirm(message) {
|
|
|
+ console.log('[Disk] → showGlobalConfirm 开始');
|
|
|
+ console.log('[Disk] 消息内容:', message);
|
|
|
+ console.log('[Disk] window.parent 存在:', window.parent && window.parent !== window);
|
|
|
+
|
|
|
+ // 通过postMessage通知父页面显示confirm对话框
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ if (window.parent && window.parent !== window) {
|
|
|
+ // 生成唯一ID用于识别响应
|
|
|
+ const confirmId = 'confirm_' + Date.now();
|
|
|
+ console.log('[Disk] 生成确认ID:', confirmId);
|
|
|
+
|
|
|
+ // 监听父窗口的响应
|
|
|
+ const handleResponse = (event) => {
|
|
|
+ console.log('[Disk] ← 收到消息响应:', event.data);
|
|
|
+ if (event.data && event.data.type === 'global-confirm-response' && event.data.id === confirmId) {
|
|
|
+ console.log('[Disk] ✓ 匹配到确认响应,结果:', event.data.confirmed);
|
|
|
+ window.removeEventListener('message', handleResponse);
|
|
|
+ resolve(event.data.confirmed);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ window.addEventListener('message', handleResponse);
|
|
|
+ console.log('[Disk] ✓ 已注册消息监听器');
|
|
|
+
|
|
|
+ // 发送确认请求到父窗口
|
|
|
+ console.log('[Disk] → 发送global-confirm消息到父窗口');
|
|
|
+ window.parent.postMessage({
|
|
|
+ type: 'global-confirm',
|
|
|
+ id: confirmId,
|
|
|
+ message: message
|
|
|
+ }, '*');
|
|
|
+ console.log('[Disk] ✓ 消息已发送,等待用户操作...');
|
|
|
+ } else {
|
|
|
+ // 降级到原生confirm(理论上不应该发生)
|
|
|
+ console.warn('[Disk] ⚠ 无父窗口,使用原生confirm');
|
|
|
+ resolve(window.confirm(message));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 response = await fetch('/api/disk/delete', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ paths: this.selection.getSelectedItems()
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ if (data.success) {
|
|
|
+ this.selection.clearSelection();
|
|
|
+ this.loadFiles();
|
|
|
+ } else {
|
|
|
+ this.showGlobalAlert('删除失败: ' + data.message);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除失败:', error);
|
|
|
+ this.showGlobalAlert('删除失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 });
|
|
|
+
|
|
|
+ // 使用fetch读取SSE流
|
|
|
+ const response = await fetch('/api/disk/remove-background', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ paths: selectedPaths })
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('[Disk] ✓ HTTP响应收到');
|
|
|
+ console.log('[Disk] 状态码:', response.status);
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('服务器请求失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取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 {
|
|
|
+ // 使用fetch读取SSE流
|
|
|
+ const response = await fetch('/api/disk/crop-mini', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ paths: selectedPaths })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('服务器请求失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取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) {
|
|
|
+ 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
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 filesToUpload = files
|
|
|
+ .filter(file => this.isImageFile(file.name))
|
|
|
+ .map(file => ({
|
|
|
+ file: file,
|
|
|
+ path: file.webkitRelativePath || file.name
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (filesToUpload.length > 0) {
|
|
|
+ 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 formData = new FormData();
|
|
|
+ formData.append('file', fileData.file);
|
|
|
+ formData.append('path', this.pathNav.getPath());
|
|
|
+ formData.append('relativePath', fileData.path);
|
|
|
+
|
|
|
+ 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 response = await fetch('/api/disk/list?path=&recursive=true');
|
|
|
+ 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} 个项目`);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[Disk] 初始化文件结构缓存失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从缓存中获取文件信息
|
|
|
+ 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 response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ if (data.success) {
|
|
|
+ this.files = data.files;
|
|
|
+
|
|
|
+ // 更新缓存
|
|
|
+ data.files.forEach(file => {
|
|
|
+ this.updateCacheItem(file.path, file);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.renderFiles();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载文件列表失败:', error);
|
|
|
+ } finally {
|
|
|
+ this.showLoading(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 静默加载文件列表(用于上传过程中增量更新,不显示加载动画,不闪烁)
|
|
|
+ async loadFilesQuietly() {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
|
|
|
+ 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 = `
|
|
|
+ <div class="check-mark">
|
|
|
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
|
+ <path d="M10 3L4.5 8.5 2 6" stroke="white" stroke-width="2" fill="none"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 检查是否是包含PNG的文件夹(通过名称模式判断)
|
|
|
+ const isAnimationFolder = file.type === 'directory' && this.isAnimationFolder(file.name);
|
|
|
+
|
|
|
+ if (isAnimationFolder) {
|
|
|
+ // 动画文件夹显示第一帧预览
|
|
|
+ // 使用服务器提供的预览信息(完全避免404错误)
|
|
|
+ if (file.hasPreview && file.previewUrl) {
|
|
|
+ // 服务器确认有预览图,直接使用服务器提供的URL
|
|
|
+ // 添加时间戳防止缓存
|
|
|
+ const previewUrl = file.previewUrl + '&t=' + Date.now();
|
|
|
+ // console.log(`[Disk] 文件夹 ${file.name} 有预览图`);
|
|
|
+ div.innerHTML = `
|
|
|
+ ${checkMark}
|
|
|
+ <div class="file-thumbnail folder-preview">
|
|
|
+ <img src="${previewUrl}" alt="${file.name}" loading="lazy">
|
|
|
+ <div class="folder-badge">📁</div>
|
|
|
+ </div>
|
|
|
+ <div class="file-name">${file.name}</div>
|
|
|
+ <input type="text" class="rename-input" style="display: none;">
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ // 服务器确认没有预览图,直接显示文件夹图标
|
|
|
+ // console.log(`[Disk] 文件夹 ${file.name} 无预览图,显示图标`);
|
|
|
+ const icon = this.getFileIcon(file);
|
|
|
+ div.innerHTML = `
|
|
|
+ ${checkMark}
|
|
|
+ <div class="file-icon">
|
|
|
+ ${icon}
|
|
|
+ </div>
|
|
|
+ <div class="file-name">${file.name}</div>
|
|
|
+ <input type="text" class="rename-input" style="display: none;">
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ } else if (isImage && file.type !== 'directory') {
|
|
|
+ // 添加时间戳防止缓存
|
|
|
+ const previewUrl = `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`;
|
|
|
+ div.innerHTML = `
|
|
|
+ ${checkMark}
|
|
|
+ <div class="file-thumbnail">
|
|
|
+ <img src="${previewUrl}" alt="${file.name}" loading="lazy">
|
|
|
+ </div>
|
|
|
+ <div class="file-name">${file.name}</div>
|
|
|
+ <input type="text" class="rename-input" style="display: none;">
|
|
|
+ ${size ? `<div class="file-info">${size}</div>` : ''}
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ const icon = this.getFileIcon(file);
|
|
|
+ div.innerHTML = `
|
|
|
+ ${checkMark}
|
|
|
+ <div class="file-icon">
|
|
|
+ ${icon}
|
|
|
+ </div>
|
|
|
+ <div class="file-name">${file.name}</div>
|
|
|
+ <input type="text" class="rename-input" style="display: none;">
|
|
|
+ ${size ? `<div class="file-info">${size}</div>` : ''}
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 用于区分单击和双击的定时器
|
|
|
+ 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;
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/disk/move', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ sourcePath,
|
|
|
+ targetFolder
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ console.error('移动失败:', error);
|
|
|
+ if (!silent) {
|
|
|
+ this.showGlobalAlert('移动失败');
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getFileIcon(file) {
|
|
|
+ if (file.type === 'directory') {
|
|
|
+ return `
|
|
|
+ <svg class="folder-icon" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M8 12h20l4 6h24a4 4 0 014 4v28a4 4 0 01-4 4H8a4 4 0 01-4-4V16a4 4 0 014-4z"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ext = file.name.split('.').pop().toLowerCase();
|
|
|
+
|
|
|
+ if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-image" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M12 8h40a4 4 0 014 4v40a4 4 0 01-4 4H12a4 4 0 01-4-4V12a4 4 0 014-4z"/>
|
|
|
+ <circle cx="20" cy="20" r="4" fill="white"/>
|
|
|
+ <path d="M8 48l16-16 12 12 16-20v24H8z" fill="white" opacity="0.8"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-video" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
|
|
|
+ <path d="M24 20l20 12-20 12z" fill="white"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) {
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-audio" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
|
|
|
+ <path d="M24 20h4v20a6 6 0 11-4-5.66V20zm20-4v20a6 6 0 11-4-5.66V16h4z" fill="white"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-zip" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M16 8h32a4 4 0 014 4v40a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
|
|
|
+ <rect x="28" y="12" width="8" height="4" fill="white"/>
|
|
|
+ <rect x="28" y="20" width="8" height="4" fill="white"/>
|
|
|
+ <rect x="28" y="28" width="8" height="4" fill="white"/>
|
|
|
+ <rect x="28" y="36" width="8" height="8" rx="2" fill="white"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (['doc', 'docx', 'txt', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-document" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
|
|
|
+ <path d="M40 8v12h12" fill="white" opacity="0.5"/>
|
|
|
+ <rect x="20" y="28" width="24" height="2" fill="white"/>
|
|
|
+ <rect x="20" y="34" width="24" height="2" fill="white"/>
|
|
|
+ <rect x="20" y="40" width="16" height="2" fill="white"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ return `
|
|
|
+ <svg class="file-icon-default" viewBox="0 0 64 64" fill="currentColor">
|
|
|
+ <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
|
|
|
+ <path d="M40 8v12h12" fill="white" opacity="0.5"/>
|
|
|
+ </svg>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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++;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/disk/create-folder', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ path: this.pathNav.getPath(),
|
|
|
+ name: folderName
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ console.error('创建文件夹失败:', error);
|
|
|
+ this.showGlobalAlert('创建文件夹失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始重命名(参考 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) {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/disk/rename', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ 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) {
|
|
|
+ window.open(`/api/disk/download?path=${encodeURIComponent(filePath)}`, '_blank');
|
|
|
+ }
|
|
|
+
|
|
|
+ async copyFile(sourcePath, targetFolder, options = {}) {
|
|
|
+ const { suppressReload = false, silent = false } = options;
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/disk/copy', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ sourcePath,
|
|
|
+ targetFolder
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ console.error('复制失败:', error);
|
|
|
+ if (!silent) {
|
|
|
+ this.showGlobalAlert('复制失败');
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showLoading(show) {
|
|
|
+ if (show) {
|
|
|
+ this.loading.classList.add('show');
|
|
|
+ } else {
|
|
|
+ this.loading.classList.remove('show');
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ window.diskManager = new DiskManager();
|
|
|
+});
|