disk.js 60 KB

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