// 素材管理主逻辑模块(参考 client/js/disk/disk.js) class ResourceManager { constructor(options = {}) { this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000'; this.files = []; this.currentPath = ''; // DOM 元素 this.container = document.querySelector('.disk-container'); this.dropZone = null; this.fileList = null; this.breadcrumb = null; this.emptyState = null; this.loading = null; this.selectionBar = null; this.selectionCount = null; this.selectionBox = null; this.contextMenu = null; this.fileInput = null; this.searchInput = null; this.searchClear = null; this.btnUpload = null; this.btnDelete = null; // 功能模块 this.pathNav = null; this.selection = null; this.searchBar = null; this.contextMenuManager = null; this.shortcutKeys = null; // 等待 DOM 加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.init()); } else { this.init(); } } async init() { this.initElements(); this.initPath(); this.initSelection(); this.initShortcutKeys(); this.initSearchBar(); this.initContextMenu(); this.bindEvents(); // 加载文件列表 await this.loadFiles(); } 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.selectionBar = document.getElementById('selectionBar'); this.selectionCount = document.getElementById('selectionCount'); this.selectionBox = document.getElementById('selectionBox'); this.contextMenu = document.getElementById('contextMenu'); this.fileInput = null; // 动态创建 this.searchInput = document.getElementById('searchInput'); this.searchClear = document.getElementById('searchClear'); this.btnUpload = document.getElementById('btnUpload'); this.btnDelete = document.getElementById('btnDelete'); } // 初始化路径导航 initPath() { if (this.breadcrumb) { this.pathNav = new ResourceManagerPathNavigator({ container: this.breadcrumb, rootName: '全部文件', onNavigate: (path) => { this.loadFiles(path); } }); } } // 初始化框选功能 initSelection() { if (this.dropZone && this.fileList && this.selectionBox) { this.selection = new ResourceManagerMultipleSelection({ container: this.dropZone, itemsContainer: this.fileList, selectionBox: this.selectionBox, selectionBar: this.selectionBar, selectionCount: this.selectionCount, itemSelector: '.file-item', onSelectionChange: (selectedItems) => { // 选择变化时的回调 } }); } } // 初始化快捷键 initShortcutKeys() { if (this.shortcutKeys) { this.shortcutKeys.destroy(); } this.shortcutKeys = new ResourceManagerShortcutKeys({ selection: this.selection, onDelete: () => this.deleteSelected(), onRename: () => this.renameSelected() }); } // 初始化搜索栏 initSearchBar() { if (this.searchInput && this.fileList) { this.searchBar = new ResourceManagerSearchBar({ input: this.searchInput, clearButton: this.searchClear, fileList: this.fileList, emptyState: this.emptyState, getResources: () => this.files, renderAll: () => this.renderFiles(), createFileItem: (file) => this.createFileItem(file), noResultMessage: '没有找到匹配的资源' }); } } // 初始化右键菜单 initContextMenu() { if (!this.dropZone || !this.contextMenu) return; this.contextMenuManager = new ResourceManagerRightClickMenu({ 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'); if (!this.contextMenu) return false; const newBtn = this.contextMenu.querySelector('[data-action="new"]'); const uploadBtn = this.contextMenu.querySelector('[data-action="upload"]'); const backBtn = this.contextMenu.querySelector('[data-action="back"]'); const deleteBtn = this.contextMenu.querySelector('[data-action="delete"]'); const renameBtn = this.contextMenu.querySelector('[data-action="rename"]'); const refreshBtn = this.contextMenu.querySelector('[data-action="refresh"]'); const isRoot = this.currentPath === ''; // 新建分类:只在根目录时显示 if (newBtn) { newBtn.style.display = isRoot ? 'flex' : 'none'; } // 上传素材:只在子目录时显示 if (uploadBtn) { uploadBtn.style.display = isRoot ? 'none' : 'flex'; } // 返回上级:只在子目录时显示 if (backBtn) { backBtn.style.display = isRoot ? 'none' : 'flex'; } // 刷新按钮始终显示 if (refreshBtn) { refreshBtn.style.display = 'flex'; } // 如果点击在文件项上 if (fileItem) { // 确保被点击的项被选中 const isSelected = this.selection && this.selection.isSelected(fileItem.dataset.path); if (!isSelected && this.selection) { this.selection.selectOnly(fileItem); } if (deleteBtn) deleteBtn.style.display = 'flex'; if (renameBtn) renameBtn.style.display = 'flex'; } else { if (deleteBtn) deleteBtn.style.display = 'none'; if (renameBtn) renameBtn.style.display = 'none'; } // 更新分隔线显示 this.updateContextMenuDividers(); return true; } // 更新右键菜单分隔线显示 updateContextMenuDividers() { if (!this.contextMenu) return; const items = Array.from(this.contextMenu.children); let lastVisibleWasDivider = true; items.forEach((item, index) => { if (item.classList.contains('context-menu-divider')) { // 如果上一个可见项也是分隔线,或者是第一个,隐藏 if (lastVisibleWasDivider) { item.style.display = 'none'; } else { item.style.display = 'block'; lastVisibleWasDivider = true; } } else if (item.style.display !== 'none') { lastVisibleWasDivider = false; } }); // 隐藏末尾的分隔线 for (let i = items.length - 1; i >= 0; i--) { const item = items[i]; if (item.classList.contains('context-menu-divider')) { if (item.style.display !== 'none') { item.style.display = 'none'; } } else if (item.style.display !== 'none') { break; } } } // 处理右键菜单操作 handleContextMenuAction(action) { switch (action) { case 'new': this.createFolder(); break; case 'upload': // 触发上传 this.triggerUpload(); break; case 'back': // 返回上级目录 this.loadFiles(''); break; case 'delete': this.deleteSelected(); break; case 'rename': this.renameSelected(); break; case 'refresh': this.loadFiles(this.currentPath); break; } } bindEvents() { // 上传按钮已移除,使用右键菜单或拖拽上传 // 删除按钮 if (this.btnDelete) { this.btnDelete.addEventListener('click', () => this.deleteSelected()); } // 拖拽上传 if (this.dropZone) { console.log('[ResourceManager] 绑定拖拽事件到 dropZone:', this.dropZone); 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)); } else { console.error('[ResourceManager] dropZone 未找到!'); } } // 触发上传(动态创建文件输入) triggerUpload() { // 创建新的文件输入(每次都重新创建以确保属性正确) const newInput = document.createElement('input'); newInput.type = 'file'; newInput.style.display = 'none'; // 使用文件夹选择模式 newInput.webkitdirectory = true; newInput.directory = true; newInput.multiple = true; // 移除旧的文件输入 if (this.fileInput) { this.fileInput.remove(); } // 添加到DOM document.body.appendChild(newInput); this.fileInput = newInput; // 绑定change事件 - 跳过浏览器确认后直接处理 this.fileInput.addEventListener('change', (e) => this.handleFileSelectDirect(e)); // 触发点击 this.fileInput.click(); } // 直接处理文件选择(跳过额外确认) async handleFileSelectDirect(e) { const files = Array.from(e.target.files); if (files.length === 0) { this.fileInput.value = ''; return; } // 验证文件夹结构 const validation = this.validateFolderFiles(files); if (!validation.valid) { this.showError(validation.errors.join('\n')); this.fileInput.value = ''; return; } // 浏览器已经确认过了,直接上传,不再弹出我们的确认框 const filesToUpload = validation.files.map(file => ({ file: file, path: file.webkitRelativePath || file.name })); try { await this.uploadFolderFiles(validation.folderName, filesToUpload); this.showSuccess(`上传成功: ${validation.folderName}`); await this.loadFiles(this.currentPath); } catch (error) { this.showError('上传失败: ' + error.message); } this.fileInput.value = ''; } // 加载文件列表 async loadFiles(path = '') { this.showLoading(true); if (this.selection) { this.selection.clearSelection(); } if (this.searchBar) { this.searchBar.clear({ noRender: true }); } this.currentPath = path; try { if (path === '') { // 根目录:加载分类列表 const response = await fetch(`${this.apiBaseUrl}/api/store/categories`); if (!response.ok) { throw new Error(`加载分类失败: ${response.status}`); } const result = await response.json(); if (result.success && result.categories) { this.files = result.categories.map(cat => ({ name: cat.name, path: cat.dir, type: 'directory', isCategory: true })); } else { this.files = []; } } else { // 子目录:加载资源列表 const response = await fetch(`${this.apiBaseUrl}/api/store/resources?category=${encodeURIComponent(path)}`); if (!response.ok) { throw new Error(`加载资源失败: ${response.status}`); } const result = await response.json(); if (result.success && result.resources) { this.files = result.resources.map(res => ({ ...res, type: 'directory' })); } else { this.files = []; } } this.renderFiles(); } catch (error) { console.error('[ResourceManager] 加载失败:', error); this.files = []; this.renderFiles(); this.showError('加载失败: ' + error.message); } finally { this.showLoading(false); } } // 渲染文件列表 renderFiles() { if (!this.fileList) return; this.fileList.innerHTML = ''; if (this.files.length === 0) { if (this.emptyState) { this.emptyState.classList.add('show'); } return; } if (this.emptyState) { this.emptyState.classList.remove('show'); } this.files.forEach(file => { 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; // 在根目录时,分类文件夹可以拖拽排序 if (this.currentPath === '' && file.isCategory) { div.draggable = true; this.setupDragReorder(div, file); } // 勾选标记 const checkMark = `
`; // 检查是否有预览图 if (file.previewUrl) { div.innerHTML = ` ${checkMark}
${file.name}
📁
${file.name}
`; } else { div.innerHTML = ` ${checkMark}
${file.name}
`; } // 点击事件 let clickTimer = null; div.addEventListener('click', (e) => { if (e.target.classList.contains('rename-input')) return; e.stopPropagation(); const isAlreadySelected = this.selection && 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 && this.selection) { this.selection.toggleSelection(div); } else if (clickedOnName && isAlreadySelected) { this.startRename(div); } else if (this.selection) { 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') { if (file.isCategory) { this.pathNav.navigateTo(file.path); } else { // 资源文件夹可以进一步打开查看 // 或者在这里可以打开预览 } } }); return div; } // 开始重命名 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; 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 = () => { 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(); }; 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(`${this.apiBaseUrl}/api/admin/store/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resourcePath: oldPath, newName }) }); const result = await response.json(); if (result.success) { this.showSuccess('重命名成功'); await this.loadFiles(this.currentPath); } else { this.showError('重命名失败: ' + (result.message || '未知错误')); await this.loadFiles(this.currentPath); } } catch (error) { this.showError('重命名失败: ' + error.message); await this.loadFiles(this.currentPath); } } // 重命名选中项 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); } } // 删除选中项 async deleteSelected() { console.log('[ResourceManager] deleteSelected 被调用'); console.log('[ResourceManager] this.selection:', this.selection); if (!this.selection) { console.log('[ResourceManager] selection 为空,退出'); return; } const count = this.selection.getSelectedCount(); console.log('[ResourceManager] 选中数量:', count); if (count === 0) { console.log('[ResourceManager] 没有选中项,退出'); return; } const confirmed = await this.showConfirm(`确定要删除选中的 ${count} 个文件/文件夹吗?`); if (!confirmed) return; try { const selectedPaths = this.selection.getSelectedItems(); for (const path of selectedPaths) { const response = await fetch(`${this.apiBaseUrl}/api/admin/store/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resourcePath: path }) }); const result = await response.json(); if (!result.success) { this.showError('删除失败: ' + (result.message || '未知错误')); } } this.selection.clearSelection(); await this.loadFiles(this.currentPath); this.showSuccess('删除成功'); } catch (error) { this.showError('删除失败: ' + error.message); } } // 设置拖拽排序 setupDragReorder(div, file) { div.addEventListener('dragstart', (e) => { e.stopPropagation(); div.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'reorder', name: file.name })); }); div.addEventListener('dragend', () => { div.classList.remove('dragging'); // 移除所有拖拽目标样式 this.fileList.querySelectorAll('.file-item.drag-over-left, .file-item.drag-over-right').forEach(el => { el.classList.remove('drag-over-left', 'drag-over-right'); }); // 确保移除上传提示 if (this.dropZone) { this.dropZone.classList.remove('drag-over'); } }); div.addEventListener('dragover', (e) => { e.preventDefault(); // 只有内部拖拽排序时才处理 const draggingEl = this.fileList.querySelector('.file-item.dragging'); if (!draggingEl || draggingEl === div) { // 外部文件拖入时,不阻止冒泡,让 dropZone 处理 return; } e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; // 判断鼠标在元素的左半边还是右半边 const rect = div.getBoundingClientRect(); const midX = rect.left + rect.width / 2; div.classList.remove('drag-over-left', 'drag-over-right'); if (e.clientX < midX) { div.classList.add('drag-over-left'); } else { div.classList.add('drag-over-right'); } }); div.addEventListener('dragleave', (e) => { // 只有内部拖拽排序时才阻止冒泡 const draggingEl = this.fileList.querySelector('.file-item.dragging'); if (draggingEl) { e.stopPropagation(); } div.classList.remove('drag-over-left', 'drag-over-right'); }); div.addEventListener('drop', async (e) => { e.preventDefault(); // 如果是外部文件拖入,让事件继续冒泡到 dropZone if (e.dataTransfer.types.includes('Files') && !e.dataTransfer.getData('text/plain')) { console.log('[ResourceManager] 外部文件拖入到文件项,转发到 dropZone'); this.handleDrop(e); return; } e.stopPropagation(); const isLeft = div.classList.contains('drag-over-left'); div.classList.remove('drag-over-left', 'drag-over-right'); try { const data = JSON.parse(e.dataTransfer.getData('text/plain')); if (data.type !== 'reorder') return; const draggedName = data.name; const targetName = file.name; if (draggedName === targetName) return; // 重新计算顺序 const currentOrder = this.files.map(f => f.name); const draggedIndex = currentOrder.indexOf(draggedName); let targetIndex = currentOrder.indexOf(targetName); if (draggedIndex === -1 || targetIndex === -1) return; // 移除拖拽的元素 currentOrder.splice(draggedIndex, 1); // 重新计算目标位置(因为移除了一个元素) targetIndex = currentOrder.indexOf(targetName); // 插入到目标位置 if (isLeft) { currentOrder.splice(targetIndex, 0, draggedName); } else { currentOrder.splice(targetIndex + 1, 0, draggedName); } // 保存新顺序 await this.saveCategoryOrder(currentOrder); } catch (error) { console.error('[ResourceManager] 拖拽排序失败:', error); } }); } // 保存分类排序 async saveCategoryOrder(order) { try { const response = await fetch(`${this.apiBaseUrl}/api/admin/store/update-order`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order }) }); const result = await response.json(); if (result.success) { this.showSuccess('排序已保存'); await this.loadFiles(''); } else { this.showError('保存排序失败: ' + (result.message || '未知错误')); } } catch (error) { this.showError('保存排序失败: ' + error.message); } } // 创建文件夹 async createFolder() { if (this.currentPath !== '') { this.showError('只能在根目录创建分类文件夹'); return; } const folderName = await this.showPrompt('请输入分类文件夹名称:'); if (!folderName || !folderName.trim()) return; const name = folderName.trim(); if (/[\\/:*?"<>|]/.test(name)) { this.showError('文件夹名称包含非法字符'); return; } try { const response = await fetch(`${this.apiBaseUrl}/api/admin/store/create-folder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); const result = await response.json(); if (result.success) { this.showSuccess('创建文件夹成功'); await this.loadFiles(''); } else { this.showError('创建文件夹失败: ' + (result.message || '未知错误')); } } catch (error) { this.showError('创建文件夹失败: ' + error.message); } } // 处理文件选择(始终为文件夹模式) async handleFileSelect(e) { const files = Array.from(e.target.files); if (files.length === 0) { this.fileInput.value = ''; return; } // 验证文件夹结构 const validation = this.validateFolderFiles(files); if (!validation.valid) { this.showError(validation.errors.join('\n')); this.fileInput.value = ''; return; } const targetPath = this.currentPath || validation.folderName; const confirmMsg = this.currentPath ? `将上传文件夹 "${validation.folderName}" 到分类 "${this.currentPath}",包含 ${validation.files.length} 个PNG图片,确认上传?` : `将上传文件夹 "${validation.folderName}",包含 ${validation.files.length} 个PNG图片,确认上传?`; const confirmed = await this.showConfirm(confirmMsg); if (!confirmed) { this.fileInput.value = ''; return; } // 根据当前位置决定上传方式 // 转换 validation.files (File[]) 为 {file, path}[] 格式 const filesToUpload = validation.files.map(file => ({ file: file, path: file.webkitRelativePath || file.name })); if (this.currentPath === '') { // 在根目录上传,文件夹名就是分类名 await this.uploadFolderFiles(validation.folderName, filesToUpload); } else { // 在子目录上传 await this.uploadFolderFiles(validation.folderName, filesToUpload); } this.fileInput.value = ''; } // 验证通过文件输入选择的文件夹 validateFolderFiles(files) { const errors = []; const validFiles = []; let folderName = ''; // 获取文件夹名(第一层目录) const paths = new Set(); for (const file of files) { const relativePath = file.webkitRelativePath || file.name; const parts = relativePath.split('/'); if (parts.length > 0) { paths.add(parts[0]); if (!folderName) folderName = parts[0]; } } // 检查是否只有一个顶级文件夹 if (paths.size > 1) { errors.push('请只选择一个文件夹'); return { valid: false, errors, files: [], folderName: '' }; } for (const file of files) { const relativePath = file.webkitRelativePath || file.name; const parts = relativePath.split('/'); // 检查是否有子文件夹(路径深度超过2层) if (parts.length > 2) { errors.push(`不允许有子文件夹: ${parts.slice(0, -1).join('/')}`); continue; } // 检查文件类型 const fileName = file.name.toLowerCase(); if (!fileName.endsWith('.png')) { errors.push(`文件 "${file.name}" 不是PNG格式,只允许PNG图片`); continue; } validFiles.push(file); } if (validFiles.length === 0 && errors.length === 0) { errors.push('文件夹为空或没有PNG图片'); } // 只显示前5个错误 const displayErrors = errors.slice(0, 5); if (errors.length > 5) { displayErrors.push(`... 还有 ${errors.length - 5} 个错误`); } return { valid: errors.length === 0, errors: displayErrors, files: validFiles, folderName }; } // 上传文件 async uploadFiles(files) { if (files.length === 0) return; // 获取目标路径 const category = this.currentPath || files[0].webkitRelativePath?.split('/')[0] || 'default'; const folderName = files[0].webkitRelativePath?.split('/')[0] || 'upload_' + Date.now(); const formData = new FormData(); formData.append('category', category); formData.append('name', folderName); formData.append('price', 0); for (const file of files) { formData.append('files', file); } try { const response = await fetch(`${this.apiBaseUrl}/api/admin/store/upload`, { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { this.showSuccess('上传成功'); await this.loadFiles(this.currentPath); } else { this.showError('上传失败: ' + (result.message || '未知错误')); } } catch (error) { this.showError('上传失败: ' + error.message); } } // 检查是否是外部文件拖入(而非内部排序拖拽) isExternalFileDrag(e) { // 如果有正在拖拽的内部元素,说明是内部排序 if (this.fileList && this.fileList.querySelector('.file-item.dragging')) { return false; } // 检查是否有文件类型 return e.dataTransfer.types.includes('Files'); } // 拖拽处理 handleDragEnter(e) { e.preventDefault(); e.stopPropagation(); console.log('[ResourceManager] dragenter 触发'); // 只有外部文件拖入时才显示上传提示 if (this.isExternalFileDrag(e)) { this.dropZone.classList.add('drag-over'); } } handleDragOver(e) { e.preventDefault(); e.stopPropagation(); } handleDragLeave(e) { e.preventDefault(); e.stopPropagation(); // 检查是否真正离开了 dropZone(而不是进入子元素) if (!this.dropZone.contains(e.relatedTarget)) { this.dropZone.classList.remove('drag-over'); } } async handleDrop(e) { console.log('[ResourceManager] ===== handleDrop 触发 ====='); e.preventDefault(); e.stopPropagation(); this.dropZone.classList.remove('drag-over'); console.log('[ResourceManager] dataTransfer.types:', Array.from(e.dataTransfer.types)); console.log('[ResourceManager] isExternalFileDrag:', this.isExternalFileDrag(e)); // 如果是内部排序拖拽,不处理文件上传 if (!this.isExternalFileDrag(e)) { console.log('[ResourceManager] 是内部拖拽,跳过'); return; } const items = e.dataTransfer.items; console.log('[ResourceManager] items:', items, 'length:', items ? items.length : 0); if (!items) return; const entries = []; for (let i = 0; i < items.length; i++) { const item = items[i].webkitGetAsEntry(); console.log(`[ResourceManager] entry ${i}:`, item ? item.name : 'null'); if (item) { entries.push(item); } } console.log('[ResourceManager] 共收集到', entries.length, '个 entries'); await this.processDropEntries(entries); } // 处理拖入的 entries(完全按照 client/js/disk/disk.js 的方式) async processDropEntries(entries) { console.log('[ResourceManager] processDropEntries 开始处理', entries.length, '个 entries'); // 检查是否有非文件夹 const hasFiles = entries.some(entry => entry.isFile); if (hasFiles) { this.showError('只能拖拽文件夹,不能拖拽单个文件'); return; } const filesToUpload = []; for (const entry of entries) { console.log('[ResourceManager] 开始遍历:', entry.name); try { await this.traverseEntry(entry, '', filesToUpload); console.log('[ResourceManager] 遍历完成:', entry.name, '当前文件数:', filesToUpload.length); } catch (err) { console.error('[ResourceManager] 遍历失败:', entry.name, err); } } console.log('[ResourceManager] 总共收集到', filesToUpload.length, '个 PNG 文件'); if (filesToUpload.length > 0) { // 按文件夹分组 const folderMap = new Map(); for (const item of filesToUpload) { const folderName = item.path.split('/')[0]; if (!folderMap.has(folderName)) { folderMap.set(folderName, []); } folderMap.get(folderName).push(item); } const folderNames = Array.from(folderMap.keys()); const confirmMsg = `将上传 ${folderMap.size} 个文件夹(${folderNames.slice(0, 5).join('、')}${folderNames.length > 5 ? '...' : ''}),共 ${filesToUpload.length} 个PNG图片\n\n确认上传?`; const confirmed = await this.showConfirm(confirmMsg); if (!confirmed) { return; } // 按文件夹逐个上传,每上传完一个就刷新显示 let successCount = 0; let failCount = 0; const totalCount = folderMap.size; for (const [folderName, files] of folderMap) { try { await this.uploadFolderFiles(folderName, files); successCount++; // 每上传完一个文件夹就刷新显示 await this.loadFiles(this.currentPath); this.showInfo(`上传进度: ${successCount}/${totalCount} - ${folderName}`); } catch (error) { console.error(`[ResourceManager] 上传 ${folderName} 失败:`, error); failCount++; } } if (failCount === 0) { this.showSuccess(`成功上传 ${successCount} 个文件夹`); } else { this.showWarning(`上传完成: ${successCount} 成功, ${failCount} 失败`); } } else { console.log('[ResourceManager] 没有找到可上传的PNG图片'); this.showError('没有找到可上传的PNG图片'); } } // 递归遍历文件夹 async traverseEntry(entry, relativePath, filesToUpload) { console.log('[ResourceManager] traverseEntry:', entry.name, 'isFile:', entry.isFile, 'isDirectory:', entry.isDirectory); if (entry.isFile) { console.log('[ResourceManager] 读取文件:', entry.name); const file = await new Promise((resolve, reject) => { entry.file(resolve, reject); }); console.log('[ResourceManager] 文件读取完成:', file.name, file.size); // 只接受 PNG 文件 if (file.name.toLowerCase().endsWith('.png')) { filesToUpload.push({ file: file, path: relativePath + file.name }); console.log('[ResourceManager] 添加PNG:', relativePath + file.name); } } else if (entry.isDirectory) { console.log('[ResourceManager] 开始读取目录:', entry.name); const dirReader = entry.createReader(); // 读取所有条目 - readEntries 每次最多返回100条,需要循环读取 let allEntries = []; let batch; let readCount = 0; do { console.log('[ResourceManager] 调用 readEntries 第', readCount + 1, '次'); batch = await new Promise((resolve) => { dirReader.readEntries( (entries) => { console.log('[ResourceManager] readEntries 成功, 返回', entries.length, '条'); resolve(entries); }, (err) => { console.error('[ResourceManager] readEntries 错误:', err); resolve([]); // 出错返回空数组继续 } ); }); allEntries = allEntries.concat(batch); readCount++; } while (batch.length > 0); console.log('[ResourceManager] 目录', entry.name, '共有', allEntries.length, '个条目'); for (const childEntry of allEntries) { await this.traverseEntry( childEntry, relativePath + entry.name + '/', filesToUpload ); } console.log('[ResourceManager] 目录', entry.name, '遍历完成'); } } // 上传单个文件夹的文件 async uploadFolderFiles(folderName, files) { if (files.length === 0) return; const category = this.currentPath || folderName; const formData = new FormData(); formData.append('category', category); formData.append('name', folderName); formData.append('price', '0'); for (const item of files) { formData.append('files', item.file, item.path); } const response = await fetch(`${this.apiBaseUrl}/api/admin/store/upload`, { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) { throw new Error(result.message || '上传失败'); } return result; } isImageFile(fileName) { const ext = fileName.split('.').pop().toLowerCase(); return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext); } showLoading(show) { if (this.loading) { if (show) { this.loading.classList.add('show'); } else { this.loading.classList.remove('show'); } } } // 显示提示消息 showHint(message, type = 'info') { const hintView = document.getElementById('hintView'); const hintMessage = document.getElementById('hintMessage'); if (!hintView || !hintMessage) { console.log(`[${type}]`, message); return; } // 移除之前的类型类 hintView.classList.remove('success', 'error', 'warning', 'info'); hintView.classList.add(type); hintMessage.textContent = message; hintView.classList.remove('hide'); hintView.classList.add('show'); // 自动隐藏 setTimeout(() => { hintView.classList.remove('show'); hintView.classList.add('hide'); }, 3000); } showError(message) { this.showHint(message, 'error'); } showSuccess(message) { this.showHint(message, 'success'); } showWarning(message) { this.showHint(message, 'warning'); } showInfo(message) { this.showHint(message, 'info'); } // 显示确认对话框 showConfirm(message) { return new Promise((resolve) => { const overlay = document.getElementById('globalConfirmOverlay'); const messageEl = document.getElementById('confirmMessage'); const okBtn = document.getElementById('confirmOkBtn'); const cancelBtn = document.getElementById('confirmCancelBtn'); if (!overlay || !messageEl || !okBtn || !cancelBtn) { // 降级到原生 confirm resolve(confirm(message)); return; } messageEl.textContent = message; overlay.classList.add('show'); const cleanup = () => { overlay.classList.remove('show'); okBtn.removeEventListener('click', handleOk); cancelBtn.removeEventListener('click', handleCancel); }; const handleOk = () => { cleanup(); resolve(true); }; const handleCancel = () => { cleanup(); resolve(false); }; okBtn.addEventListener('click', handleOk); cancelBtn.addEventListener('click', handleCancel); }); } // 显示输入对话框 showPrompt(message, defaultValue = '') { return new Promise((resolve) => { const overlay = document.getElementById('globalPromptOverlay'); const messageEl = document.getElementById('promptMessage'); const input = document.getElementById('promptInput'); const okBtn = document.getElementById('promptOkBtn'); const cancelBtn = document.getElementById('promptCancelBtn'); if (!overlay || !messageEl || !input || !okBtn || !cancelBtn) { // 降级到原生 prompt resolve(prompt(message, defaultValue)); return; } messageEl.textContent = message; input.value = defaultValue; overlay.classList.add('show'); // 自动聚焦输入框 setTimeout(() => { input.focus(); input.select(); }, 100); const cleanup = () => { overlay.classList.remove('show'); okBtn.removeEventListener('click', handleOk); cancelBtn.removeEventListener('click', handleCancel); input.removeEventListener('keydown', handleKeydown); }; const handleOk = () => { const value = input.value; cleanup(); resolve(value); }; const handleCancel = () => { cleanup(); resolve(null); }; const handleKeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleOk(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }; okBtn.addEventListener('click', handleOk); cancelBtn.addEventListener('click', handleCancel); input.addEventListener('keydown', handleKeydown); }); } } // 导出 if (typeof module !== 'undefined' && module.exports) { module.exports = ResourceManager; } else { window.ResourceManager = ResourceManager; }