export.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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. <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
  9. <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
  10. <style>
  11. * {
  12. box-sizing: border-box;
  13. margin: 0;
  14. padding: 0;
  15. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  16. }
  17. body {
  18. background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
  19. color: #333;
  20. line-height: 1.6;
  21. padding: 20px;
  22. min-height: 100vh;
  23. }
  24. .container {
  25. max-width: 1200px;
  26. margin: 0 auto;
  27. background-color: rgba(255, 255, 255, 0.95);
  28. border-radius: 12px;
  29. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  30. padding: 30px;
  31. }
  32. h1 {
  33. text-align: center;
  34. margin-bottom: 25px;
  35. color: #2c3e50;
  36. font-weight: 600;
  37. }
  38. .description {
  39. text-align: center;
  40. margin-bottom: 30px;
  41. color: #555;
  42. font-size: 16px;
  43. }
  44. .stats {
  45. display: flex;
  46. justify-content: space-between;
  47. margin-bottom: 20px;
  48. padding: 15px;
  49. background-color: #f8f9fa;
  50. border-radius: 8px;
  51. }
  52. .stat-item {
  53. text-align: center;
  54. }
  55. .stat-value {
  56. font-size: 24px;
  57. font-weight: bold;
  58. color: #3498db;
  59. }
  60. .stat-label {
  61. font-size: 14px;
  62. color: #7f8c8d;
  63. }
  64. .action-buttons {
  65. display: flex;
  66. gap: 10px;
  67. justify-content: center;
  68. margin-bottom: 20px;
  69. }
  70. button {
  71. padding: 12px 20px;
  72. color: white;
  73. border: none;
  74. border-radius: 6px;
  75. cursor: pointer;
  76. font-size: 16px;
  77. font-weight: 500;
  78. transition: background-color 0.3s, transform 0.2s;
  79. }
  80. button:hover {
  81. transform: translateY(-2px);
  82. }
  83. button:active {
  84. transform: translateY(0);
  85. }
  86. button:disabled {
  87. background-color: #95a5a6;
  88. cursor: not-allowed;
  89. transform: none;
  90. }
  91. .load-btn {
  92. background-color: #3498db;
  93. }
  94. .load-btn:hover {
  95. background-color: #2980b9;
  96. }
  97. .download-all {
  98. background-color: #2ecc71;
  99. }
  100. .download-all:hover {
  101. background-color: #27ae60;
  102. }
  103. .download-zip {
  104. background-color: #9b59b6;
  105. }
  106. .download-zip:hover {
  107. background-color: #8e44ad;
  108. }
  109. .clear {
  110. background-color: #e74c3c;
  111. }
  112. .clear:hover {
  113. background-color: #c0392b;
  114. }
  115. .folders-container {
  116. display: grid;
  117. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  118. gap: 15px;
  119. margin-top: 25px;
  120. }
  121. .folder-card {
  122. background-color: #f8f9fa;
  123. border-radius: 8px;
  124. padding: 15px;
  125. border-left: 4px solid #3498db;
  126. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
  127. transition: transform 0.2s;
  128. }
  129. .folder-card:hover {
  130. transform: translateY(-5px);
  131. }
  132. .folder-header {
  133. display: flex;
  134. justify-content: space-between;
  135. align-items: center;
  136. margin-bottom: 10px;
  137. }
  138. .folder-name {
  139. font-weight: 600;
  140. font-size: 16px;
  141. color: #2c3e50;
  142. word-break: break-all;
  143. }
  144. .folder-status {
  145. font-size: 12px;
  146. padding: 3px 8px;
  147. border-radius: 10px;
  148. background-color: #ecf0f1;
  149. }
  150. .status-pending {
  151. background-color: #f39c12;
  152. color: white;
  153. }
  154. .status-loading {
  155. background-color: #3498db;
  156. color: white;
  157. }
  158. .status-ready {
  159. background-color: #2ecc71;
  160. color: white;
  161. }
  162. .status-downloading {
  163. background-color: #9b59b6;
  164. color: white;
  165. }
  166. .status-completed {
  167. background-color: #27ae60;
  168. color: white;
  169. }
  170. .status-error {
  171. background-color: #e74c3c;
  172. color: white;
  173. }
  174. .file-count {
  175. font-size: 14px;
  176. color: #7f8c8d;
  177. margin-bottom: 10px;
  178. }
  179. .progress-bar {
  180. height: 6px;
  181. background-color: #ecf0f1;
  182. border-radius: 3px;
  183. margin-top: 8px;
  184. overflow: hidden;
  185. }
  186. .progress {
  187. height: 100%;
  188. background-color: #2ecc71;
  189. width: 0%;
  190. transition: width 0.3s;
  191. }
  192. .file-list {
  193. margin-top: 10px;
  194. max-height: 0;
  195. overflow: hidden;
  196. transition: max-height 0.3s ease;
  197. }
  198. .file-list.expanded {
  199. max-height: 200px;
  200. overflow-y: auto;
  201. }
  202. .file-item {
  203. display: flex;
  204. justify-content: space-between;
  205. align-items: center;
  206. padding: 8px 0;
  207. border-bottom: 1px solid #eee;
  208. font-size: 14px;
  209. }
  210. .file-item:last-child {
  211. border-bottom: none;
  212. }
  213. .file-name {
  214. flex: 1;
  215. word-break: break-all;
  216. }
  217. .file-size {
  218. color: #7f8c8d;
  219. font-size: 12px;
  220. margin-right: 10px;
  221. }
  222. .expand-btn {
  223. background: none;
  224. border: none;
  225. color: #3498db;
  226. cursor: pointer;
  227. font-size: 12px;
  228. padding: 5px 10px;
  229. margin-top: 5px;
  230. }
  231. .summary {
  232. margin-top: 20px;
  233. padding: 15px;
  234. background-color: #f8f9fa;
  235. border-radius: 8px;
  236. text-align: center;
  237. }
  238. .overall-progress {
  239. margin-top: 10px;
  240. }
  241. @media (max-width: 768px) {
  242. .folders-container {
  243. grid-template-columns: 1fr;
  244. }
  245. .stats {
  246. flex-direction: column;
  247. gap: 10px;
  248. }
  249. .action-buttons {
  250. flex-direction: column;
  251. }
  252. }
  253. </style>
  254. </head>
  255. <body>
  256. <div class="container">
  257. <h1>AWS S3 文件夹批量下载工具</h1>
  258. <p class="description">批量下载指定目录中的文件并打包为ZIP</p>
  259. <div class="stats">
  260. <div class="stat-item">
  261. <div class="stat-value" id="totalFolders">0</div>
  262. <div class="stat-label">总文件夹数</div>
  263. </div>
  264. <div class="stat-item">
  265. <div class="stat-value" id="loadedFolders">0</div>
  266. <div class="stat-label">已加载</div>
  267. </div>
  268. <div class="stat-item">
  269. <div class="stat-value" id="readyFolders">0</div>
  270. <div class="stat-label">准备下载</div>
  271. </div>
  272. <div class="stat-item">
  273. <div class="stat-value" id="completedFolders">0</div>
  274. <div class="stat-label">已完成</div>
  275. </div>
  276. </div>
  277. <div class="action-buttons">
  278. <button id="loadFolders" class="load-btn">加载所有文件夹</button>
  279. <button id="downloadZip" class="download-zip" disabled>打包下载ZIP</button>
  280. <button id="clearAll" class="clear">清空列表</button>
  281. </div>
  282. <div class="folders-container" id="foldersContainer">
  283. <!-- 文件夹卡片将在这里动态生成 -->
  284. </div>
  285. <div class="summary" id="summary" style="display: none;">
  286. <div id="summaryText"></div>
  287. <div class="progress-bar overall-progress">
  288. <div class="progress" id="overallProgress"></div>
  289. </div>
  290. </div>
  291. </div>
  292. <script>
  293. // 您的文件夹数组
  294. const 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"];
  295. // AWS S3配置 - 请替换为您的实际配置
  296. const awsConfig = {
  297. region: 'cn-northwest-1', // 替换为您的区域
  298. credentials: {
  299. accessKeyId: 'AKIATLPEDU37QV5CHLMH',
  300. secretAccessKey: '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'
  301. }
  302. };
  303. // 初始化S3客户端
  304. AWS.config.update(awsConfig);
  305. const s3 = new AWS.S3();
  306. const bucketName = 'ccrb'; // 您的存储桶名称
  307. // 存储文件夹数据
  308. let folderData = [];
  309. // DOM元素
  310. const loadFoldersBtn = document.getElementById('loadFolders');
  311. const downloadZipBtn = document.getElementById('downloadZip');
  312. const clearAllBtn = document.getElementById('clearAll');
  313. const foldersContainer = document.getElementById('foldersContainer');
  314. const summary = document.getElementById('summary');
  315. const summaryText = document.getElementById('summaryText');
  316. const overallProgress = document.getElementById('overallProgress');
  317. const totalFoldersEl = document.getElementById('totalFolders');
  318. const loadedFoldersEl = document.getElementById('loadedFolders');
  319. const readyFoldersEl = document.getElementById('readyFolders');
  320. const completedFoldersEl = document.getElementById('completedFolders');
  321. // 初始化文件夹数据
  322. function initializeFolderData() {
  323. folderData = folders.map(folder => ({
  324. id: folder,
  325. path: folder,
  326. files: [],
  327. status: 'pending', // pending, loading, ready, downloading, completed, error
  328. expanded: false,
  329. fileCount: 0,
  330. completedFiles: 0
  331. }));
  332. updateStats();
  333. renderFolders();
  334. }
  335. // 加载所有文件夹的文件列表
  336. async function loadAllFolders() {
  337. loadFoldersBtn.disabled = true;
  338. loadFoldersBtn.textContent = '加载中...';
  339. // 分批加载,避免同时发起太多请求
  340. const batchSize = 5;
  341. for (let i = 0; i < folderData.length; i += batchSize) {
  342. const batch = folderData.slice(i, i + batchSize);
  343. await Promise.allSettled(batch.map(folder => loadFolderFiles(folder)));
  344. // 更新统计信息
  345. updateStats();
  346. // 添加延迟以避免请求过于频繁
  347. await new Promise(resolve => setTimeout(resolve, 1000));
  348. }
  349. loadFoldersBtn.textContent = '加载完成';
  350. updateDownloadButtonState();
  351. }
  352. // 加载文件夹中的文件
  353. async function loadFolderFiles(folder) {
  354. folder.status = 'loading';
  355. updateFolderUI(folder);
  356. try {
  357. const params = {
  358. Bucket: bucketName,
  359. Prefix: folder.path
  360. };
  361. const data = await s3.listObjectsV2(params).promise();
  362. // 过滤掉文件夹本身(如果存在)
  363. folder.files = data.Contents.filter(item =>
  364. item.Key !== folder.path
  365. ).map(item => ({
  366. key: item.Key,
  367. name: item.Key.replace(folder.path, ''),
  368. size: formatFileSize(item.Size),
  369. status: 'pending',
  370. progress: 0,
  371. url: null
  372. }));
  373. folder.fileCount = folder.files.length;
  374. folder.status = folder.files.length > 0 ? 'ready' : 'empty';
  375. updateFolderUI(folder);
  376. updateStats();
  377. } catch (error) {
  378. console.error(`加载文件夹 ${folder.path} 失败:`, error);
  379. folder.status = 'error';
  380. folder.error = error.message;
  381. updateFolderUI(folder);
  382. updateStats();
  383. }
  384. }
  385. // 下载所有文件并打包为ZIP
  386. async function downloadAllAsZip() {
  387. summary.style.display = 'block';
  388. summaryText.textContent = '正在准备下载...';
  389. overallProgress.style.width = '0%';
  390. // 收集所有需要下载的文件
  391. const allFiles = [];
  392. folderData.forEach(folder => {
  393. if (folder.status === 'ready' || folder.status === 'partial') {
  394. folder.files.forEach(file => {
  395. if (file.status !== 'completed') {
  396. allFiles.push({
  397. folder: folder.path,
  398. key: file.key,
  399. name: file.name || file.key.split('/').pop()
  400. });
  401. }
  402. });
  403. }
  404. });
  405. if (allFiles.length === 0) {
  406. alert('没有可下载的文件');
  407. summary.style.display = 'none';
  408. return;
  409. }
  410. summaryText.textContent = `正在下载 ${allFiles.length} 个文件...`;
  411. // 创建ZIP文件
  412. const zip = new JSZip();
  413. let downloadedCount = 0;
  414. // 分批下载文件,避免同时发起太多请求
  415. const batchSize = 5;
  416. for (let i = 0; i < allFiles.length; i += batchSize) {
  417. const batch = allFiles.slice(i, i + batchSize);
  418. // 下载当前批次的所有文件
  419. await Promise.allSettled(batch.map(file => downloadFileForZip(file, zip)));
  420. downloadedCount += batch.length;
  421. // 更新进度
  422. const progress = (downloadedCount / allFiles.length) * 100;
  423. overallProgress.style.width = `${progress}%`;
  424. summaryText.textContent = `已下载 ${downloadedCount}/${allFiles.length} 个文件 (${Math.round(progress)}%)`;
  425. // 添加延迟以避免请求过于频繁
  426. await new Promise(resolve => setTimeout(resolve, 500));
  427. }
  428. summaryText.textContent = '正在生成ZIP文件...';
  429. // 生成ZIP文件
  430. const zipBlob = await zip.generateAsync({ type: 'blob' });
  431. // 下载ZIP文件
  432. saveAs(zipBlob, 'folders_download.zip');
  433. summaryText.textContent = `下载完成!共打包 ${allFiles.length} 个文件`;
  434. // 更新文件夹状态
  435. folderData.forEach(folder => {
  436. if (folder.status === 'ready' || folder.status === 'partial') {
  437. folder.status = 'completed';
  438. folder.completedFiles = folder.fileCount;
  439. updateFolderUI(folder);
  440. }
  441. });
  442. updateStats();
  443. updateDownloadButtonState();
  444. }
  445. // 下载单个文件并添加到ZIP
  446. async function downloadFileForZip(file, zip) {
  447. try {
  448. const params = {
  449. Bucket: bucketName,
  450. Key: file.key
  451. };
  452. const data = await s3.getObject(params).promise();
  453. // 将文件添加到ZIP中,保持文件夹结构
  454. const folderPath = file.folder + '/';
  455. const filePath = folderPath + file.name;
  456. zip.file(filePath, data.Body);
  457. // 更新对应文件夹的完成状态
  458. const folder = folderData.find(f => f.path === file.folder);
  459. if (folder) {
  460. const fileObj = folder.files.find(f => f.key === file.key);
  461. if (fileObj) {
  462. fileObj.status = 'completed';
  463. fileObj.progress = 100;
  464. folder.completedFiles++;
  465. updateFolderUI(folder);
  466. }
  467. }
  468. return true;
  469. } catch (error) {
  470. console.error(`下载文件 ${file.key} 失败:`, error);
  471. return false;
  472. }
  473. }
  474. // 更新文件夹UI
  475. function updateFolderUI(folder) {
  476. const folderElement = document.getElementById(`folder-${folder.id}`);
  477. if (!folderElement) return;
  478. const statusElement = folderElement.querySelector('.folder-status');
  479. const progressBar = folderElement.querySelector('.progress');
  480. const fileCountElement = folderElement.querySelector('.file-count');
  481. const fileList = folderElement.querySelector('.file-list');
  482. // 更新状态
  483. let statusText = '';
  484. let statusClass = '';
  485. switch (folder.status) {
  486. case 'pending':
  487. statusText = '等待中';
  488. statusClass = 'status-pending';
  489. break;
  490. case 'loading':
  491. statusText = '加载中';
  492. statusClass = 'status-loading';
  493. break;
  494. case 'ready':
  495. statusText = '准备下载';
  496. statusClass = 'status-ready';
  497. break;
  498. case 'empty':
  499. statusText = '空文件夹';
  500. statusClass = 'status-pending';
  501. break;
  502. case 'downloading':
  503. statusText = '下载中';
  504. statusClass = 'status-downloading';
  505. break;
  506. case 'completed':
  507. statusText = '已完成';
  508. statusClass = 'status-completed';
  509. break;
  510. case 'partial':
  511. statusText = '部分完成';
  512. statusClass = 'status-ready';
  513. break;
  514. case 'error':
  515. statusText = '错误';
  516. statusClass = 'status-error';
  517. break;
  518. }
  519. statusElement.textContent = statusText;
  520. statusElement.className = `folder-status ${statusClass}`;
  521. // 更新文件计数
  522. fileCountElement.textContent = `${folder.completedFiles}/${folder.fileCount} 个文件`;
  523. // 更新进度条
  524. if (folder.fileCount > 0) {
  525. const progress = (folder.completedFiles / folder.fileCount) * 100;
  526. progressBar.style.width = `${progress}%`;
  527. }
  528. // 更新文件列表
  529. if (folder.expanded) {
  530. fileList.classList.add('expanded');
  531. renderFileList(folder, fileList);
  532. }
  533. }
  534. // 渲染文件列表
  535. function renderFileList(folder, container) {
  536. container.innerHTML = '';
  537. folder.files.forEach(file => {
  538. const fileItem = document.createElement('div');
  539. fileItem.className = 'file-item';
  540. let fileStatus = '';
  541. let fileStatusClass = '';
  542. switch (file.status) {
  543. case 'pending':
  544. fileStatus = '等待下载';
  545. break;
  546. case 'downloading':
  547. fileStatus = '下载中...';
  548. break;
  549. case 'completed':
  550. fileStatus = '已完成';
  551. fileStatusClass = 'status-completed';
  552. break;
  553. case 'error':
  554. fileStatus = `错误: ${file.error}`;
  555. fileStatusClass = 'status-error';
  556. break;
  557. }
  558. fileItem.innerHTML = `
  559. <div class="file-name">${file.name || file.key}</div>
  560. <div class="file-size">${file.size}</div>
  561. <div class="folder-status ${fileStatusClass}">${fileStatus}</div>
  562. `;
  563. container.appendChild(fileItem);
  564. });
  565. }
  566. // 渲染文件夹列表
  567. function renderFolders() {
  568. foldersContainer.innerHTML = '';
  569. folderData.forEach(folder => {
  570. const folderElement = document.createElement('div');
  571. folderElement.className = 'folder-card';
  572. folderElement.id = `folder-${folder.id}`;
  573. folderElement.innerHTML = `
  574. <div class="folder-header">
  575. <div class="folder-name">${folder.path}</div>
  576. <div class="folder-status status-pending">等待中</div>
  577. </div>
  578. <div class="file-count">0/0 个文件</div>
  579. <div class="progress-bar">
  580. <div class="progress"></div>
  581. </div>
  582. <button class="expand-btn">显示文件</button>
  583. <div class="file-list"></div>
  584. `;
  585. // 添加事件监听器
  586. const expandBtn = folderElement.querySelector('.expand-btn');
  587. const fileList = folderElement.querySelector('.file-list');
  588. expandBtn.addEventListener('click', () => {
  589. folder.expanded = !folder.expanded;
  590. if (folder.expanded) {
  591. fileList.classList.add('expanded');
  592. renderFileList(folder, fileList);
  593. expandBtn.textContent = '隐藏文件';
  594. } else {
  595. fileList.classList.remove('expanded');
  596. expandBtn.textContent = '显示文件';
  597. }
  598. });
  599. foldersContainer.appendChild(folderElement);
  600. updateFolderUI(folder);
  601. });
  602. }
  603. // 更新统计信息
  604. function updateStats() {
  605. totalFoldersEl.textContent = folderData.length;
  606. loadedFoldersEl.textContent = folderData.filter(f => f.status !== 'pending').length;
  607. readyFoldersEl.textContent = folderData.filter(f => f.status === 'ready' || f.status === 'partial').length;
  608. completedFoldersEl.textContent = folderData.filter(f => f.status === 'completed').length;
  609. }
  610. // 更新下载按钮状态
  611. function updateDownloadButtonState() {
  612. const hasReadyFolders = folderData.some(f =>
  613. f.status === 'ready' || f.status === 'partial'
  614. );
  615. downloadZipBtn.disabled = !hasReadyFolders;
  616. }
  617. // 格式化文件大小
  618. function formatFileSize(bytes) {
  619. if (bytes === 0) return '0 B';
  620. const k = 1024;
  621. const sizes = ['B', 'KB', 'MB', 'GB'];
  622. const i = Math.floor(Math.log(bytes) / Math.log(k));
  623. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  624. }
  625. // 事件监听器
  626. loadFoldersBtn.addEventListener('click', loadAllFolders);
  627. downloadZipBtn.addEventListener('click', downloadAllAsZip);
  628. clearAllBtn.addEventListener('click', () => {
  629. if (confirm('确定要清空所有文件夹吗?')) {
  630. initializeFolderData();
  631. summary.style.display = 'none';
  632. loadFoldersBtn.disabled = false;
  633. loadFoldersBtn.textContent = '加载所有文件夹';
  634. updateDownloadButtonState();
  635. }
  636. });
  637. // 初始化
  638. initializeFolderData();
  639. </script>
  640. </body>
  641. </html>