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