disk.js 75 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081
  1. // 网盘管理系统 - 客户端逻辑
  2. // 版本: v1.2 - 修复404错误
  3. // console.log('[Disk] 加载 DiskManager v1.2');
  4. class DiskManager {
  5. constructor() {
  6. this.files = [];
  7. this.uploadingFiles = [];
  8. this.dragCounter = 0;
  9. this.clipboard = null;
  10. this.shortcutKeys = null;
  11. this.container = document.querySelector('.disk-container');
  12. // 全局文件结构缓存(用于快速验证文件夹内容)
  13. this.fileStructureCache = new Map(); // key: 文件路径, value: 文件信息(包括pngCount)
  14. this.cacheInitialized = false;
  15. // 当前登录用户名
  16. this.currentUsername = null;
  17. this.init();
  18. }
  19. // 获取当前登录用户名
  20. getCurrentUsername() {
  21. // 如果已经缓存了用户名,直接返回
  22. if (this.currentUsername) {
  23. return this.currentUsername;
  24. }
  25. // 从导航栏 iframe 中获取用户名
  26. try {
  27. // 尝试从父窗口的 navigationFrame 获取
  28. let targetWindow = window.parent;
  29. while (targetWindow && targetWindow !== window) {
  30. try {
  31. const navigationFrame = targetWindow.document.getElementById('navigationFrame');
  32. if (navigationFrame && navigationFrame.contentWindow) {
  33. const navWindow = navigationFrame.contentWindow;
  34. const navDoc = navigationFrame.contentDocument || navWindow.document;
  35. // 检查用户是否已登录
  36. const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
  37. if (userAvatarContainer) {
  38. const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
  39. if (computedStyle.display !== 'none') {
  40. // 用户已登录,从 userAvatar 的 alt 属性获取用户名
  41. const userAvatar = navDoc.getElementById('userAvatar');
  42. if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') {
  43. const username = userAvatar.alt;
  44. // 缓存用户名
  45. this.currentUsername = username;
  46. console.log('[DiskManager] 从导航栏获取用户名:', username);
  47. return username;
  48. }
  49. }
  50. }
  51. }
  52. } catch (e) {
  53. // 跨域或访问限制,继续尝试上层窗口
  54. console.warn('[DiskManager] 访问导航栏失败:', e);
  55. }
  56. // 尝试更上层的窗口
  57. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  58. targetWindow = targetWindow.parent;
  59. } else {
  60. break;
  61. }
  62. }
  63. } catch (error) {
  64. console.warn('[DiskManager] 无法获取用户名:', error);
  65. }
  66. // 如果仍然没有获取到,返回 null
  67. return this.currentUsername;
  68. }
  69. // 设置当前用户名(从外部调用,例如登录成功后)
  70. setCurrentUsername(username) {
  71. this.currentUsername = username;
  72. }
  73. // 从 localStorage 恢复登录状态
  74. restoreLoginFromStorage() {
  75. try {
  76. const loginDataStr = localStorage.getItem('loginData');
  77. if (!loginDataStr) {
  78. return;
  79. }
  80. const loginData = JSON.parse(loginDataStr);
  81. const now = Date.now();
  82. // 检查是否过期
  83. if (now >= loginData.expireTime) {
  84. localStorage.removeItem('loginData');
  85. return;
  86. }
  87. // 未过期,恢复登录状态
  88. if (loginData.user && loginData.user.username) {
  89. this.setCurrentUsername(loginData.user.username);
  90. console.log('[DiskManager] 从 localStorage 恢复登录状态:', loginData.user.username);
  91. }
  92. } catch (error) {
  93. console.error('[DiskManager] 恢复登录状态失败:', error);
  94. }
  95. }
  96. // 监听登录成功消息
  97. initUserListener() {
  98. window.addEventListener('message', (event) => {
  99. if (event.data && event.data.type === 'login-success' && event.data.user) {
  100. const username = event.data.user.username;
  101. this.setCurrentUsername(username);
  102. console.log('[DiskManager] 收到登录成功消息,设置用户名:', username);
  103. // 登录成功后重新加载文件列表
  104. this.loadFiles();
  105. } else if (event.data && event.data.type === 'logout') {
  106. this.setCurrentUsername(null);
  107. console.log('[DiskManager] 收到登出消息,清除用户名');
  108. // 登出后清空文件列表
  109. this.files = [];
  110. this.renderFiles();
  111. }
  112. });
  113. }
  114. async init() {
  115. // 先尝试从 localStorage 恢复登录状态(同步操作,立即执行)
  116. this.restoreLoginFromStorage();
  117. // 并行加载工具栏和右键菜单(不阻塞页面)
  118. const toolbarPromise = this.ensureToolBar();
  119. const contextMenuPromise = this.ensureContextMenu();
  120. // 初始化基本元素(这些不依赖工具栏和菜单的HTML)
  121. this.initElements();
  122. this.initUploadProgress();
  123. this.initUserListener();
  124. // 等待工具栏加载完成后,初始化依赖它的组件
  125. await toolbarPromise;
  126. this.initElements(); // 重新获取工具栏中的元素
  127. this.initPath();
  128. this.initSelection();
  129. this.initShortcutKeys();
  130. this.initSearchBar();
  131. this.bindEvents();
  132. // 工具栏加载完成后立即开始加载文件列表
  133. const username = this.getCurrentUsername();
  134. if (username) {
  135. console.log('[DiskManager] 初始化时检测到已登录用户:', username);
  136. // 开始加载文件(不阻塞后续初始化)
  137. this.loadFiles();
  138. }
  139. // 等待右键菜单加载完成后初始化(并行进行,不影响文件加载)
  140. await contextMenuPromise;
  141. this.initContextMenu();
  142. // 后台初始化文件结构缓存(不阻塞页面加载)
  143. this.initFileStructureCache();
  144. }
  145. async ensureToolBar() {
  146. if (document.getElementById('breadcrumb')) {
  147. return;
  148. }
  149. if (!this.container) {
  150. return;
  151. }
  152. try {
  153. const response = await fetch('tool-bar.html', { cache: 'no-cache' });
  154. if (!response.ok) {
  155. throw new Error('加载工具栏失败');
  156. }
  157. const html = await response.text();
  158. this.container.insertAdjacentHTML('afterbegin', html);
  159. } catch (error) {
  160. console.error('加载工具栏失败:', error);
  161. }
  162. }
  163. async ensureContextMenu() {
  164. if (document.getElementById('contextMenu')) {
  165. return;
  166. }
  167. const menuContainer = document.getElementById('contextMenuContainer');
  168. if (!menuContainer) {
  169. return;
  170. }
  171. try {
  172. const response = await fetch('right-click-menu.html', { cache: 'no-cache' });
  173. if (!response.ok) {
  174. throw new Error('加载右键菜单失败');
  175. }
  176. const html = await response.text();
  177. menuContainer.innerHTML = html;
  178. } catch (error) {
  179. console.error('加载右键菜单失败:', error);
  180. }
  181. }
  182. initElements() {
  183. this.dropZone = document.getElementById('dropZone');
  184. this.fileList = document.getElementById('fileList');
  185. this.breadcrumb = document.getElementById('breadcrumb');
  186. this.emptyState = document.getElementById('emptyState');
  187. this.loading = document.getElementById('loading');
  188. this.btnCreateFolder = document.getElementById('btnCreateFolder');
  189. this.fileInput = document.getElementById('fileInput');
  190. this.searchInput = document.getElementById('searchInput');
  191. this.searchClear = document.getElementById('searchClear');
  192. this.selectionBar = document.getElementById('selectionBar');
  193. this.selectionCount = document.getElementById('selectionCount');
  194. this.selectionBox = document.getElementById('selectionBox');
  195. this.btnDownload = document.getElementById('btnDownload');
  196. this.btnDelete = document.getElementById('btnDelete');
  197. this.btnUpload = document.getElementById('btnUpload');
  198. this.uploadProgress = document.getElementById('uploadProgress');
  199. this.uploadProgressList = document.getElementById('uploadProgressList');
  200. this.uploadProgressTemplate = document.getElementById('uploadProgressTemplate');
  201. this.uploadProgressClose = document.getElementById('uploadProgressClose');
  202. this.contextMenu = document.getElementById('contextMenu');
  203. }
  204. // 初始化路径导航
  205. initPath() {
  206. this.pathNav = new PathNavigator({
  207. container: this.breadcrumb,
  208. rootName: '全部文件',
  209. onNavigate: (path) => {
  210. this.loadFiles();
  211. }
  212. });
  213. }
  214. // 初始化框选功能
  215. initSelection() {
  216. this.selection = new MultipleSelection({
  217. container: this.dropZone,
  218. itemsContainer: this.fileList,
  219. selectionBox: this.selectionBox,
  220. selectionBar: this.selectionBar,
  221. selectionCount: this.selectionCount,
  222. itemSelector: '.file-item',
  223. onSelectionChange: (selectedItems) => {
  224. // 选择变化时的回调(可用于其他逻辑)
  225. }
  226. });
  227. }
  228. initUploadProgress() {
  229. if (this.uploadProgressClose) {
  230. this.uploadProgressClose.addEventListener('click', () => this.hideUploadProgress());
  231. }
  232. this.hideUploadProgress();
  233. }
  234. initContextMenu() {
  235. if (!this.dropZone || !this.contextMenu) {
  236. return;
  237. }
  238. this.contextMenuManager = new RightClickMenu({
  239. target: this.dropZone,
  240. menu: this.contextMenu,
  241. onAction: (action, event) => this.handleContextMenuAction(action, event),
  242. onBeforeShow: (event) => this.handleBeforeShowContextMenu(event)
  243. });
  244. }
  245. handleBeforeShowContextMenu(event) {
  246. // 在显示右键菜单前,检查是否点击在文件项上
  247. const fileItem = event.target.closest('.file-item');
  248. // console.log('[Disk] 右键菜单即将显示');
  249. // console.log('[Disk] 点击目标:', event.target);
  250. // console.log('[Disk] 最近的文件项:', fileItem);
  251. if (fileItem) {
  252. // 如果点击在文件项上,确保它被选中
  253. const isSelected = this.selection.isSelected(fileItem.dataset.path);
  254. // console.log('[Disk] 文件项路径:', fileItem.dataset.path);
  255. // console.log('[Disk] 是否已选中:', isSelected);
  256. if (!isSelected) {
  257. // console.log('[Disk] → 自动选中该文件项');
  258. this.selection.selectOnly(fileItem);
  259. // console.log('[Disk] ✓ 文件项已选中');
  260. }
  261. } else {
  262. // console.log('[Disk] ⚠ 右键点击在空白区域');
  263. }
  264. }
  265. showUploadProgress() {
  266. if (this.uploadProgress) {
  267. this.uploadProgress.classList.add('show');
  268. }
  269. }
  270. hideUploadProgress() {
  271. if (this.uploadProgress) {
  272. this.uploadProgress.classList.remove('show');
  273. }
  274. if (this.uploadProgressList) {
  275. this.uploadProgressList.innerHTML = '';
  276. }
  277. }
  278. showGlobalLoading(text = '正在处理...') {
  279. // 直接调用父窗口的 GlobalLoading(不通过 postMessage)
  280. try {
  281. // 优先使用父窗口的 GlobalLoading
  282. if (window.parent && window.parent !== window && window.parent.GlobalLoading) {
  283. window.parent.GlobalLoading.show(text);
  284. return;
  285. }
  286. // 如果父窗口没有 GlobalLoading,尝试更上层的窗口
  287. let targetWindow = window.parent;
  288. while (targetWindow && targetWindow !== window) {
  289. if (targetWindow.GlobalLoading) {
  290. targetWindow.GlobalLoading.show(text);
  291. return;
  292. }
  293. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  294. targetWindow = targetWindow.parent;
  295. } else {
  296. break;
  297. }
  298. }
  299. } catch (error) {
  300. console.error('[Disk] 显示 loading 失败:', error);
  301. }
  302. }
  303. updateGlobalLoadingProgress(current, total, operation = '处理') {
  304. // 更新loading进度
  305. const text = `正在${operation} ${current}/${total} 张图片...`;
  306. this.showGlobalLoading(text);
  307. }
  308. hideGlobalLoading() {
  309. // 直接调用父窗口的 GlobalLoading(不通过 postMessage)
  310. try {
  311. // 优先使用父窗口的 GlobalLoading
  312. if (window.parent && window.parent !== window && window.parent.GlobalLoading) {
  313. window.parent.GlobalLoading.hide();
  314. return;
  315. }
  316. // 如果父窗口没有 GlobalLoading,尝试更上层的窗口
  317. let targetWindow = window.parent;
  318. while (targetWindow && targetWindow !== window) {
  319. if (targetWindow.GlobalLoading) {
  320. targetWindow.GlobalLoading.hide();
  321. return;
  322. }
  323. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  324. targetWindow = targetWindow.parent;
  325. } else {
  326. break;
  327. }
  328. }
  329. } catch (error) {
  330. console.error('[Disk] 隐藏 loading 失败:', error);
  331. }
  332. }
  333. showGlobalAlert(message) {
  334. // 直接调用父窗口的 GlobalAlert(不通过 postMessage)
  335. try {
  336. // 优先使用父窗口的 GlobalAlert(disk 在 assets.html 的 iframe 中)
  337. if (window.parent && window.parent !== window && window.parent.GlobalAlert) {
  338. window.parent.GlobalAlert.show(message, 3000);
  339. return;
  340. }
  341. // 如果父窗口没有 GlobalAlert,尝试更上层的窗口(index.html)
  342. let targetWindow = window.parent;
  343. while (targetWindow && targetWindow !== window) {
  344. if (targetWindow.GlobalAlert) {
  345. targetWindow.GlobalAlert.show(message, 3000);
  346. return;
  347. }
  348. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  349. targetWindow = targetWindow.parent;
  350. } else {
  351. break;
  352. }
  353. }
  354. // 降级处理:如果没有 GlobalAlert,使用 console
  355. console.log('[Alert]', message);
  356. } catch (error) {
  357. console.error('[Disk] 显示 alert 失败:', error);
  358. console.log('[Alert]', message);
  359. }
  360. }
  361. // 统一的错误处理函数:解析服务端错误响应并显示
  362. async handleServerError(response, defaultMessage = '操作失败') {
  363. let errorMessage = defaultMessage;
  364. try {
  365. // 尝试解析 JSON 错误响应
  366. const errorData = await response.json().catch(() => null);
  367. if (errorData) {
  368. // 优先使用服务端返回的 message
  369. if (errorData.message) {
  370. errorMessage = errorData.message;
  371. } else if (errorData.error) {
  372. errorMessage = errorData.error;
  373. } else if (typeof errorData === 'string') {
  374. errorMessage = errorData;
  375. }
  376. }
  377. } catch (e) {
  378. // 如果解析失败,使用默认消息或状态码
  379. if (response.status) {
  380. errorMessage = `${defaultMessage} (状态码: ${response.status})`;
  381. }
  382. }
  383. this.showGlobalAlert(errorMessage);
  384. return errorMessage;
  385. }
  386. showGlobalConfirm(message) {
  387. // 直接调用父窗口的 GlobalConfirm(不通过 postMessage)
  388. return new Promise((resolve) => {
  389. try {
  390. // 优先使用父窗口的 GlobalConfirm
  391. if (window.parent && window.parent !== window && window.parent.GlobalConfirm) {
  392. window.parent.GlobalConfirm.show(message).then((confirmed) => {
  393. resolve(confirmed);
  394. }).catch(() => {
  395. resolve(false);
  396. });
  397. return;
  398. }
  399. // 如果父窗口没有 GlobalConfirm,尝试更上层的窗口
  400. let targetWindow = window.parent;
  401. while (targetWindow && targetWindow !== window) {
  402. if (targetWindow.GlobalConfirm) {
  403. targetWindow.GlobalConfirm.show(message).then((confirmed) => {
  404. resolve(confirmed);
  405. }).catch(() => {
  406. resolve(false);
  407. });
  408. return;
  409. }
  410. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  411. targetWindow = targetWindow.parent;
  412. } else {
  413. break;
  414. }
  415. }
  416. // 如果找不到 GlobalConfirm,直接返回 false(不降级到原生 confirm)
  417. console.error('[Disk] 无法找到 GlobalConfirm,操作已取消');
  418. resolve(false);
  419. } catch (error) {
  420. console.error('[Disk] 显示 confirm 失败:', error);
  421. // 不降级到原生 confirm,直接返回 false
  422. resolve(false);
  423. }
  424. });
  425. }
  426. handleContextMenuAction(action) {
  427. console.log(`[Disk] 右键菜单动作: ${action}`);
  428. switch (action) {
  429. case 'new':
  430. this.createFolder();
  431. break;
  432. case 'cut':
  433. this.cutSelected();
  434. break;
  435. case 'copy':
  436. this.copySelected();
  437. break;
  438. case 'paste':
  439. this.pasteClipboard();
  440. break;
  441. case 'remove-bg':
  442. console.log('[Disk] 触发一键抠背景功能');
  443. this.removeBackgroundFromSelected();
  444. break;
  445. case 'crop-mini':
  446. console.log('[Disk] 触发剪裁最小区域功能');
  447. this.cropMiniFromSelected();
  448. break;
  449. default:
  450. console.warn('[Disk] 未知的菜单动作:', action);
  451. break;
  452. }
  453. }
  454. initShortcutKeys() {
  455. if (this.shortcutKeys) {
  456. this.shortcutKeys.destroy();
  457. }
  458. this.shortcutKeys = new ShortcutKeys({
  459. selection: this.selection,
  460. onDelete: () => this.deleteSelected(),
  461. onRename: () => this.renameSelected(),
  462. onCopy: () => this.copySelected(),
  463. onCut: () => this.cutSelected(),
  464. onPaste: () => this.pasteClipboard()
  465. });
  466. }
  467. initSearchBar() {
  468. this.searchBar = new SearchBar({
  469. input: this.searchInput,
  470. clearButton: this.searchClear,
  471. fileList: this.fileList,
  472. emptyState: this.emptyState,
  473. getFiles: () => this.files,
  474. renderAll: () => this.renderFiles(),
  475. createFileItem: (file) => this.createFileItem(file)
  476. });
  477. }
  478. bindEvents() {
  479. // 拖拽上传事件
  480. this.dropZone.addEventListener('dragenter', (e) => this.handleDragEnter(e));
  481. this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
  482. this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
  483. this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));
  484. document.addEventListener('dragend', () => this.resetDropState());
  485. // 按钮事件
  486. if (this.fileInput) {
  487. this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
  488. }
  489. if (this.btnCreateFolder) {
  490. this.btnCreateFolder.addEventListener('click', () => this.createFolder());
  491. }
  492. // 选择操作按钮
  493. this.btnDownload.addEventListener('click', () => this.downloadSelected());
  494. this.btnDelete.addEventListener('click', () => this.deleteSelected());
  495. // 上传按钮
  496. if (this.btnUpload) {
  497. this.btnUpload.addEventListener('click', () => {
  498. if (this.fileInput) {
  499. const currentPath = this.pathNav.getPath();
  500. const isRootDir = !currentPath || currentPath === '';
  501. // 根目录只允许上传文件夹
  502. if (isRootDir) {
  503. this.fileInput.setAttribute('webkitdirectory', '');
  504. this.fileInput.removeAttribute('accept');
  505. } else {
  506. this.fileInput.removeAttribute('webkitdirectory');
  507. this.fileInput.setAttribute('accept', 'image/*');
  508. }
  509. this.fileInput.click();
  510. }
  511. });
  512. }
  513. }
  514. // 下载选中的文件
  515. downloadSelected() {
  516. const selectedPaths = this.selection.getSelectedItems();
  517. selectedPaths.forEach(filePath => {
  518. const file = this.files.find(f => f.path === filePath);
  519. if (file && file.type !== 'directory') {
  520. this.downloadFile(filePath);
  521. }
  522. });
  523. }
  524. // 删除选中的文件
  525. async deleteSelected() {
  526. const count = this.selection.getSelectedCount();
  527. if (count === 0) return;
  528. const confirmMsg = `确定要删除选中的 ${count} 个文件/文件夹吗?`;
  529. const confirmed = await this.showGlobalConfirm(confirmMsg);
  530. if (!confirmed) {
  531. return;
  532. }
  533. try {
  534. const username = this.getCurrentUsername();
  535. if (!username) {
  536. this.showGlobalAlert('请先登录');
  537. return;
  538. }
  539. const response = await fetch('/api/disk/delete', {
  540. method: 'POST',
  541. headers: {
  542. 'Content-Type': 'application/json'
  543. },
  544. body: JSON.stringify({
  545. username: username,
  546. paths: this.selection.getSelectedItems()
  547. })
  548. });
  549. if (!response.ok) {
  550. await this.handleServerError(response, '删除失败');
  551. return;
  552. }
  553. const data = await response.json();
  554. if (data.success) {
  555. this.selection.clearSelection();
  556. this.loadFiles();
  557. } else {
  558. this.showGlobalAlert('删除失败: ' + (data.message || '未知错误'));
  559. }
  560. } catch (error) {
  561. this.showGlobalAlert('删除失败: ' + (error.message || '网络错误'));
  562. }
  563. }
  564. renameSelected() {
  565. if (!this.selection || this.selection.getSelectedCount() !== 1) {
  566. return;
  567. }
  568. const selectedPath = this.selection.getSelectedItems()[0];
  569. const selectedItem = this.fileList.querySelector(`[data-path="${selectedPath}"]`);
  570. if (selectedItem) {
  571. this.startRename(selectedItem);
  572. }
  573. }
  574. copySelected() {
  575. if (!this.selection || !this.selection.hasSelection()) {
  576. return false;
  577. }
  578. const selectedPaths = this.selection.getSelectedItems();
  579. this.clearCutVisuals();
  580. this.clipboard = {
  581. mode: 'copy',
  582. items: selectedPaths.map(path => ({ path }))
  583. };
  584. return true;
  585. }
  586. cutSelected() {
  587. if (!this.selection || !this.selection.hasSelection()) {
  588. return false;
  589. }
  590. const selectedPaths = this.selection.getSelectedItems();
  591. this.clipboard = {
  592. mode: 'cut',
  593. items: selectedPaths.map(path => ({ path }))
  594. };
  595. this.applyCutVisuals(selectedPaths);
  596. return true;
  597. }
  598. async pasteClipboard() {
  599. if (!this.clipboard || !this.clipboard.items || this.clipboard.items.length === 0) {
  600. return false;
  601. }
  602. const targetFolder = this.pathNav.getPath();
  603. const isCut = this.clipboard.mode === 'cut';
  604. let hasSuccess = false;
  605. for (const item of this.clipboard.items) {
  606. let result = false;
  607. if (isCut) {
  608. result = await this.moveFile(item.path, targetFolder, { suppressReload: true });
  609. } else {
  610. result = await this.copyFile(item.path, targetFolder, { suppressReload: true });
  611. }
  612. hasSuccess = hasSuccess || result;
  613. }
  614. if (hasSuccess) {
  615. await this.loadFiles();
  616. }
  617. if (isCut) {
  618. this.clipboard = null;
  619. this.clearCutVisuals();
  620. }
  621. return hasSuccess;
  622. }
  623. async removeBackgroundFromSelected() {
  624. console.log('\n' + '='.repeat(70));
  625. console.log('[Disk] 🎨 一键抠图功能被触发');
  626. console.log('='.repeat(70));
  627. const selectedPaths = this.selection.getSelectedItems();
  628. console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`);
  629. if (selectedPaths.length === 0) {
  630. console.warn('[Disk] ⚠ 没有选中任何项');
  631. this.showGlobalAlert('请先选择要抠图的文件夹');
  632. return;
  633. }
  634. // getSelectedItems() 返回的已经是路径数组
  635. console.log('[Disk] 选中的路径:', selectedPaths);
  636. // 确认操作
  637. const confirmMsg = selectedPaths.length === 1
  638. ? `一键抠背景会覆盖原文件,确定要对"${selectedPaths[0]}"进行处理吗?`
  639. : `一键抠背景会覆盖原文件,确定要对选中的 ${selectedPaths.length} 个文件夹进行处理吗?`;
  640. console.log('[Disk] → 准备显示确认对话框...');
  641. console.log('[Disk] 确认消息:', confirmMsg);
  642. // 通过父窗口显示全局确认对话框
  643. console.log('[Disk] → 调用 showGlobalConfirm()');
  644. const confirmed = await this.showGlobalConfirm(confirmMsg);
  645. console.log('[Disk] ← showGlobalConfirm() 返回,结果:', confirmed);
  646. if (!confirmed) {
  647. console.log('[Disk] ✗ 用户取消操作,函数返回');
  648. return;
  649. }
  650. console.log('[Disk] ✓ 用户确认操作,继续执行');
  651. // 计算总图片数
  652. let totalPngCount = 0;
  653. selectedPaths.forEach(path => {
  654. const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path);
  655. console.log('[Disk] 检查文件:', path, '信息:', fileInfo);
  656. if (fileInfo && fileInfo.pngCount) {
  657. console.log('[Disk] pngCount:', fileInfo.pngCount);
  658. totalPngCount += fileInfo.pngCount;
  659. }
  660. });
  661. console.log('[Disk] 总PNG数量:', totalPngCount);
  662. // 显示loading(带总数信息)
  663. console.log('[Disk] → 显示全局Loading...');
  664. const loadingText = totalPngCount > 0
  665. ? `正在处理 0/${totalPngCount} 张图片...`
  666. : '正在处理中,请稍候...';
  667. this.showGlobalLoading(loadingText);
  668. console.log('[Disk] ✓ Loading已显示');
  669. // 保存总数,用于更新进度
  670. this.currentProcessTotal = totalPngCount;
  671. try {
  672. console.log('[Disk] → 准备发送HTTP请求到服务器');
  673. console.log('[Disk] URL: /api/disk/remove-background');
  674. console.log('[Disk] 方法: POST');
  675. console.log('[Disk] 数据:', { paths: selectedPaths });
  676. const username = this.getCurrentUsername();
  677. if (!username) {
  678. this.showGlobalAlert('请先登录');
  679. return;
  680. }
  681. // 使用fetch读取SSE流
  682. const response = await fetch('/api/disk/remove-background', {
  683. method: 'POST',
  684. headers: {
  685. 'Content-Type': 'application/json'
  686. },
  687. body: JSON.stringify({ username: username, paths: selectedPaths })
  688. });
  689. console.log('[Disk] ✓ HTTP响应收到');
  690. console.log('[Disk] 状态码:', response.status);
  691. if (!response.ok) {
  692. this.hideGlobalLoading();
  693. await this.handleServerError(response, '处理失败');
  694. return;
  695. }
  696. // 读取SSE流
  697. console.log('[Disk] → 开始读取SSE流...');
  698. const reader = response.body.getReader();
  699. const decoder = new TextDecoder();
  700. let buffer = '';
  701. let finalResult = null;
  702. while (true) {
  703. const { done, value } = await reader.read();
  704. if (done) {
  705. console.log('[Disk] ✓ SSE流读取完成');
  706. break;
  707. }
  708. const chunk = decoder.decode(value, { stream: true });
  709. console.log('[Disk] ← 收到数据块:', chunk.substring(0, 100));
  710. buffer += chunk;
  711. const lines = buffer.split('\n\n');
  712. buffer = lines.pop() || '';
  713. for (const line of lines) {
  714. if (line.startsWith('data: ')) {
  715. const jsonStr = line.substring(6);
  716. try {
  717. const data = JSON.parse(jsonStr);
  718. console.log('[Disk] ← 解析到事件:', data);
  719. if (data.type === 'image-progress') {
  720. // 更新进度
  721. console.log(`[Disk] → 更新进度: ${data.current}/${data.total}`);
  722. this.updateGlobalLoadingProgress(data.current, data.total, '处理');
  723. } else if (data.type === 'complete') {
  724. console.log('[Disk] ← 收到完成事件');
  725. finalResult = data;
  726. } else if (data.type === 'error') {
  727. console.error('[Disk] ← 收到错误事件');
  728. throw new Error(data.message);
  729. }
  730. } catch (e) {
  731. console.error('[Disk] 解析SSE数据失败:', e, '原始数据:', jsonStr);
  732. }
  733. }
  734. }
  735. }
  736. console.log('[Disk] ✓ 最终结果:', finalResult);
  737. if (finalResult && finalResult.success) {
  738. console.log('[Disk] ✓✓✓ 服务器处理成功!');
  739. console.log('[Disk] 处理文件夹数:', finalResult.folders || 0);
  740. console.log('[Disk] 处理图片数:', finalResult.processed || 0);
  741. console.log('[Disk] → 隐藏Loading...');
  742. this.hideGlobalLoading();
  743. console.log('[Disk] ✓ Loading已隐藏');
  744. const message = `处理完成!\n共处理 ${finalResult.processed || 0} 张图片`;
  745. console.log('[Disk] → 显示成功提示');
  746. this.showGlobalAlert(message);
  747. console.log('[Disk] → 清除图片缓存...');
  748. await this.clearImageCache(selectedPaths);
  749. console.log('[Disk] ✓ 缓存已清除');
  750. console.log('[Disk] → 刷新文件列表以更新预览图...');
  751. await this.loadFiles();
  752. console.log('[Disk] ✓ 文件列表已刷新');
  753. console.log('='.repeat(70));
  754. console.log('[Disk] 🎉🎉🎉 一键抠图完成!');
  755. console.log('='.repeat(70) + '\n');
  756. } else {
  757. throw new Error(finalResult?.message || '处理失败');
  758. }
  759. } catch (error) {
  760. console.error('\n' + '='.repeat(70));
  761. console.error('[Disk] ✗✗✗ 客户端处理失败');
  762. console.error('[Disk] 错误信息:', error.message);
  763. console.error('[Disk] 错误堆栈:', error.stack);
  764. console.error('='.repeat(70) + '\n');
  765. this.hideGlobalLoading();
  766. this.showGlobalAlert('处理失败: ' + error.message);
  767. }
  768. }
  769. async cropMiniFromSelected() {
  770. console.log('[Disk] 剪裁最小区域功能被触发');
  771. const selectedPaths = this.selection.getSelectedItems();
  772. console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`);
  773. if (selectedPaths.length === 0) {
  774. this.showGlobalAlert('请先选择要剪裁的文件夹');
  775. return;
  776. }
  777. // 计算总图片数
  778. let totalPngCount = 0;
  779. selectedPaths.forEach(path => {
  780. const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path);
  781. if (fileInfo && fileInfo.pngCount) {
  782. totalPngCount += fileInfo.pngCount;
  783. }
  784. });
  785. // 确认操作
  786. const confirmMsg = selectedPaths.length === 1
  787. ? `确定要对"${selectedPaths[0]}"进行剪裁吗?`
  788. : `确定要对选中的 ${selectedPaths.length} 个文件夹进行剪裁吗?`;
  789. const confirmed = await this.showGlobalConfirm(confirmMsg);
  790. if (!confirmed) {
  791. return;
  792. }
  793. // 显示loading
  794. const loadingText = totalPngCount > 0
  795. ? `正在剪裁 0/${totalPngCount} 张图片...`
  796. : '正在剪裁中,请稍候...';
  797. this.showGlobalLoading(loadingText);
  798. // 保存总数,用于更新进度
  799. this.currentProcessTotal = totalPngCount;
  800. try {
  801. const username = this.getCurrentUsername();
  802. if (!username) {
  803. this.showGlobalAlert('请先登录');
  804. return;
  805. }
  806. // 使用fetch读取SSE流
  807. const response = await fetch('/api/disk/crop-mini', {
  808. method: 'POST',
  809. headers: {
  810. 'Content-Type': 'application/json'
  811. },
  812. body: JSON.stringify({ username: username, paths: selectedPaths })
  813. });
  814. if (!response.ok) {
  815. this.hideGlobalLoading();
  816. await this.handleServerError(response, '剪裁失败');
  817. return;
  818. }
  819. // 读取SSE流
  820. const reader = response.body.getReader();
  821. const decoder = new TextDecoder();
  822. let buffer = '';
  823. let finalResult = null;
  824. while (true) {
  825. const { done, value } = await reader.read();
  826. if (done) break;
  827. buffer += decoder.decode(value, { stream: true });
  828. const lines = buffer.split('\n\n');
  829. buffer = lines.pop() || '';
  830. for (const line of lines) {
  831. if (line.startsWith('data: ')) {
  832. const jsonStr = line.substring(6);
  833. try {
  834. const data = JSON.parse(jsonStr);
  835. if (data.type === 'image-progress') {
  836. this.updateGlobalLoadingProgress(data.current, data.total, '剪裁');
  837. } else if (data.type === 'complete') {
  838. finalResult = data;
  839. } else if (data.type === 'error') {
  840. throw new Error(data.message);
  841. }
  842. } catch (e) {
  843. console.error('[Disk] 解析SSE数据失败:', e);
  844. }
  845. }
  846. }
  847. }
  848. if (finalResult && finalResult.success) {
  849. this.hideGlobalLoading();
  850. const message = `剪裁完成!\n共处理 ${finalResult.processed || 0} 张图片`;
  851. this.showGlobalAlert(message);
  852. // 清除缓存并刷新
  853. await this.clearImageCache(selectedPaths);
  854. await this.loadFiles();
  855. } else {
  856. throw new Error(finalResult?.message || '剪裁失败');
  857. }
  858. } catch (error) {
  859. console.error('[Disk] 剪裁失败:', error);
  860. this.hideGlobalLoading();
  861. this.showGlobalAlert('剪裁失败: ' + error.message);
  862. }
  863. }
  864. applyCutVisuals(paths = []) {
  865. this.clearCutVisuals();
  866. paths.forEach(path => {
  867. const item = this.fileList.querySelector(`[data-path="${path}"]`);
  868. if (item) {
  869. item.classList.add('cut');
  870. }
  871. });
  872. }
  873. clearCutVisuals() {
  874. this.fileList.querySelectorAll('.file-item.cut').forEach(item => item.classList.remove('cut'));
  875. }
  876. clearSearch(options) {
  877. if (this.searchBar) {
  878. this.searchBar.clear(options);
  879. }
  880. }
  881. // 检查是否是外部文件拖入(而非内部拖拽操作)
  882. isExternalFileDrag(e) {
  883. return e.dataTransfer.types.includes('Files') &&
  884. !e.dataTransfer.types.includes('text/plain');
  885. }
  886. // 拖拽处理
  887. handleDragEnter(e) {
  888. e.preventDefault();
  889. e.stopPropagation();
  890. if (this.isExternalFileDrag(e)) {
  891. this.dropZone.classList.add('drag-over');
  892. this.dragCounter++;
  893. }
  894. }
  895. handleDragOver(e) {
  896. e.preventDefault();
  897. e.stopPropagation();
  898. }
  899. handleDragLeave(e) {
  900. e.preventDefault();
  901. e.stopPropagation();
  902. if (this.isExternalFileDrag(e)) {
  903. this.dragCounter = Math.max(0, this.dragCounter - 1);
  904. if (this.dragCounter === 0) {
  905. this.dropZone.classList.remove('drag-over');
  906. }
  907. }
  908. }
  909. async handleDrop(e) {
  910. e.preventDefault();
  911. e.stopPropagation();
  912. this.dropZone.classList.remove('drag-over');
  913. this.dragCounter = 0;
  914. const items = e.dataTransfer.items;
  915. if (!items) return;
  916. const entries = [];
  917. for (let i = 0; i < items.length; i++) {
  918. const item = items[i].webkitGetAsEntry();
  919. if (item) {
  920. entries.push(item);
  921. }
  922. }
  923. await this.processEntries(entries);
  924. }
  925. resetDropState() {
  926. this.dragCounter = 0;
  927. this.dropZone.classList.remove('drag-over');
  928. }
  929. async processEntries(entries) {
  930. const currentPath = this.pathNav.getPath();
  931. const isRootDir = !currentPath || currentPath === '';
  932. // 检查是否在根目录上传文件(非文件夹)
  933. if (isRootDir) {
  934. const hasFiles = entries.some(entry => entry.isFile);
  935. if (hasFiles) {
  936. this.showGlobalAlert('根目录只允许上传文件夹');
  937. return;
  938. }
  939. }
  940. const filesToUpload = [];
  941. for (const entry of entries) {
  942. await this.traverseEntry(entry, '', filesToUpload);
  943. }
  944. if (filesToUpload.length > 0) {
  945. // 显示确认对话框
  946. const folderName = entries.length === 1 && entries[0].isDirectory
  947. ? entries[0].name
  948. : '选中的文件';
  949. const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`;
  950. const confirmed = await this.showGlobalConfirm(confirmMsg);
  951. if (!confirmed) {
  952. return;
  953. }
  954. await this.uploadFiles(filesToUpload);
  955. }
  956. }
  957. async traverseEntry(entry, relativePath, filesToUpload) {
  958. if (entry.isFile) {
  959. const file = await new Promise((resolve) => {
  960. entry.file(resolve);
  961. });
  962. // 只接受图片格式的文件
  963. if (this.isImageFile(file.name)) {
  964. filesToUpload.push({
  965. file: file,
  966. path: relativePath + file.name
  967. });
  968. }
  969. // 非图片格式的文件直接跳过
  970. } else if (entry.isDirectory) {
  971. const dirReader = entry.createReader();
  972. const entries = await new Promise((resolve) => {
  973. dirReader.readEntries(resolve);
  974. });
  975. for (const childEntry of entries) {
  976. await this.traverseEntry(
  977. childEntry,
  978. relativePath + entry.name + '/',
  979. filesToUpload
  980. );
  981. }
  982. }
  983. }
  984. async handleFileSelect(e) {
  985. const currentPath = this.pathNav.getPath();
  986. const isRootDir = !currentPath || currentPath === '';
  987. const files = Array.from(e.target.files);
  988. if (files.length === 0) {
  989. this.fileInput.value = '';
  990. return;
  991. }
  992. // 检查登录状态
  993. const username = this.getCurrentUsername();
  994. if (!username) {
  995. this.showGlobalAlert('请先登录');
  996. this.fileInput.value = '';
  997. return;
  998. }
  999. // 只接受图片格式的文件,其他格式直接跳过
  1000. const filesToUpload = files
  1001. .filter(file => this.isImageFile(file.name))
  1002. .map(file => ({
  1003. file: file,
  1004. path: file.webkitRelativePath || file.name
  1005. }));
  1006. if (filesToUpload.length > 0) {
  1007. // 显示确认对话框
  1008. const folderName = files[0].webkitRelativePath
  1009. ? files[0].webkitRelativePath.split('/')[0]
  1010. : '选中的文件';
  1011. const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`;
  1012. const confirmed = await this.showGlobalConfirm(confirmMsg);
  1013. if (!confirmed) {
  1014. this.fileInput.value = '';
  1015. return;
  1016. }
  1017. await this.uploadFiles(filesToUpload);
  1018. }
  1019. this.fileInput.value = '';
  1020. }
  1021. async uploadFiles(filesToUpload) {
  1022. if (!filesToUpload.length) return;
  1023. // 显示全局loading遮罩
  1024. this.showGlobalLoading(`正在上传 ${filesToUpload.length} 个文件...`);
  1025. let hasSuccess = false;
  1026. for (let i = 0; i < filesToUpload.length; i++) {
  1027. const fileData = filesToUpload[i];
  1028. // 更新进度文本
  1029. this.showGlobalLoading(`正在上传... (${i + 1}/${filesToUpload.length})`);
  1030. try {
  1031. await this.uploadFile(fileData, null);
  1032. hasSuccess = true;
  1033. // 每上传成功一个文件就立即刷新显示(静默刷新,不显示加载动画)
  1034. await this.loadFilesQuietly();
  1035. } catch (error) {
  1036. console.error('上传失败:', error);
  1037. }
  1038. }
  1039. // 所有文件处理完成后,隐藏loading
  1040. this.hideGlobalLoading();
  1041. if (hasSuccess) {
  1042. // 上传成功后刷新文件列表
  1043. await this.loadFiles();
  1044. }
  1045. }
  1046. async uploadFile(fileData, progressItem) {
  1047. const username = this.getCurrentUsername();
  1048. if (!username) {
  1049. throw new Error('请先登录');
  1050. }
  1051. const formData = new FormData();
  1052. formData.append('file', fileData.file);
  1053. formData.append('path', this.pathNav.getPath());
  1054. formData.append('relativePath', fileData.path);
  1055. formData.append('username', username);
  1056. const response = await fetch('/api/disk/upload', {
  1057. method: 'POST',
  1058. body: formData
  1059. });
  1060. if (!response.ok) {
  1061. throw new Error('上传失败');
  1062. }
  1063. return response.json();
  1064. }
  1065. createProgressItem(fileName) {
  1066. if (!this.uploadProgressTemplate || !this.uploadProgressList) {
  1067. return null;
  1068. }
  1069. const fragment = this.uploadProgressTemplate.content.cloneNode(true);
  1070. const item = fragment.querySelector('.upload-progress-item');
  1071. if (!item) {
  1072. return null;
  1073. }
  1074. const nameEl = item.querySelector('.upload-progress-name');
  1075. if (nameEl) {
  1076. nameEl.textContent = fileName;
  1077. }
  1078. this.uploadProgressList.appendChild(fragment);
  1079. return this.uploadProgressList.lastElementChild;
  1080. }
  1081. updateProgressItem(item, status, message) {
  1082. if (!item) return;
  1083. const statusEl = item.querySelector('.upload-progress-status');
  1084. const barFill = item.querySelector('.upload-progress-bar-fill');
  1085. if (statusEl) {
  1086. statusEl.textContent = message;
  1087. }
  1088. if (!barFill) return;
  1089. if (status === 'success') {
  1090. barFill.style.width = '100%';
  1091. barFill.style.background = '#52c41a';
  1092. } else if (status === 'error') {
  1093. barFill.style.background = '#ff4d4f';
  1094. }
  1095. }
  1096. // 初始化文件结构缓存(递归加载所有文件夹信息)
  1097. async initFileStructureCache() {
  1098. if (this.cacheInitialized) {
  1099. return;
  1100. }
  1101. try {
  1102. // console.log('[Disk] 开始初始化文件结构缓存...');
  1103. const username = this.getCurrentUsername();
  1104. if (!username) {
  1105. console.warn('[Disk] 未登录,无法初始化缓存');
  1106. return;
  1107. }
  1108. const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=&recursive=true`);
  1109. if (!response.ok) {
  1110. await this.handleServerError(response, '加载文件列表失败');
  1111. return;
  1112. }
  1113. const data = await response.json();
  1114. if (data.success && data.files) {
  1115. // 构建缓存 Map
  1116. this.fileStructureCache.clear();
  1117. data.files.forEach(file => {
  1118. this.fileStructureCache.set(file.path, file);
  1119. });
  1120. this.cacheInitialized = true;
  1121. // console.log(`[Disk] 文件结构缓存初始化完成,共 ${this.fileStructureCache.size} 个项目`);
  1122. } else if (!data.success && data.message) {
  1123. this.showGlobalAlert('加载文件列表失败: ' + data.message);
  1124. }
  1125. } catch (error) {
  1126. this.showGlobalAlert('加载文件列表失败: ' + (error.message || '网络错误'));
  1127. }
  1128. }
  1129. // 从缓存中获取文件信息
  1130. getFileFromCache(filePath) {
  1131. return this.fileStructureCache.get(filePath);
  1132. }
  1133. // 更新缓存中的单个文件信息
  1134. updateCacheItem(filePath, fileInfo) {
  1135. this.fileStructureCache.set(filePath, fileInfo);
  1136. }
  1137. // 从缓存中移除文件信息
  1138. removeCacheItem(filePath) {
  1139. this.fileStructureCache.delete(filePath);
  1140. }
  1141. // 清除图片缓存(用于抠图后更新预览)
  1142. async clearImageCache(folderPaths) {
  1143. try {
  1144. // 清除浏览器的图片缓存
  1145. if ('caches' in window) {
  1146. const cacheNames = await caches.keys();
  1147. for (const cacheName of cacheNames) {
  1148. const cache = await caches.open(cacheName);
  1149. const requests = await cache.keys();
  1150. for (const request of requests) {
  1151. const url = request.url;
  1152. // 检查是否是处理过的文件夹的图片
  1153. for (const folderPath of folderPaths) {
  1154. if (url.includes(encodeURIComponent(folderPath))) {
  1155. await cache.delete(request);
  1156. console.log('[Disk] 清除缓存:', url);
  1157. }
  1158. }
  1159. }
  1160. }
  1161. }
  1162. } catch (error) {
  1163. console.error('[Disk] 清除缓存失败:', error);
  1164. }
  1165. }
  1166. // 加载文件列表
  1167. async loadFiles() {
  1168. this.showLoading(true);
  1169. this.selection.clearSelection();
  1170. this.clearSearch();
  1171. try {
  1172. const username = this.getCurrentUsername();
  1173. if (!username) {
  1174. this.showGlobalAlert('请先登录');
  1175. this.showLoading(false);
  1176. return;
  1177. }
  1178. const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`);
  1179. if (!response.ok) {
  1180. await this.handleServerError(response, '加载文件列表失败');
  1181. return;
  1182. }
  1183. const data = await response.json();
  1184. if (data.success) {
  1185. this.files = data.files;
  1186. // 更新缓存
  1187. data.files.forEach(file => {
  1188. this.updateCacheItem(file.path, file);
  1189. });
  1190. this.renderFiles();
  1191. } else if (data.message) {
  1192. this.showGlobalAlert('加载文件列表失败: ' + data.message);
  1193. }
  1194. } catch (error) {
  1195. this.showGlobalAlert('加载文件列表失败: ' + (error.message || '网络错误'));
  1196. } finally {
  1197. this.showLoading(false);
  1198. }
  1199. }
  1200. // 静默加载文件列表(用于上传过程中增量更新,不显示加载动画,不闪烁)
  1201. async loadFilesQuietly() {
  1202. try {
  1203. const username = this.getCurrentUsername();
  1204. if (!username) {
  1205. return;
  1206. }
  1207. const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`);
  1208. if (!response.ok) {
  1209. // 静默加载失败时不显示错误,避免干扰用户
  1210. return;
  1211. }
  1212. const data = await response.json();
  1213. if (data.success) {
  1214. this.files = data.files;
  1215. this.renderFilesSmooth();
  1216. }
  1217. } catch (error) {
  1218. console.error('加载文件列表失败:', error);
  1219. }
  1220. }
  1221. renderFiles() {
  1222. this.fileList.innerHTML = '';
  1223. if (this.files.length === 0) {
  1224. this.emptyState.classList.add('show');
  1225. return;
  1226. }
  1227. this.emptyState.classList.remove('show');
  1228. this.files.forEach(file => {
  1229. const fileItem = this.createFileItem(file);
  1230. this.fileList.appendChild(fileItem);
  1231. });
  1232. }
  1233. // 平滑渲染文件列表(增量更新,避免闪烁)
  1234. renderFilesSmooth() {
  1235. if (this.files.length === 0) {
  1236. this.emptyState.classList.add('show');
  1237. this.fileList.innerHTML = '';
  1238. return;
  1239. }
  1240. this.emptyState.classList.remove('show');
  1241. // 获取当前已存在的文件路径
  1242. const existingPaths = new Set();
  1243. const existingItems = this.fileList.querySelectorAll('.file-item');
  1244. existingItems.forEach(item => {
  1245. existingPaths.add(item.dataset.path);
  1246. });
  1247. // 创建新文件路径集合
  1248. const newPaths = new Set(this.files.map(f => f.path));
  1249. // 删除不存在的文件项
  1250. existingItems.forEach(item => {
  1251. if (!newPaths.has(item.dataset.path)) {
  1252. item.remove();
  1253. }
  1254. });
  1255. // 只添加新文件项
  1256. this.files.forEach(file => {
  1257. if (!existingPaths.has(file.path)) {
  1258. const fileItem = this.createFileItem(file);
  1259. this.fileList.appendChild(fileItem);
  1260. }
  1261. });
  1262. }
  1263. createFileItem(file) {
  1264. const div = document.createElement('div');
  1265. div.className = 'file-item';
  1266. div.dataset.name = file.name;
  1267. div.dataset.type = file.type;
  1268. div.dataset.path = file.path;
  1269. div.draggable = true;
  1270. const size = file.type === 'directory' ? '' : this.formatFileSize(file.size);
  1271. const isImage = this.isImageFile(file.name);
  1272. // 勾选标记
  1273. const checkMark = `
  1274. <div class="check-mark">
  1275. <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
  1276. <path d="M10 3L4.5 8.5 2 6" stroke="white" stroke-width="2" fill="none"/>
  1277. </svg>
  1278. </div>
  1279. `;
  1280. // 检查是否是包含PNG的文件夹(通过名称模式判断)
  1281. const isAnimationFolder = file.type === 'directory' && this.isAnimationFolder(file.name);
  1282. if (isAnimationFolder) {
  1283. // 动画文件夹显示第一帧预览
  1284. // 使用服务器提供的预览信息(完全避免404错误)
  1285. if (file.hasPreview && file.previewUrl) {
  1286. // 服务器确认有预览图,直接使用服务器提供的URL
  1287. // 确保 URL 包含用户名参数(如果服务器没有提供)
  1288. const username = this.getCurrentUsername();
  1289. let previewUrl = file.previewUrl;
  1290. if (username && !previewUrl.includes('username=')) {
  1291. // 如果 URL 中没有用户名参数,添加它
  1292. const separator = previewUrl.includes('?') ? '&' : '?';
  1293. previewUrl = previewUrl + separator + 'username=' + encodeURIComponent(username);
  1294. }
  1295. // 添加时间戳防止缓存
  1296. previewUrl = previewUrl + (previewUrl.includes('?') ? '&' : '?') + 't=' + Date.now();
  1297. // console.log(`[Disk] 文件夹 ${file.name} 有预览图`);
  1298. div.innerHTML = `
  1299. ${checkMark}
  1300. <div class="file-thumbnail folder-preview">
  1301. <img src="${previewUrl}" alt="${file.name}" loading="lazy">
  1302. <div class="folder-badge">📁</div>
  1303. </div>
  1304. <div class="file-name">${file.name}</div>
  1305. <input type="text" class="rename-input" style="display: none;">
  1306. `;
  1307. } else {
  1308. // 服务器确认没有预览图,直接显示文件夹图标
  1309. // console.log(`[Disk] 文件夹 ${file.name} 无预览图,显示图标`);
  1310. const icon = this.getFileIcon(file);
  1311. div.innerHTML = `
  1312. ${checkMark}
  1313. <div class="file-icon">
  1314. ${icon}
  1315. </div>
  1316. <div class="file-name">${file.name}</div>
  1317. <input type="text" class="rename-input" style="display: none;">
  1318. `;
  1319. }
  1320. } else if (isImage && file.type !== 'directory') {
  1321. // 添加时间戳防止缓存
  1322. const username = this.getCurrentUsername();
  1323. const previewUrl = username
  1324. ? `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}&t=${Date.now()}`
  1325. : `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`;
  1326. div.innerHTML = `
  1327. ${checkMark}
  1328. <div class="file-thumbnail">
  1329. <img src="${previewUrl}" alt="${file.name}" loading="lazy">
  1330. </div>
  1331. <div class="file-name">${file.name}</div>
  1332. <input type="text" class="rename-input" style="display: none;">
  1333. ${size ? `<div class="file-info">${size}</div>` : ''}
  1334. `;
  1335. } else {
  1336. const icon = this.getFileIcon(file);
  1337. div.innerHTML = `
  1338. ${checkMark}
  1339. <div class="file-icon">
  1340. ${icon}
  1341. </div>
  1342. <div class="file-name">${file.name}</div>
  1343. <input type="text" class="rename-input" style="display: none;">
  1344. ${size ? `<div class="file-info">${size}</div>` : ''}
  1345. `;
  1346. }
  1347. // 用于区分单击和双击的定时器
  1348. let clickTimer = null;
  1349. // 单击选中(Windows 11 行为)
  1350. div.addEventListener('click', (e) => {
  1351. // 如果正在重命名,不处理
  1352. if (e.target.classList.contains('rename-input')) {
  1353. return;
  1354. }
  1355. e.stopPropagation();
  1356. const isAlreadySelected = this.selection.isSelected(file.path);
  1357. const clickedOnName = e.target.classList.contains('file-name');
  1358. const ctrlKey = e.ctrlKey;
  1359. // 清除之前的定时器
  1360. if (clickTimer) {
  1361. clearTimeout(clickTimer);
  1362. clickTimer = null;
  1363. }
  1364. // 延迟执行单击操作,给双击留出时间
  1365. clickTimer = setTimeout(() => {
  1366. clickTimer = null;
  1367. if (ctrlKey) {
  1368. // Ctrl+点击:切换选中状态(多选)
  1369. this.selection.toggleSelection(div);
  1370. } else if (clickedOnName && isAlreadySelected) {
  1371. // 点击已选中项的名称:触发重命名
  1372. this.startRename(div);
  1373. } else {
  1374. // 普通点击:选中当前项,取消其他选择
  1375. this.selection.selectOnly(div);
  1376. }
  1377. }, 200);
  1378. });
  1379. // 双击进入文件夹或打开文件(整个项目都可以双击)
  1380. div.addEventListener('dblclick', (e) => {
  1381. // 如果正在重命名,不处理
  1382. if (e.target.classList.contains('rename-input')) {
  1383. return;
  1384. }
  1385. e.stopPropagation();
  1386. // 取消单击的定时器,防止双击时执行单击操作
  1387. if (clickTimer) {
  1388. clearTimeout(clickTimer);
  1389. clickTimer = null;
  1390. }
  1391. if (file.type === 'directory') {
  1392. this.navigateToPath(file.path);
  1393. } else {
  1394. this.downloadFile(file.path);
  1395. }
  1396. });
  1397. // 拖拽开始
  1398. div.addEventListener('dragstart', (e) => {
  1399. e.stopPropagation();
  1400. div.classList.add('dragging');
  1401. e.dataTransfer.effectAllowed = 'move';
  1402. e.dataTransfer.setData('text/plain', JSON.stringify({
  1403. type: file.type, // 'directory' 或 'file'
  1404. path: file.path,
  1405. name: file.name,
  1406. isDirectory: file.type === 'directory',
  1407. pngCount: file.pngCount || 0, // PNG文件数量
  1408. hasPreview: file.hasPreview || false, // 是否有预览图
  1409. needsMatting: file.needsMatting || false // 是否需要抠图
  1410. }));
  1411. });
  1412. div.addEventListener('dragend', () => {
  1413. div.classList.remove('dragging');
  1414. document.querySelectorAll('.file-item.drag-target').forEach(el => {
  1415. el.classList.remove('drag-target');
  1416. });
  1417. });
  1418. // 文件夹可以接收拖拽
  1419. if (file.type === 'directory') {
  1420. div.addEventListener('dragover', (e) => {
  1421. e.preventDefault();
  1422. e.stopPropagation();
  1423. const draggingEl = document.querySelector('.file-item.dragging');
  1424. if (draggingEl && draggingEl !== div) {
  1425. div.classList.add('drag-target');
  1426. e.dataTransfer.dropEffect = 'move';
  1427. }
  1428. });
  1429. div.addEventListener('dragleave', (e) => {
  1430. e.stopPropagation();
  1431. div.classList.remove('drag-target');
  1432. });
  1433. div.addEventListener('drop', async (e) => {
  1434. e.preventDefault();
  1435. e.stopPropagation();
  1436. div.classList.remove('drag-target');
  1437. try {
  1438. const data = JSON.parse(e.dataTransfer.getData('text/plain'));
  1439. if (data.type === 'move-file' && data.path !== file.path) {
  1440. await this.moveFile(data.path, file.path);
  1441. }
  1442. } catch (error) {
  1443. // 可能是外部文件拖拽,忽略
  1444. }
  1445. });
  1446. }
  1447. return div;
  1448. }
  1449. // 移动文件/文件夹
  1450. async moveFile(sourcePath, targetFolder, options = {}) {
  1451. const { suppressReload = false, silent = false } = options;
  1452. const username = this.getCurrentUsername();
  1453. if (!username) {
  1454. if (!silent) {
  1455. this.showGlobalAlert('请先登录');
  1456. }
  1457. return;
  1458. }
  1459. try {
  1460. const response = await fetch('/api/disk/move', {
  1461. method: 'POST',
  1462. headers: {
  1463. 'Content-Type': 'application/json'
  1464. },
  1465. body: JSON.stringify({
  1466. username: username,
  1467. sourcePath,
  1468. targetFolder
  1469. })
  1470. });
  1471. if (!response.ok) {
  1472. if (!silent) {
  1473. await this.handleServerError(response, '移动失败');
  1474. }
  1475. return false;
  1476. }
  1477. const data = await response.json();
  1478. if (data.success) {
  1479. if (!suppressReload) {
  1480. await this.loadFiles();
  1481. }
  1482. return true;
  1483. } else {
  1484. if (!silent) {
  1485. this.showGlobalAlert('移动失败: ' + (data.message || '未知错误'));
  1486. }
  1487. return false;
  1488. }
  1489. } catch (error) {
  1490. if (!silent) {
  1491. this.showGlobalAlert('移动失败: ' + (error.message || '网络错误'));
  1492. }
  1493. return false;
  1494. }
  1495. }
  1496. getFileIcon(file) {
  1497. if (file.type === 'directory') {
  1498. return `
  1499. <svg class="folder-icon" viewBox="0 0 64 64" fill="currentColor">
  1500. <path d="M8 12h20l4 6h24a4 4 0 014 4v28a4 4 0 01-4 4H8a4 4 0 01-4-4V16a4 4 0 014-4z"/>
  1501. </svg>
  1502. `;
  1503. }
  1504. const ext = file.name.split('.').pop().toLowerCase();
  1505. if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
  1506. return `
  1507. <svg class="file-icon-image" viewBox="0 0 64 64" fill="currentColor">
  1508. <path d="M12 8h40a4 4 0 014 4v40a4 4 0 01-4 4H12a4 4 0 01-4-4V12a4 4 0 014-4z"/>
  1509. <circle cx="20" cy="20" r="4" fill="white"/>
  1510. <path d="M8 48l16-16 12 12 16-20v24H8z" fill="white" opacity="0.8"/>
  1511. </svg>
  1512. `;
  1513. }
  1514. if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
  1515. return `
  1516. <svg class="file-icon-video" viewBox="0 0 64 64" fill="currentColor">
  1517. <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
  1518. <path d="M24 20l20 12-20 12z" fill="white"/>
  1519. </svg>
  1520. `;
  1521. }
  1522. if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) {
  1523. return `
  1524. <svg class="file-icon-audio" viewBox="0 0 64 64" fill="currentColor">
  1525. <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
  1526. <path d="M24 20h4v20a6 6 0 11-4-5.66V20zm20-4v20a6 6 0 11-4-5.66V16h4z" fill="white"/>
  1527. </svg>
  1528. `;
  1529. }
  1530. if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
  1531. return `
  1532. <svg class="file-icon-zip" viewBox="0 0 64 64" fill="currentColor">
  1533. <path d="M16 8h32a4 4 0 014 4v40a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
  1534. <rect x="28" y="12" width="8" height="4" fill="white"/>
  1535. <rect x="28" y="20" width="8" height="4" fill="white"/>
  1536. <rect x="28" y="28" width="8" height="4" fill="white"/>
  1537. <rect x="28" y="36" width="8" height="8" rx="2" fill="white"/>
  1538. </svg>
  1539. `;
  1540. }
  1541. if (['doc', 'docx', 'txt', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
  1542. return `
  1543. <svg class="file-icon-document" viewBox="0 0 64 64" fill="currentColor">
  1544. <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
  1545. <path d="M40 8v12h12" fill="white" opacity="0.5"/>
  1546. <rect x="20" y="28" width="24" height="2" fill="white"/>
  1547. <rect x="20" y="34" width="24" height="2" fill="white"/>
  1548. <rect x="20" y="40" width="16" height="2" fill="white"/>
  1549. </svg>
  1550. `;
  1551. }
  1552. return `
  1553. <svg class="file-icon-default" viewBox="0 0 64 64" fill="currentColor">
  1554. <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
  1555. <path d="M40 8v12h12" fill="white" opacity="0.5"/>
  1556. </svg>
  1557. `;
  1558. }
  1559. formatFileSize(bytes) {
  1560. if (bytes === 0) return '0 B';
  1561. const k = 1024;
  1562. const sizes = ['B', 'KB', 'MB', 'GB'];
  1563. const i = Math.floor(Math.log(bytes) / Math.log(k));
  1564. return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
  1565. }
  1566. isImageFile(fileName) {
  1567. const ext = fileName.split('.').pop().toLowerCase();
  1568. return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
  1569. }
  1570. isAnimationFolder(folderName) {
  1571. // 判断是否是动画文件夹(通过名称模式)
  1572. // 放宽匹配规则,让更多文件夹能显示预览图
  1573. return /^player_\d+$/i.test(folderName) ||
  1574. /^(idle|walk|run|attack|死亡|站立|行走|攻击)/i.test(folderName) ||
  1575. /动画|animation|ani|sprite|序列|sequence/i.test(folderName) ||
  1576. // 新增:所有文件夹都尝试显示预览图(如果有图片就显示)
  1577. true; // 默认返回true,让所有文件夹都尝试加载预览图
  1578. }
  1579. // 注意:guessFirstFrameName、tryAlternativePreview 和 showFolderIcon 方法已废弃
  1580. // 现在使用服务器端提供的预览信息,完全避免客户端的404错误
  1581. // 导航到指定路径
  1582. navigateToPath(path) {
  1583. this.pathNav.navigateTo(path);
  1584. }
  1585. // 创建文件夹
  1586. async createFolder() {
  1587. let folderName = '新建文件夹';
  1588. let counter = 1;
  1589. while (this.files.some(f => f.name === folderName && f.type === 'directory')) {
  1590. folderName = `新建文件夹 (${counter})`;
  1591. counter++;
  1592. }
  1593. const username = this.getCurrentUsername();
  1594. if (!username) {
  1595. this.showGlobalAlert('请先登录');
  1596. return;
  1597. }
  1598. try {
  1599. const response = await fetch('/api/disk/create-folder', {
  1600. method: 'POST',
  1601. headers: {
  1602. 'Content-Type': 'application/json'
  1603. },
  1604. body: JSON.stringify({
  1605. username: username,
  1606. path: this.pathNav.getPath(),
  1607. name: folderName
  1608. })
  1609. });
  1610. if (!response.ok) {
  1611. await this.handleServerError(response, '创建文件夹失败');
  1612. return;
  1613. }
  1614. const data = await response.json();
  1615. if (data.success) {
  1616. await this.loadFiles();
  1617. const newFolderItem = this.fileList.querySelector(`[data-name="${folderName}"]`);
  1618. if (newFolderItem) {
  1619. this.startRename(newFolderItem);
  1620. }
  1621. } else {
  1622. this.showGlobalAlert('创建文件夹失败: ' + (data.message || '未知错误'));
  1623. }
  1624. } catch (error) {
  1625. this.showGlobalAlert('创建文件夹失败: ' + (error.message || '网络错误'));
  1626. }
  1627. }
  1628. // 开始重命名(参考 Windows 行为)
  1629. startRename(fileItem) {
  1630. const nameEl = fileItem.querySelector('.file-name');
  1631. const input = fileItem.querySelector('.rename-input');
  1632. if (!nameEl || !input) return;
  1633. const oldName = fileItem.dataset.name;
  1634. const filePath = fileItem.dataset.path;
  1635. // 标记当前正在重命名
  1636. this._isRenaming = true;
  1637. // 显示输入框,隐藏文件名
  1638. input.value = oldName;
  1639. nameEl.style.display = 'none';
  1640. input.style.display = '';
  1641. // 选中文件名(不含扩展名,文件夹则全选)
  1642. setTimeout(() => {
  1643. const dotIndex = oldName.lastIndexOf('.');
  1644. if (dotIndex > 0 && fileItem.dataset.type !== 'directory') {
  1645. input.setSelectionRange(0, dotIndex);
  1646. } else {
  1647. input.select();
  1648. }
  1649. input.focus();
  1650. }, 10);
  1651. // 状态标记
  1652. let finished = false;
  1653. const exitRename = () => {
  1654. this._isRenaming = false;
  1655. input.style.display = 'none';
  1656. nameEl.style.display = '';
  1657. };
  1658. const commitRename = async () => {
  1659. if (finished) return;
  1660. finished = true;
  1661. const newName = input.value.trim();
  1662. exitRename();
  1663. // 名字有效且有变化才提交
  1664. if (newName && newName !== oldName) {
  1665. await this.renameFile(filePath, newName);
  1666. }
  1667. };
  1668. const cancelRename = () => {
  1669. if (finished) return;
  1670. finished = true;
  1671. exitRename();
  1672. };
  1673. // 使用 addEventListener 确保事件被捕获
  1674. const handleBlur = () => {
  1675. input.removeEventListener('blur', handleBlur);
  1676. input.removeEventListener('keydown', handleKeydown);
  1677. if (!finished) {
  1678. commitRename();
  1679. }
  1680. };
  1681. const handleKeydown = (e) => {
  1682. if (e.key === 'Enter') {
  1683. e.preventDefault();
  1684. e.stopPropagation();
  1685. input.removeEventListener('blur', handleBlur);
  1686. input.removeEventListener('keydown', handleKeydown);
  1687. commitRename();
  1688. } else if (e.key === 'Escape') {
  1689. e.preventDefault();
  1690. e.stopPropagation();
  1691. input.removeEventListener('blur', handleBlur);
  1692. input.removeEventListener('keydown', handleKeydown);
  1693. cancelRename();
  1694. }
  1695. };
  1696. input.addEventListener('blur', handleBlur);
  1697. input.addEventListener('keydown', handleKeydown);
  1698. }
  1699. // 执行重命名
  1700. async renameFile(oldPath, newName) {
  1701. const username = this.getCurrentUsername();
  1702. if (!username) {
  1703. this.showGlobalAlert('请先登录');
  1704. return;
  1705. }
  1706. try {
  1707. const response = await fetch('/api/disk/rename', {
  1708. method: 'POST',
  1709. headers: {
  1710. 'Content-Type': 'application/json'
  1711. },
  1712. body: JSON.stringify({
  1713. username: username,
  1714. oldPath: oldPath,
  1715. newName: newName
  1716. })
  1717. });
  1718. const data = await response.json();
  1719. if (data.success) {
  1720. this.loadFiles();
  1721. } else {
  1722. this.showGlobalAlert('重命名失败: ' + data.message);
  1723. this.loadFiles();
  1724. }
  1725. } catch (error) {
  1726. console.error('重命名失败:', error);
  1727. this.showGlobalAlert('重命名失败');
  1728. this.loadFiles();
  1729. }
  1730. }
  1731. // 下载文件
  1732. downloadFile(filePath) {
  1733. const username = this.getCurrentUsername();
  1734. if (!username) {
  1735. this.showGlobalAlert('请先登录');
  1736. return;
  1737. }
  1738. window.open(`/api/disk/download?username=${encodeURIComponent(username)}&path=${encodeURIComponent(filePath)}`, '_blank');
  1739. }
  1740. async copyFile(sourcePath, targetFolder, options = {}) {
  1741. const { suppressReload = false, silent = false } = options;
  1742. const username = this.getCurrentUsername();
  1743. if (!username) {
  1744. if (!silent) {
  1745. this.showGlobalAlert('请先登录');
  1746. }
  1747. return;
  1748. }
  1749. try {
  1750. const response = await fetch('/api/disk/copy', {
  1751. method: 'POST',
  1752. headers: {
  1753. 'Content-Type': 'application/json'
  1754. },
  1755. body: JSON.stringify({
  1756. username: username,
  1757. sourcePath,
  1758. targetFolder
  1759. })
  1760. });
  1761. if (!response.ok) {
  1762. if (!silent) {
  1763. await this.handleServerError(response, '复制失败');
  1764. }
  1765. return false;
  1766. }
  1767. const data = await response.json();
  1768. if (data.success) {
  1769. if (!suppressReload) {
  1770. await this.loadFiles();
  1771. }
  1772. return true;
  1773. } else {
  1774. if (!silent) {
  1775. this.showGlobalAlert('复制失败: ' + (data.message || '未知错误'));
  1776. }
  1777. return false;
  1778. }
  1779. } catch (error) {
  1780. if (!silent) {
  1781. this.showGlobalAlert('复制失败: ' + (error.message || '网络错误'));
  1782. }
  1783. return false;
  1784. }
  1785. }
  1786. showLoading(show) {
  1787. if (show) {
  1788. this.loading.classList.add('show');
  1789. } else {
  1790. this.loading.classList.remove('show');
  1791. }
  1792. }
  1793. }
  1794. // 初始化
  1795. document.addEventListener('DOMContentLoaded', () => {
  1796. window.diskManager = new DiskManager();
  1797. });