| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712 |
- // 网盘管理系统 - 客户端逻辑
- // 版本: 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();
- });
|