export.html 31 KB

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