| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373 |
- // 素材管理主逻辑模块(参考 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 = `
- <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>
- `;
- // 检查是否有预览图
- if (file.previewUrl) {
- div.innerHTML = `
- ${checkMark}
- <div class="file-thumbnail folder-preview">
- <img src="${this.apiBaseUrl}${file.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 {
- div.innerHTML = `
- ${checkMark}
- <div class="file-icon folder-icon">
- <svg 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>
- </div>
- <div class="file-name">${file.name}</div>
- <input type="text" class="rename-input" style="display: none;">
- `;
- }
- // 点击事件
- 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;
- }
|