file_browser.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. // 文件浏览器功能
  2. class FileBrowser {
  3. constructor() {
  4. this.currentPath = new URLSearchParams(window.location.search).get('path') || '';
  5. this.uploadingFiles = new Map(); // 存储正在上传的文件
  6. this.init();
  7. }
  8. init() {
  9. this.loadFiles();
  10. this.updateBreadcrumb();
  11. this.bindEvents();
  12. }
  13. // 绑定事件
  14. bindEvents() {
  15. // 上传按钮事件
  16. document.getElementById('upload-btn').addEventListener('click', () => {
  17. this.showUploadDialog();
  18. });
  19. // 文件选择事件
  20. document.getElementById('file-input').addEventListener('change', (e) => {
  21. this.handleFileSelect(e);
  22. });
  23. // 创建目录按钮事件
  24. document.getElementById('create-dir-btn').addEventListener('click', () => {
  25. this.showCreateDirectoryDialog();
  26. });
  27. // 删除目录按钮事件
  28. document.getElementById('delete-dir-btn').addEventListener('click', () => {
  29. this.showDeleteDirectoryDialog();
  30. });
  31. }
  32. // 加载文件列表
  33. loadFiles() {
  34. const contentElement = document.getElementById('file-list-content');
  35. Utils.showLoading(contentElement);
  36. const url = `/api/files?path=${encodeURIComponent(this.currentPath)}`;
  37. Utils.ajax(url)
  38. .then(data => {
  39. this.renderFiles(data.files);
  40. })
  41. .catch(error => {
  42. Utils.showError(contentElement, error.message);
  43. });
  44. }
  45. // 渲染文件列表
  46. renderFiles(files) {
  47. const contentElement = document.getElementById('file-list-content');
  48. // 检查是否显示删除目录按钮
  49. this.updateDeleteDirectoryButton(files);
  50. if (files.length === 0) {
  51. contentElement.innerHTML = '<div class="loading">此目录为空</div>';
  52. return;
  53. }
  54. let html = '';
  55. // 如果不在根目录,显示返回上级目录
  56. if (this.currentPath) {
  57. const parentPath = this.currentPath.substring(0, this.currentPath.lastIndexOf('/'));
  58. html += `
  59. <div class="file-item" onclick="fileBrowser.navigateToPath('${parentPath}')">
  60. <div class="file-item-name">
  61. <span class="file-icon folder">📁</span>
  62. <span>..</span>
  63. </div>
  64. <div class="file-item-size">-</div>
  65. <div class="file-item-modified">-</div>
  66. </div>
  67. `;
  68. }
  69. files.forEach(file => {
  70. const icon = file.type === 'folder' ? '📁' : '📄';
  71. const size = file.type === 'folder' ? '-' : Utils.formatFileSize(file.size);
  72. const onclick = file.type === 'folder'
  73. ? `fileBrowser.navigateToPath('${file.path}')`
  74. : `fileBrowser.viewFile('${file.path}')`;
  75. html += `
  76. <div class="file-item" onclick="${onclick}">
  77. <div class="file-item-name">
  78. <span class="file-icon ${file.type}">${icon}</span>
  79. <span>${file.name}</span>
  80. </div>
  81. <div class="file-item-size">${size}</div>
  82. <div class="file-item-modified">${file.modified}</div>
  83. </div>
  84. `;
  85. });
  86. contentElement.innerHTML = html;
  87. }
  88. // 导航到指定路径
  89. navigateToPath(path) {
  90. const url = new URL(window.location);
  91. if (path) {
  92. url.searchParams.set('path', path);
  93. } else {
  94. url.searchParams.delete('path');
  95. }
  96. window.location.href = url.toString();
  97. }
  98. // 查看文件详情
  99. viewFile(filePath) {
  100. // 对路径进行正确的编码
  101. const encodedPath = filePath.split('/').map(part => encodeURIComponent(part)).join('/');
  102. window.location.href = `/file/${encodedPath}`;
  103. }
  104. // 更新面包屑导航
  105. updateBreadcrumb() {
  106. const breadcrumbPath = document.getElementById('breadcrumb-path');
  107. if (!this.currentPath) {
  108. breadcrumbPath.innerHTML = '';
  109. return;
  110. }
  111. const parts = this.currentPath.split('/').filter(part => part);
  112. let html = '';
  113. let currentPath = '';
  114. parts.forEach((part, index) => {
  115. currentPath += (currentPath ? '/' : '') + part;
  116. if (index === parts.length - 1) {
  117. html += ` / <span class="breadcrumb-item">${part}</span>`;
  118. } else {
  119. html += ` / <span class="breadcrumb-item">
  120. <a href="?path=${encodeURIComponent(currentPath)}" class="breadcrumb-link">${part}</a>
  121. </span>`;
  122. }
  123. });
  124. breadcrumbPath.innerHTML = html;
  125. }
  126. // 显示上传对话框
  127. showUploadDialog() {
  128. document.getElementById('file-input').click();
  129. }
  130. // 处理文件选择
  131. handleFileSelect(event) {
  132. const files = Array.from(event.target.files);
  133. if (files.length === 0) return;
  134. // 重置文件选择器
  135. event.target.value = '';
  136. // 开始上传文件
  137. this.uploadFiles(files);
  138. }
  139. // 上传文件
  140. async uploadFiles(files) {
  141. const formData = new FormData();
  142. // 添加当前路径
  143. formData.append('path', this.currentPath);
  144. // 添加所有文件
  145. files.forEach(file => {
  146. formData.append('files', file);
  147. });
  148. // 在列表中显示上传中的文件
  149. this.showUploadingFiles(files);
  150. try {
  151. // 发送上传请求
  152. const response = await fetch('/api/upload', {
  153. method: 'POST',
  154. body: formData
  155. });
  156. if (!response.ok) {
  157. throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  158. }
  159. const result = await response.json();
  160. // 处理上传结果
  161. this.handleUploadResult(result);
  162. } catch (error) {
  163. console.error('上传失败:', error);
  164. this.handleUploadError(error.message);
  165. }
  166. }
  167. // 在列表中显示上传中的文件
  168. showUploadingFiles(files) {
  169. const contentElement = document.getElementById('file-list-content');
  170. files.forEach(file => {
  171. const fileId = 'upload-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
  172. this.uploadingFiles.set(fileId, file);
  173. const uploadItem = document.createElement('div');
  174. uploadItem.className = 'file-item uploading';
  175. uploadItem.id = fileId;
  176. uploadItem.innerHTML = `
  177. <div class="file-item-name">
  178. <span class="file-icon file">📄</span>
  179. <span>${file.name}</span>
  180. <span class="upload-status">上传中...</span>
  181. </div>
  182. <div class="file-item-size">${Utils.formatFileSize(file.size)}</div>
  183. <div class="file-item-modified">
  184. <div class="upload-progress">
  185. <div class="progress-bar">
  186. <div class="progress-fill"></div>
  187. </div>
  188. </div>
  189. </div>
  190. `;
  191. // 添加到列表顶部(在返回上级目录项之后)
  192. const firstFileItem = contentElement.querySelector('.file-item:not(.uploading)');
  193. if (firstFileItem && firstFileItem.textContent.includes('..')) {
  194. firstFileItem.insertAdjacentElement('afterend', uploadItem);
  195. } else {
  196. contentElement.insertBefore(uploadItem, contentElement.firstChild);
  197. }
  198. // 模拟进度条动画
  199. setTimeout(() => {
  200. const progressFill = uploadItem.querySelector('.progress-fill');
  201. if (progressFill) {
  202. progressFill.style.width = '90%';
  203. }
  204. }, 100);
  205. });
  206. }
  207. // 处理上传结果
  208. handleUploadResult(result) {
  209. // 移除上传中的文件项
  210. this.uploadingFiles.forEach((file, fileId) => {
  211. const uploadItem = document.getElementById(fileId);
  212. if (uploadItem) {
  213. uploadItem.remove();
  214. }
  215. });
  216. this.uploadingFiles.clear();
  217. // 显示上传结果
  218. this.showUploadResultDialog(result);
  219. }
  220. // 处理上传错误
  221. handleUploadError(errorMessage) {
  222. // 移除上传中的文件项并显示错误
  223. this.uploadingFiles.forEach((file, fileId) => {
  224. const uploadItem = document.getElementById(fileId);
  225. if (uploadItem) {
  226. const statusElement = uploadItem.querySelector('.upload-status');
  227. const progressElement = uploadItem.querySelector('.upload-progress');
  228. if (statusElement) statusElement.textContent = '上传失败';
  229. if (progressElement) progressElement.innerHTML = '<span class="error-text">❌</span>';
  230. uploadItem.classList.add('upload-error');
  231. // 3秒后移除
  232. setTimeout(() => {
  233. uploadItem.remove();
  234. }, 3000);
  235. }
  236. });
  237. this.uploadingFiles.clear();
  238. // 显示错误消息
  239. this.showUploadResultDialog({
  240. success: false,
  241. uploaded_count: 0,
  242. failed_count: this.uploadingFiles.size,
  243. message: errorMessage,
  244. uploaded_files: [],
  245. failed_files: []
  246. });
  247. }
  248. // 显示上传结果对话框
  249. showUploadResultDialog(result) {
  250. const modalHtml = `
  251. <div id="upload-result-modal" class="modal-overlay">
  252. <div class="modal-content upload-result-modal">
  253. <div class="modal-header">
  254. <h3>上传结果</h3>
  255. </div>
  256. <div class="modal-body">
  257. <div class="upload-result ${result.success ? 'success' : 'error'}">
  258. <div class="result-icon">${result.success ? '✅' : '❌'}</div>
  259. <div class="result-message">
  260. <p><strong>${result.message}</strong></p>
  261. ${result.uploaded_count > 0 ? `<p>成功上传: ${result.uploaded_count} 个文件</p>` : ''}
  262. ${result.failed_count > 0 ? `<p>上传失败: ${result.failed_count} 个文件</p>` : ''}
  263. </div>
  264. </div>
  265. ${result.failed_files && result.failed_files.length > 0 ? `
  266. <div class="failed-files">
  267. <h4>失败文件详情:</h4>
  268. <ul>
  269. ${result.failed_files.map(file =>
  270. `<li><strong>${file.filename}:</strong> ${file.error}</li>`
  271. ).join('')}
  272. </ul>
  273. </div>
  274. ` : ''}
  275. </div>
  276. <div class="modal-footer">
  277. <button onclick="fileBrowser.closeUploadResultDialog()" class="btn btn-primary">确认</button>
  278. </div>
  279. </div>
  280. </div>
  281. `;
  282. document.body.insertAdjacentHTML('beforeend', modalHtml);
  283. }
  284. // 关闭上传结果对话框
  285. closeUploadResultDialog() {
  286. const modal = document.getElementById('upload-result-modal');
  287. if (modal) {
  288. modal.remove();
  289. }
  290. // 刷新文件列表
  291. this.loadFiles();
  292. }
  293. // 更新删除目录按钮的显示状态
  294. updateDeleteDirectoryButton(files) {
  295. const deleteBtn = document.getElementById('delete-dir-btn');
  296. // 只有在非根目录且目录为空时才显示删除按钮
  297. if (this.currentPath && files.length === 0) {
  298. deleteBtn.style.display = 'inline-block';
  299. } else {
  300. deleteBtn.style.display = 'none';
  301. }
  302. }
  303. // 显示创建目录对话框
  304. showCreateDirectoryDialog() {
  305. const modalHtml = `
  306. <div id="create-dir-modal" class="modal-overlay">
  307. <div class="modal-content">
  308. <div class="modal-header">
  309. <h3>创建目录</h3>
  310. <button onclick="fileBrowser.closeCreateDirectoryDialog()" class="modal-close">×</button>
  311. </div>
  312. <div class="modal-body">
  313. <div class="form-group">
  314. <label class="form-label">目录名称:</label>
  315. <input type="text" id="dir-name-input" class="form-input" placeholder="请输入目录名称" maxlength="255">
  316. <small style="color: #666; margin-top: 5px; display: block;">
  317. 目录名称不能包含以下字符: / \\ : * ? " < > |
  318. </small>
  319. </div>
  320. </div>
  321. <div class="modal-footer">
  322. <button onclick="fileBrowser.closeCreateDirectoryDialog()" class="btn btn-cancel">取消</button>
  323. <button onclick="fileBrowser.createDirectory()" class="btn btn-primary" id="create-dir-confirm">创建</button>
  324. </div>
  325. </div>
  326. </div>
  327. `;
  328. document.body.insertAdjacentHTML('beforeend', modalHtml);
  329. // 聚焦到输入框
  330. setTimeout(() => {
  331. const input = document.getElementById('dir-name-input');
  332. if (input) {
  333. input.focus();
  334. // 回车键确认
  335. input.addEventListener('keypress', (e) => {
  336. if (e.key === 'Enter') {
  337. this.createDirectory();
  338. }
  339. });
  340. }
  341. }, 100);
  342. }
  343. // 关闭创建目录对话框
  344. closeCreateDirectoryDialog() {
  345. const modal = document.getElementById('create-dir-modal');
  346. if (modal) {
  347. modal.remove();
  348. }
  349. }
  350. // 创建目录
  351. async createDirectory() {
  352. const input = document.getElementById('dir-name-input');
  353. const confirmBtn = document.getElementById('create-dir-confirm');
  354. if (!input) return;
  355. const dirName = input.value.trim();
  356. // 验证目录名称
  357. if (!dirName) {
  358. alert('请输入目录名称');
  359. input.focus();
  360. return;
  361. }
  362. // 检查非法字符
  363. const invalidChars = /[\/\\:*?"<>|]/;
  364. if (invalidChars.test(dirName)) {
  365. alert('目录名称不能包含以下字符: / \\ : * ? " < > |');
  366. input.focus();
  367. return;
  368. }
  369. // 禁用按钮,防止重复提交
  370. confirmBtn.disabled = true;
  371. confirmBtn.textContent = '创建中...';
  372. try {
  373. const response = await fetch('/api/create-directory', {
  374. method: 'POST',
  375. headers: {
  376. 'Content-Type': 'application/json',
  377. },
  378. body: JSON.stringify({
  379. path: this.currentPath,
  380. name: dirName
  381. })
  382. });
  383. const result = await response.json();
  384. if (response.ok && result.success) {
  385. // 成功创建
  386. this.closeCreateDirectoryDialog();
  387. this.loadFiles(); // 刷新文件列表
  388. // 显示成功消息
  389. this.showMessage('目录创建成功', 'success');
  390. } else {
  391. // 创建失败
  392. alert(result.error || '创建目录失败');
  393. confirmBtn.disabled = false;
  394. confirmBtn.textContent = '创建';
  395. input.focus();
  396. }
  397. } catch (error) {
  398. console.error('创建目录失败:', error);
  399. alert('创建目录失败: ' + error.message);
  400. confirmBtn.disabled = false;
  401. confirmBtn.textContent = '创建';
  402. }
  403. }
  404. // 显示删除目录对话框
  405. showDeleteDirectoryDialog() {
  406. const currentDirName = this.currentPath.split('/').pop() || this.currentPath;
  407. const modalHtml = `
  408. <div id="delete-dir-modal" class="modal-overlay">
  409. <div class="modal-content">
  410. <div class="modal-header">
  411. <h3>删除目录</h3>
  412. <button onclick="fileBrowser.closeDeleteDirectoryDialog()" class="modal-close">×</button>
  413. </div>
  414. <div class="modal-body">
  415. <div class="delete-warning">
  416. <div class="warning-icon">⚠️</div>
  417. <div class="warning-text">
  418. <p>您确定要删除目录 "<span class="file-name-highlight">${currentDirName}</span>" 吗?</p>
  419. <p><strong>此操作不可撤销!</strong></p>
  420. </div>
  421. </div>
  422. </div>
  423. <div class="modal-footer">
  424. <button onclick="fileBrowser.closeDeleteDirectoryDialog()" class="btn btn-cancel">取消</button>
  425. <button onclick="fileBrowser.deleteDirectory()" class="btn btn-danger" id="delete-dir-confirm">删除</button>
  426. </div>
  427. </div>
  428. </div>
  429. `;
  430. document.body.insertAdjacentHTML('beforeend', modalHtml);
  431. }
  432. // 关闭删除目录对话框
  433. closeDeleteDirectoryDialog() {
  434. const modal = document.getElementById('delete-dir-modal');
  435. if (modal) {
  436. modal.remove();
  437. }
  438. }
  439. // 删除目录
  440. async deleteDirectory() {
  441. const confirmBtn = document.getElementById('delete-dir-confirm');
  442. // 禁用按钮,防止重复提交
  443. confirmBtn.disabled = true;
  444. confirmBtn.textContent = '删除中...';
  445. try {
  446. const response = await fetch('/api/delete-directory', {
  447. method: 'DELETE',
  448. headers: {
  449. 'Content-Type': 'application/json',
  450. },
  451. body: JSON.stringify({
  452. path: this.currentPath
  453. })
  454. });
  455. const result = await response.json();
  456. if (response.ok && result.success) {
  457. // 成功删除
  458. this.closeDeleteDirectoryDialog();
  459. // 导航到父目录
  460. const parentPath = this.currentPath.substring(0, this.currentPath.lastIndexOf('/'));
  461. this.navigateToPath(parentPath);
  462. // 显示成功消息
  463. this.showMessage('目录删除成功', 'success');
  464. } else {
  465. // 删除失败
  466. alert(result.error || '删除目录失败');
  467. confirmBtn.disabled = false;
  468. confirmBtn.textContent = '删除';
  469. }
  470. } catch (error) {
  471. console.error('删除目录失败:', error);
  472. alert('删除目录失败: ' + error.message);
  473. confirmBtn.disabled = false;
  474. confirmBtn.textContent = '删除';
  475. }
  476. }
  477. // 显示消息提示
  478. showMessage(text, type = 'info') {
  479. const messageHtml = `
  480. <div class="delete-message ${type}">
  481. <div class="message-icon">${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'}</div>
  482. <div class="message-text">${text}</div>
  483. <button onclick="this.parentElement.remove()" class="message-close">×</button>
  484. </div>
  485. `;
  486. document.body.insertAdjacentHTML('beforeend', messageHtml);
  487. // 3秒后自动消失
  488. setTimeout(() => {
  489. const message = document.querySelector('.delete-message');
  490. if (message) {
  491. message.remove();
  492. }
  493. }, 3000);
  494. }
  495. }
  496. // 初始化文件浏览器
  497. let fileBrowser;
  498. document.addEventListener('DOMContentLoaded', function() {
  499. fileBrowser = new FileBrowser();
  500. });