disk.js 74 KB

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