export.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>AWS S3 文件夹下载工具</title>
  7. <script src="https://sdk.amazonaws.com/js/aws-sdk-2.938.0.min.js"></script>
  8. <style>
  9. * {
  10. box-sizing: border-box;
  11. margin: 0;
  12. padding: 0;
  13. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  14. }
  15. body {
  16. background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
  17. color: #333;
  18. line-height: 1.6;
  19. padding: 20px;
  20. min-height: 100vh;
  21. }
  22. .container {
  23. max-width: 900px;
  24. margin: 0 auto;
  25. background-color: rgba(255, 255, 255, 0.95);
  26. border-radius: 12px;
  27. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  28. padding: 30px;
  29. }
  30. h1 {
  31. text-align: center;
  32. margin-bottom: 25px;
  33. color: #2c3e50;
  34. font-weight: 600;
  35. }
  36. .description {
  37. text-align: center;
  38. margin-bottom: 30px;
  39. color: #555;
  40. font-size: 16px;
  41. }
  42. .input-section {
  43. display: flex;
  44. flex-direction: column;
  45. gap: 15px;
  46. margin-bottom: 25px;
  47. }
  48. .folder-input {
  49. display: flex;
  50. gap: 10px;
  51. }
  52. input[type="text"] {
  53. flex: 1;
  54. padding: 12px 15px;
  55. border: 1px solid #ddd;
  56. border-radius: 6px;
  57. font-size: 16px;
  58. transition: border 0.3s;
  59. }
  60. input[type="text"]:focus {
  61. border-color: #3498db;
  62. outline: none;
  63. box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
  64. }
  65. button {
  66. padding: 12px 20px;
  67. background-color: #3498db;
  68. color: white;
  69. border: none;
  70. border-radius: 6px;
  71. cursor: pointer;
  72. font-size: 16px;
  73. font-weight: 500;
  74. transition: background-color 0.3s, transform 0.2s;
  75. }
  76. button:hover {
  77. background-color: #2980b9;
  78. transform: translateY(-2px);
  79. }
  80. button:active {
  81. transform: translateY(0);
  82. }
  83. button:disabled {
  84. background-color: #95a5a6;
  85. cursor: not-allowed;
  86. transform: none;
  87. }
  88. .action-buttons {
  89. display: flex;
  90. gap: 10px;
  91. justify-content: center;
  92. margin-top: 20px;
  93. }
  94. .download-all {
  95. background-color: #2ecc71;
  96. }
  97. .download-all:hover {
  98. background-color: #27ae60;
  99. }
  100. .clear {
  101. background-color: #e74c3c;
  102. }
  103. .clear:hover {
  104. background-color: #c0392b;
  105. }
  106. .folders-list {
  107. margin-top: 25px;
  108. }
  109. .folder-item {
  110. background-color: #f8f9fa;
  111. border-radius: 8px;
  112. padding: 15px;
  113. margin-bottom: 15px;
  114. border-left: 4px solid #3498db;
  115. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
  116. }
  117. .folder-header {
  118. display: flex;
  119. justify-content: space-between;
  120. align-items: center;
  121. margin-bottom: 10px;
  122. }
  123. .folder-name {
  124. font-weight: 600;
  125. font-size: 18px;
  126. color: #2c3e50;
  127. }
  128. .file-list {
  129. margin-top: 10px;
  130. max-height: 0;
  131. overflow: hidden;
  132. transition: max-height 0.3s ease;
  133. }
  134. .file-list.expanded {
  135. max-height: 500px;
  136. }
  137. .file-item {
  138. display: flex;
  139. justify-content: space-between;
  140. align-items: center;
  141. padding: 10px;
  142. border-bottom: 1px solid #eee;
  143. }
  144. .file-item:last-child {
  145. border-bottom: none;
  146. }
  147. .file-name {
  148. flex: 1;
  149. }
  150. .file-size {
  151. color: #7f8c8d;
  152. font-size: 14px;
  153. margin-right: 15px;
  154. }
  155. .progress-bar {
  156. height: 6px;
  157. background-color: #ecf0f1;
  158. border-radius: 3px;
  159. margin-top: 8px;
  160. overflow: hidden;
  161. }
  162. .progress {
  163. height: 100%;
  164. background-color: #2ecc71;
  165. width: 0%;
  166. transition: width 0.3s;
  167. }
  168. .status {
  169. margin-top: 10px;
  170. font-size: 14px;
  171. color: #7f8c8d;
  172. }
  173. .completed {
  174. color: #2ecc71;
  175. }
  176. .error {
  177. color: #e74c3c;
  178. }
  179. .expand-btn {
  180. background: none;
  181. border: none;
  182. color: #3498db;
  183. cursor: pointer;
  184. font-size: 14px;
  185. padding: 5px 10px;
  186. }
  187. .no-folders {
  188. text-align: center;
  189. padding: 20px;
  190. color: #7f8c8d;
  191. font-style: italic;
  192. }
  193. .summary {
  194. margin-top: 20px;
  195. padding: 15px;
  196. background-color: #f8f9fa;
  197. border-radius: 8px;
  198. text-align: center;
  199. }
  200. @media (max-width: 600px) {
  201. .container {
  202. padding: 15px;
  203. }
  204. .folder-input {
  205. flex-direction: column;
  206. }
  207. .action-buttons {
  208. flex-direction: column;
  209. }
  210. }
  211. </style>
  212. </head>
  213. <body>
  214. <div class="container">
  215. <h1>AWS S3 文件夹下载工具</h1>
  216. <p class="description">输入文件夹路径,批量下载S3存储桶中的文件</p>
  217. <div class="input-section">
  218. <div class="folder-input">
  219. <input type="text" id="folderPath" placeholder="输入文件夹路径,例如: documents/reports/">
  220. <button id="addFolder">添加文件夹</button>
  221. </div>
  222. </div>
  223. <div class="action-buttons">
  224. <button id="downloadAll" class="download-all" disabled>全部下载</button>
  225. <button id="clearAll" class="clear">清空列表</button>
  226. </div>
  227. <div class="folders-list" id="foldersList">
  228. <div class="no-folders" id="noFoldersMessage">暂无文件夹,请添加文件夹路径</div>
  229. </div>
  230. <div class="summary" id="summary" style="display: none;">
  231. <div id="summaryText"></div>
  232. <div class="progress-bar">
  233. <div class="progress" id="overallProgress"></div>
  234. </div>
  235. </div>
  236. </div>
  237. <script>
  238. // AWS S3配置 - 请替换为您的实际配置
  239. const awsConfig = {
  240. region: 'cn-northwest-1'; //設置區域
  241. credentials: {
  242. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  243. secretAccessKey: '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'
  244. }
  245. };
  246. // 初始化S3客户端
  247. AWS.config.update(awsConfig);
  248. const s3 = new AWS.S3();
  249. const bucketName = 'ccrb'; // 您的存储桶名称
  250. // 存储文件夹数据
  251. let folders = ["411325201408040121","440104201312168333","440604201604210182","440604201512070039","440305201207071553","411503201201270459","440305201208211554","440114200712210420","421181200905305829","440305201006230060","H10192830","36042320100705006x","445122201001140920","440304200911101831","430702201004080100","441422201507140015","440304201608261810","441523201403146764","440104201504076716","440307201308140020","150422201211302119","350305200910182398","511322201008261122","445381201002020040","440304201101091816","440303201409054513","42122420130806584x","460107201410300425","440881201301096726","610722201212110641","450422201211302119","362322200907013620","44051220171107001X","440306201602225117","440307201409126519","430724201606220113","440306201310090433","411302201312150399","511322200809261312","411622200810091026","610702200911200929","440304200909272041","420116201003150422","410823201310070203","440303201310117764","500234201312302184","442000201402214621","442000201501254634","360735201306230513","411423201212280230","440104201204136529","44010420120116652X","440303201402288528","42102320130828245X","441422201211232631","42130220130426841X","440305201712231239","440305201709281235","440305201802090142","42000201402214621","44158120140208901X ","360731201310017613","430903201207130312","441781201111186996","430621201205050219","445281201407234391","430902201403030136","440305201407099016","440104201712246118","440103201709182719","46902820090622071X","441623201003023410","420103201008124416","421181201008213531","360721200911050059","370212201509262531","44030620151119081","411626200905066531","420801201004044019","440305200902141512","44030620106094333","441324200809181616","430321201005030153","430981200909240254","44030520151214005X","440306201304270518","441501200909275018","810000201001300331","440306200911291017","440306201402061015","43020220140417","441424201210175121","140624198706080534","441622201210056665","440304201704101825","440105201212115425","44030520100106784X","440307201001193128","441623200903025521","440306200909140527","44182720081125610X","440303201404088132","440305201807160170","140105201903120222","321088200910315250","341225200911208210","522425201008120095","44030520170807121X","440106201706223326","445281201202283018","441501201203214090","431122201007290246","44158120100211882X","640221201606255730","360921201401250312","440303201312027340","440304201408050090","440183201410162416","440305201407081553","421023201412251012","440303201305232717","43052120090518017X","441827201006070112","44030620090905002X","450821200909241021","440304200910291821","440306201302193918","450703201211132751","440306201211192012","220102201703285218","430104201002160078","45132320090321511X","420506200910181817","441323201405100013","440304201406090099","43022320130908019X","440104201606306527","440104201510206513","44030620110627071X","440304201106171831","61072120160107262X","430621201411020045","440306201512280913","412824201008124416","440305201708241223","440981201110064612","440305201112311534","440304201205267417","440307201608200339","42088120160704583X","440306201006094333","440305201402120015","451021201003091399","440511201411174750","440113201503062116","411330200911181833","440306200911271315","440304201205150059","500116201112254351","44528120120718043x","360924201402152828","421281201310080720","450481201312201640","440304201206297618","440307201703220514","620702201310141210","430581201312210477","440515201403081939","430626201503160493","440823201406284914","431022201312050016","44030420140701635X","440307201401281821","445222201404092922","440306201402184568","440307201011162819","441702200410231726","430527200409116361","350824201402054614","44030420131008185X","361128201607072090","510824200811185063","440282200906022200","440305201006180032","440104201704116938","440103201508217815","44030620160514091X","430421201010050592","440305201005081518","442000201501254645","440304201209171818","440306201906213692","440513201004211609","440513201008055084","440511201304296744","440514200405200822","430481201602270229","440303200810068546","441302201211082038","360722201210162112","370283201308061552","441502201306061126","445224201209195160","44522420130321572X","360982201407026816","440104201504176514","430528201310220231","440305201312207811","440303201604298118","445281201209030830","620622201007295217","511324200911243551","430722201001160178","429004200403151355","440305201304051589","440233200809268017","440304201611031813","511011200910065062","421381201410289594","411622201503206510","44030620151119081X","340506201510100517","440604201508210078","440604201409070022","440604201608240194","431025201407220224","430223201408230106","50010120140112062X","440823201508132014","440305200901053414","230306200911094057","440305201005063416","620102201006175341","421024201001060824","440514201403174944","440303200908262726","43062601503160493","440104201204056713","445222201609115235","44060420161206017X","440604201604130019","440604201708210320","440514201412212359","411328201306280019","440306201212053911","420321201208120018","42900420040315","230183200910011311","411421200910280230","610125201004203516","430523201008120238","431102200901080016","340104200905039010","220102200811273316","430722201109200231","341003201205182610","511381201202223496","18923782659","440306201402011018","440307201606270050","440305201109211532","421081201203152150","441622201112087417","440304200908274627","621124200907122372","440981200912054630","44030420160715006X","23011020190710082X","421125201506070026","441427201409201517","440306201409193547","44030320140131451X","210204201003037033","445281200909171074","440304201005316317","440305200412151224","440103201805252422","440304201407017619","440304201310167610","440304201211281848","420822200311113720","430102201501120094","440103201606057413","440513200911033039","450923200912257479","440305201603241534","440511201105116210","440304201510221845","440304201510301861","440606201503270330","440304201703110041","371521201612040025","440303201405088521","440305201309278221","411102201006120099","440303200909164511","441424201007260320","441223200911252626","411622201606131515","421124201607031011","CAN440118050505","440304200912257133","371722200912033866","450921201211260415","440306201607080834","441203201510191219","430626200310030022","440402201308229176"];
  252. folders = ["411325201408040121"];
  253. // DOM元素
  254. const folderPathInput = document.getElementById('folderPath');
  255. const addFolderBtn = document.getElementById('addFolder');
  256. const downloadAllBtn = document.getElementById('downloadAll');
  257. const clearAllBtn = document.getElementById('clearAll');
  258. const foldersList = document.getElementById('foldersList');
  259. const noFoldersMessage = document.getElementById('noFoldersMessage');
  260. const summary = document.getElementById('summary');
  261. const summaryText = document.getElementById('summaryText');
  262. const overallProgress = document.getElementById('overallProgress');
  263. // 添加文件夹
  264. addFolderBtn.addEventListener('click', () => {
  265. const folderPath = folderPathInput.value.trim();
  266. if (!folderPath) {
  267. alert('请输入文件夹路径');
  268. return;
  269. }
  270. // 确保路径以斜杠结尾
  271. const normalizedPath = folderPath.endsWith('/') ? folderPath : folderPath + '/';
  272. // 检查是否已存在
  273. if (folders.some(f => f.path === normalizedPath)) {
  274. alert('该文件夹已存在列表中');
  275. return;
  276. }
  277. // 添加到文件夹列表
  278. const folder = {
  279. id: Date.now().toString(),
  280. path: normalizedPath,
  281. files: [],
  282. status: 'pending', // pending, loading, ready, downloading, completed, error
  283. expanded: false
  284. };
  285. folders.push(folder);
  286. renderFolders();
  287. folderPathInput.value = '';
  288. // 获取文件夹中的文件列表
  289. loadFolderFiles(folder);
  290. });
  291. // 加载文件夹中的文件
  292. async function loadFolderFiles(folder) {
  293. folder.status = 'loading';
  294. updateFolderUI(folder);
  295. try {
  296. const params = {
  297. Bucket: bucketName,
  298. Prefix: folder.path
  299. };
  300. const data = await s3.listObjectsV2(params).promise();
  301. // 过滤掉文件夹本身(如果存在)
  302. folder.files = data.Contents.filter(item =>
  303. item.Key !== folder.path
  304. ).map(item => ({
  305. key: item.Key,
  306. name: item.Key.replace(folder.path, ''),
  307. size: formatFileSize(item.Size),
  308. status: 'pending',
  309. progress: 0,
  310. url: null
  311. }));
  312. folder.status = folder.files.length > 0 ? 'ready' : 'empty';
  313. updateFolderUI(folder);
  314. updateDownloadButtonState();
  315. } catch (error) {
  316. console.error('加载文件列表失败:', error);
  317. folder.status = 'error';
  318. folder.error = error.message;
  319. updateFolderUI(folder);
  320. }
  321. }
  322. // 下载单个文件
  323. async function downloadFile(file, folder) {
  324. file.status = 'downloading';
  325. updateFolderUI(folder);
  326. try {
  327. const params = {
  328. Bucket: bucketName,
  329. Key: file.key
  330. };
  331. // 获取预签名URL
  332. const url = await s3.getSignedUrlPromise('getObject', params);
  333. file.url = url;
  334. // 创建隐藏的下载链接并触发点击
  335. const a = document.createElement('a');
  336. a.href = url;
  337. a.download = file.name;
  338. a.style.display = 'none';
  339. document.body.appendChild(a);
  340. a.click();
  341. document.body.removeChild(a);
  342. file.status = 'completed';
  343. file.progress = 100;
  344. updateFolderUI(folder);
  345. // 检查文件夹是否全部完成
  346. checkFolderCompletion(folder);
  347. } catch (error) {
  348. console.error('下载文件失败:', error);
  349. file.status = 'error';
  350. file.error = error.message;
  351. updateFolderUI(folder);
  352. }
  353. }
  354. // 下载文件夹中的所有文件
  355. async function downloadFolder(folder) {
  356. folder.status = 'downloading';
  357. updateFolderUI(folder);
  358. for (const file of folder.files) {
  359. if (file.status !== 'completed') {
  360. await downloadFile(file, folder);
  361. // 添加延迟以避免请求过于频繁
  362. await new Promise(resolve => setTimeout(resolve, 500));
  363. }
  364. }
  365. }
  366. // 下载所有文件夹
  367. async function downloadAllFolders() {
  368. summary.style.display = 'block';
  369. const totalFiles = folders.reduce((sum, folder) => sum + folder.files.length, 0);
  370. let completedFiles = 0;
  371. summaryText.textContent = `正在下载 ${totalFiles} 个文件...`;
  372. for (const folder of folders) {
  373. if (folder.status === 'ready' || folder.status === 'partial') {
  374. folder.status = 'downloading';
  375. updateFolderUI(folder);
  376. for (const file of folder.files) {
  377. if (file.status !== 'completed') {
  378. await downloadFile(file, folder);
  379. completedFiles++;
  380. // 更新总进度
  381. const progress = (completedFiles / totalFiles) * 100;
  382. overallProgress.style.width = `${progress}%`;
  383. summaryText.textContent = `已下载 ${completedFiles}/${totalFiles} 个文件 (${Math.round(progress)}%)`;
  384. // 添加延迟以避免请求过于频繁
  385. await new Promise(resolve => setTimeout(resolve, 500));
  386. }
  387. }
  388. }
  389. }
  390. summaryText.textContent = `下载完成!共下载 ${completedFiles} 个文件`;
  391. }
  392. // 检查文件夹是否全部完成
  393. function checkFolderCompletion(folder) {
  394. const completedCount = folder.files.filter(f => f.status === 'completed').length;
  395. const totalCount = folder.files.length;
  396. if (completedCount === totalCount) {
  397. folder.status = 'completed';
  398. } else if (completedCount > 0) {
  399. folder.status = 'partial';
  400. }
  401. updateFolderUI(folder);
  402. updateDownloadButtonState();
  403. }
  404. // 更新文件夹UI
  405. function updateFolderUI(folder) {
  406. const folderElement = document.getElementById(`folder-${folder.id}`);
  407. if (!folderElement) return;
  408. const statusElement = folderElement.querySelector('.status');
  409. const progressBar = folderElement.querySelector('.progress');
  410. const downloadBtn = folderElement.querySelector('.download-btn');
  411. const fileList = folderElement.querySelector('.file-list');
  412. // 更新状态
  413. let statusText = '';
  414. let statusClass = '';
  415. switch (folder.status) {
  416. case 'pending':
  417. statusText = '等待中...';
  418. break;
  419. case 'loading':
  420. statusText = '加载文件列表中...';
  421. break;
  422. case 'ready':
  423. statusText = `准备下载 (${folder.files.length} 个文件)`;
  424. statusClass = 'completed';
  425. downloadBtn.disabled = false;
  426. break;
  427. case 'empty':
  428. statusText = '文件夹为空';
  429. break;
  430. case 'downloading':
  431. const completed = folder.files.filter(f => f.status === 'completed').length;
  432. statusText = `下载中... ${completed}/${folder.files.length}`;
  433. downloadBtn.disabled = true;
  434. break;
  435. case 'completed':
  436. statusText = `下载完成 (${folder.files.length} 个文件)`;
  437. statusClass = 'completed';
  438. downloadBtn.disabled = true;
  439. break;
  440. case 'partial':
  441. const completedPartial = folder.files.filter(f => f.status === 'completed').length;
  442. statusText = `部分完成 (${completedPartial}/${folder.files.length})`;
  443. downloadBtn.disabled = false;
  444. break;
  445. case 'error':
  446. statusText = `错误: ${folder.error}`;
  447. statusClass = 'error';
  448. break;
  449. }
  450. statusElement.textContent = statusText;
  451. statusElement.className = `status ${statusClass}`;
  452. // 更新进度条
  453. if (folder.files.length > 0) {
  454. const completedFiles = folder.files.filter(f => f.status === 'completed').length;
  455. const progress = (completedFiles / folder.files.length) * 100;
  456. progressBar.style.width = `${progress}%`;
  457. }
  458. // 更新文件列表
  459. if (folder.expanded) {
  460. fileList.classList.add('expanded');
  461. renderFileList(folder, fileList);
  462. }
  463. }
  464. // 渲染文件列表
  465. function renderFileList(folder, container) {
  466. container.innerHTML = '';
  467. folder.files.forEach(file => {
  468. const fileItem = document.createElement('div');
  469. fileItem.className = 'file-item';
  470. let fileStatus = '';
  471. let fileStatusClass = '';
  472. switch (file.status) {
  473. case 'pending':
  474. fileStatus = '等待下载';
  475. break;
  476. case 'downloading':
  477. fileStatus = '下载中...';
  478. break;
  479. case 'completed':
  480. fileStatus = '已完成';
  481. fileStatusClass = 'completed';
  482. break;
  483. case 'error':
  484. fileStatus = `错误: ${file.error}`;
  485. fileStatusClass = 'error';
  486. break;
  487. }
  488. fileItem.innerHTML = `
  489. <div class="file-name">${file.name}</div>
  490. <div class="file-size">${file.size}</div>
  491. <div class="status ${fileStatusClass}">${fileStatus}</div>
  492. `;
  493. container.appendChild(fileItem);
  494. });
  495. }
  496. // 渲染文件夹列表
  497. function renderFolders() {
  498. if (folders.length === 0) {
  499. noFoldersMessage.style.display = 'block';
  500. foldersList.innerHTML = '';
  501. foldersList.appendChild(noFoldersMessage);
  502. return;
  503. }
  504. noFoldersMessage.style.display = 'none';
  505. foldersList.innerHTML = '';
  506. folders.forEach(folder => {
  507. const folderElement = document.createElement('div');
  508. folderElement.className = 'folder-item';
  509. folderElement.id = `folder-${folder.id}`;
  510. folderElement.innerHTML = `
  511. <div class="folder-header">
  512. <div class="folder-name">${folder.path}</div>
  513. <div>
  514. <button class="expand-btn">${folder.expanded ? '收起' : '展开'}</button>
  515. <button class="download-btn" ${folder.status !== 'ready' && folder.status !== 'partial' ? 'disabled' : ''}>下载文件夹</button>
  516. </div>
  517. </div>
  518. <div class="progress-bar">
  519. <div class="progress"></div>
  520. </div>
  521. <div class="status">等待中...</div>
  522. <div class="file-list"></div>
  523. `;
  524. // 添加事件监听器
  525. const expandBtn = folderElement.querySelector('.expand-btn');
  526. const downloadBtn = folderElement.querySelector('.download-btn');
  527. const fileList = folderElement.querySelector('.file-list');
  528. expandBtn.addEventListener('click', () => {
  529. folder.expanded = !folder.expanded;
  530. if (folder.expanded) {
  531. fileList.classList.add('expanded');
  532. renderFileList(folder, fileList);
  533. } else {
  534. fileList.classList.remove('expanded');
  535. }
  536. expandBtn.textContent = folder.expanded ? '收起' : '展开';
  537. });
  538. downloadBtn.addEventListener('click', () => {
  539. downloadFolder(folder);
  540. });
  541. foldersList.appendChild(folderElement);
  542. updateFolderUI(folder);
  543. });
  544. }
  545. // 更新下载按钮状态
  546. function updateDownloadButtonState() {
  547. const hasReadyFolders = folders.some(f =>
  548. f.status === 'ready' || f.status === 'partial'
  549. );
  550. downloadAllBtn.disabled = !hasReadyFolders;
  551. }
  552. // 格式化文件大小
  553. function formatFileSize(bytes) {
  554. if (bytes === 0) return '0 B';
  555. const k = 1024;
  556. const sizes = ['B', 'KB', 'MB', 'GB'];
  557. const i = Math.floor(Math.log(bytes) / Math.log(k));
  558. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  559. }
  560. // 事件监听器
  561. downloadAllBtn.addEventListener('click', downloadAllFolders);
  562. clearAllBtn.addEventListener('click', () => {
  563. folders = [];
  564. renderFolders();
  565. summary.style.display = 'none';
  566. updateDownloadButtonState();
  567. });
  568. folderPathInput.addEventListener('keypress', (e) => {
  569. if (e.key === 'Enter') {
  570. addFolderBtn.click();
  571. }
  572. });
  573. // 初始化
  574. renderFolders();
  575. </script>
  576. </body>
  577. </html>