excel格式:

直接在前端做 zip 壓縮/解壓_文件名

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Excel數據文件下載工具</title>
  <!-- 引入必要的庫 -->
  <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'Arial', sans-serif;
    }
    
    body {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
      background-color: #f8f9fa;
    }
    
    .container {
      background: white;
      border-radius: 10px;
      box-shadow: 0 2px 15px rgba(0,0,0,0.1);
      padding: 30px;
    }
    
    h1 {
      color: #333;
      margin-bottom: 20px;
      text-align: center;
      font-size: 28px;
    }
    
    .description {
      text-align: center;
      color: #666;
      margin-bottom: 30px;
      line-height: 1.6;
    }
    
    .feature-note {
      background-color: #fff3cd;
      border-left: 4px solid #ffc107;
      padding: 15px;
      margin: 20px 0;
      border-radius: 0 5px 5px 0;
    }
    
    .upload-section {
      border: 2px dashed #007bff;
      border-radius: 10px;
      padding: 40px 20px;
      text-align: center;
      margin-bottom: 30px;
      cursor: pointer;
      transition: all 0.3s;
    }
    
    .upload-section:hover {
      background-color: #f8f9fa;
    }
    
    .upload-icon {
      font-size: 48px;
      color: #007bff;
      margin-bottom: 20px;
    }
    
    #excelFile {
      display: none;
    }
    
    .file-info {
      margin: 20px 0;
      padding: 15px;
      background-color: #e9f7fe;
      border-radius: 5px;
      display: none;
    }
    
    .controls {
      display: flex;
      justify-content: center;
      gap: 15px;
      margin-bottom: 30px;
      flex-wrap: wrap;
    }
    
    button {
      padding: 10px 25px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
      transition: background-color 0.3s;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    
    #downloadAll {
      background-color: #28a745;
      color: white;
    }
    
    #downloadAll:hover {
      background-color: #218838;
    }
    
    #downloadZip {
      background-color: #17a2b8;
      color: white;
    }
    
    #downloadZip:hover {
      background-color: #138496;
    }
    
    #clearData {
      background-color: #dc3545;
      color: white;
    }
    
    #clearData:hover {
      background-color: #c82333;
    }
    
    .data-table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 30px;
      display: none;
    }
    
    .data-table th, .data-table td {
      padding: 12px 15px;
      border: 1px solid #ddd;
      text-align: left;
    }
    
    .data-table th {
      background-color: #007bff;
      color: white;
    }
    
    .data-table tr:nth-child(even) {
      background-color: #f8f9fa;
    }
    
    .data-table tr:hover {
      background-color: #e9f7fe;
    }
    
    .path-cell {
      max-width: 400px;
      word-break: break-all;
    }
    
    .filename-preview {
      font-size: 12px;
      color: #28a745;
      margin-top: 5px;
      font-style: italic;
    }
    
    .download-btn {
      background-color: #007bff;
      color: white;
      padding: 6px 12px;
      font-size: 14px;
      border-radius: 4px;
      cursor: pointer;
      border: none;
    }
    
    .download-btn:hover {
      background-color: #0056b3;
    }
    
    .multi-path {
      color: #dc3545;
      font-weight: bold;
    }
    
    .progress-container {
      margin: 20px 0;
      height: 8px;
      background-color: #eee;
      border-radius: 4px;
      overflow: hidden;
      display: none;
    }
    
    .progress-bar {
      height: 100%;
      background-color: #28a745;
      width: 0%;
      transition: width 0.3s;
    }
    
    .status-message {
      text-align: center;
      padding: 10px;
      margin: 10px 0;
      border-radius: 5px;
      display: none;
    }
    
    .success {
      background-color: #d4edda;
      color: #155724;
      display: block;
    }
    
    .error {
      background-color: #f8d7da;
      color: #721c24;
      display: block;
    }
    
    .loading {
      background-color: #d1ecf1;
      color: #0c5460;
      display: block;
    }
    
    .empty-state {
      text-align: center;
      padding: 50px 0;
      color: #666;
    }
    
    .empty-state i {
      font-size: 48px;
      margin-bottom: 20px;
      color: #ccc;
    }
    
    .file-count {
      background-color: #007bff;
      color: white;
      padding: 5px 15px;
      border-radius: 20px;
      display: inline-block;
      margin-bottom: 20px;
    }
  </style>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
  <div class="container">
    <h1><i class="fas fa-file-excel"></i> Excel數據文件下載工具</h1>
    
    <div class="description">
      <p>上傳包含文件下載信息的Excel文件(需包含path和name列),支持path列中逗號分隔的多個鏈接</p>
      <p><strong>格式要求:</strong>Excel文件需包含兩列 - path(文件鏈接,多個鏈接用逗號分隔)和name(文件名稱)</p>
    </div>
    
    <div class="feature-note">
      <strong>功能:</strong>當同一個人有多個文件時,會自動在文件名後添加序號(如:張三1.pdf、張三2.jpg)
    </div>
    
    <!-- 文件上傳區域 -->
    <div class="upload-section" onclick="document.getElementById('excelFile').click()">
      <div class="upload-icon">
        <i class="fas fa-cloud-upload-alt"></i>
      </div>
      <h3>點擊上傳Excel文件</h3>
      <p class="text-muted">支持.xlsx, .xls格式</p>
      <input type="file" id="excelFile" accept=".xlsx,.xls" onchange="handleFileUpload(event)">
    </div>
    
    <!-- 文件信息 -->
    <div class="file-info" id="fileInfo">
      <h4><i class="fas fa-file-excel"></i> <span id="fileName">文件名</span></h4>
      <p>數據行數: <span id="rowCount">0</span> | 文件總數: <span id="totalFileCount">0</span></p>
    </div>
    
    <!-- 控制按鈕 -->
    <div class="controls">
      <button id="downloadAll" disabled><i class="fas fa-download"></i> 下載所有文件</button>
      <button id="downloadZip" disabled><i class="fas fa-file-archive"></i> 下載壓縮包</button>
      <button id="clearData"><i class="fas fa-trash"></i> 清空數據</button>
    </div>
    
    <!-- 狀態消息 -->
    <div class="status-message" id="statusMessage"></div>
    
    <!-- 進度條 -->
    <div class="progress-container" id="progressContainer">
      <div class="progress-bar" id="progressBar"></div>
    </div>
    
    <!-- 文件總數標記 -->
    <div id="fileCountDisplay" class="file-count" style="display: none;"></div>
    
    <!-- 數據表格 -->
    <table class="data-table" id="dataTable">
      <thead>
        <tr>
          <th>序號</th>
          <th>名稱</th>
          <th>文件路徑</th>
          <th>生成文件名</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody id="tableBody">
        <!-- 數據將動態生成 -->
      </tbody>
    </table>
  </div>

  <script>
    // 全局變量存儲解析的數據
    let excelData = [];
    let allFileUrls = [];
    
    // DOM元素
    const excelFileInput = document.getElementById('excelFile');
    const fileInfoDiv = document.getElementById('fileInfo');
    const fileNameSpan = document.getElementById('fileName');
    const rowCountSpan = document.getElementById('rowCount');
    const totalFileCountSpan = document.getElementById('totalFileCount');
    const dataTable = document.getElementById('dataTable');
    const tableBody = document.getElementById('tableBody');
    const downloadAllBtn = document.getElementById('downloadAll');
    const downloadZipBtn = document.getElementById('downloadZip');
    const clearDataBtn = document.getElementById('clearData');
    const statusMessage = document.getElementById('statusMessage');
    const progressContainer = document.getElementById('progressContainer');
    const progressBar = document.getElementById('progressBar');
    const fileCountDisplay = document.getElementById('fileCountDisplay');
    
    // 初始化事件監聽
    document.addEventListener('DOMContentLoaded', () => {
      clearDataBtn.addEventListener('click', clearAllData);
      downloadAllBtn.addEventListener('click', downloadAllFiles);
      downloadZipBtn.addEventListener('click', downloadAllAsZip);
    });
    
    // 處理Excel文件上傳
    function handleFileUpload(event) {
      const file = event.target.files[0];
      if (!file) return;
      
      // 檢查文件類型
      const fileExtension = file.name.split('.').pop().toLowerCase();
      if (fileExtension !== 'xlsx' && fileExtension !== 'xls') {
        showStatus('請上傳Excel文件(.xlsx或.xls)', 'error');
        return;
      }
      
      showStatus('正在解析Excel文件...', 'loading');
      
      const reader = new FileReader();
      reader.onload = function(e) {
        try {
          // 解析Excel文件
          const data = new Uint8Array(e.target.result);
          const workbook = XLSX.read(data, { type: 'array' });
          
          // 獲取第一個工作表
          const firstSheetName = workbook.SheetNames[0];
          const worksheet = workbook.Sheets[firstSheetName];
          
          // 轉換為JSON
          const jsonData = XLSX.utils.sheet_to_json(worksheet);
          
          // 驗證數據格式
          if (!validateExcelData(jsonData)) {
            showStatus('Excel格式錯誤:必須包含path和name列', 'error');
            return;
          }
          
          // 處理數據(拆分逗號分隔的path)
          processExcelData(jsonData);
          
          // 顯示數據
          displayExcelData();
          
          // 更新文件信息
          fileNameSpan.textContent = file.name;
          rowCountSpan.textContent = jsonData.length;
          totalFileCountSpan.textContent = allFileUrls.length;
          
          fileInfoDiv.style.display = 'block';
          dataTable.style.display = 'table';
          fileCountDisplay.style.display = 'inline-block';
          fileCountDisplay.textContent = `總計 ${allFileUrls.length} 個文件`;
          
          // 啓用按鈕
          downloadAllBtn.disabled = false;
          downloadZipBtn.disabled = false;
          
          showStatus('Excel文件解析成功', 'success');
          
        } catch (error) {
          showStatus(`解析失敗: ${error.message}`, 'error');
          console.error(error);
        }
      };
      
      reader.readAsArrayBuffer(file);
    }
    
    // 驗證Excel數據格式
    function validateExcelData(data) {
      if (data.length === 0) return false;
      
      // 檢查第一行是否包含path和name列(不區分大小寫)
      const firstRow = data[0];
      const keys = Object.keys(firstRow).map(key => key.toLowerCase());
      
      return keys.includes('path') && keys.includes('name');
    }
    
    // 提取學生姓名(從"-"前獲取)
    function extractStudentName(fullName) {
      if (!fullName) return '未知名稱';
      
      if (fullName.includes('-')) {
        return fullName.split('-')[0].trim();
      }
      return fullName.trim();
    }
    
    // 處理Excel數據(拆分逗號分隔的path)
    function processExcelData(jsonData) {
      excelData = [];
      allFileUrls = [];
      
      // 首先構建所有文件的列表
      let allFiles = [];
      
      jsonData.forEach((row, index) => {
        // 標準化列名(不區分大小寫)
        const rowData = {};
        Object.keys(row).forEach(key => {
          const lowerKey = key.toLowerCase();
          rowData[lowerKey] = row[key];
        });
        
        const originalName = rowData.name || `未命名_${index + 1}`;
        const studentName = extractStudentName(originalName);
        const pathStr = (rowData.path || '').toString().trim();
        
        // 拆分逗號分隔的path
        const paths = pathStr.split(',').map(path => path.trim()).filter(path => path);
        
        // 添加到總文件列表
        paths.forEach(path => {
          allFiles.push({
            path: path,
            originalName: originalName,
            studentName: studentName,
            rowIndex: index
          });
        });
      });
      
      // 統計每個學生的文件數量
      const studentFileCount = {};
      allFiles.forEach(file => {
        if (!studentFileCount[file.studentName]) {
          studentFileCount[file.studentName] = 0;
        }
        studentFileCount[file.studentName]++;
      });
      
      // 為每個學生的文件分配序號並生成最終數據
      const studentCurrentIndex = {};
      
      jsonData.forEach((row, index) => {
        const rowData = {};
        Object.keys(row).forEach(key => {
          const lowerKey = key.toLowerCase();
          rowData[lowerKey] = row[key];
        });
        
        const originalName = rowData.name || `未命名_${index + 1}`;
        const studentName = extractStudentName(originalName);
        const pathStr = (rowData.path || '').toString().trim();
        
        // 初始化當前序號
        if (!studentCurrentIndex[studentName]) {
          studentCurrentIndex[studentName] = 0;
        }
        
        // 拆分逗號分隔的path
        const paths = pathStr.split(',').map(path => path.trim()).filter(path => path);
        
        // 為每個路徑生成帶序號的文件名
        const fileItems = [];
        paths.forEach(path => {
          if (path) {
            // 增加當前計數器
            studentCurrentIndex[studentName]++;
            
            // 獲取文件擴展名
            const extension = path.split('.').pop().split('?')[0].toLowerCase();
            
            // 生成帶序號的文件名
            let newFileName = studentName;
            if (studentFileCount[studentName] > 1) {
              newFileName += studentCurrentIndex[studentName];
            }
            newFileName += `.${extension}`;
            
            // 添加到文件列表
            fileItems.push({
              path: path,
              newFileName: newFileName,
              originalName: originalName,
              studentName: studentName,
              index: studentCurrentIndex[studentName]
            });
            
            // 添加到總文件列表
            allFileUrls.push({
              url: path,
              name: studentName,
              newFileName: newFileName,
              originalRow: index + 1,
              fileIndex: studentCurrentIndex[studentName]
            });
          }
        });
        
        // 存儲處理後的數據
        excelData.push({
          originalName: originalName,
          studentName: studentName,
          files: fileItems,
          hasMultiplePaths: fileItems.length > 1
        });
      });
    }
    
    // 顯示Excel數據
    function displayExcelData() {
      tableBody.innerHTML = '';
      
      excelData.forEach((item, index) => {
        if (item.files && item.files.length > 0) {
          item.files.forEach((file, fileIndex) => {
            const row = document.createElement('tr');
            
            // 序號(只在第一個文件顯示行號)
            const indexCell = document.createElement('td');
            if (fileIndex === 0) {
              indexCell.textContent = index + 1;
              indexCell.rowSpan = item.files.length;
            }
            
            // 名稱(只在第一個文件顯示)
            const nameCell = document.createElement('td');
            if (fileIndex === 0) {
              nameCell.textContent = item.studentName;
              nameCell.rowSpan = item.files.length;
            }
            
            // 路徑
            const pathCell = document.createElement('td');
            pathCell.className = 'path-cell';
            pathCell.textContent = file.path || '';
            
            // 生成的文件名
            const fileNameCell = document.createElement('td');
            fileNameCell.innerHTML = `<strong>${file.newFileName || ''}</strong>`;
            if (item.files.length > 1) {
              fileNameCell.innerHTML += `<div class="filename-preview">(${item.studentName}${file.index})</div>`;
            }
            
            // 操作按鈕
            const actionCell = document.createElement('td');
            const downloadBtn = document.createElement('button');
            downloadBtn.className = 'download-btn';
            downloadBtn.innerHTML = '<i class="fas fa-download"></i> 下載';
            downloadBtn.onclick = () => {
              if (file.path && file.newFileName) {
                downloadSingleFileWithNewName(file.path, file.newFileName);
              } else {
                showStatus('文件信息不完整', 'error');
              }
            };
            
            actionCell.appendChild(downloadBtn);
            
            // 添加單元格到行
            if (fileIndex === 0) {
              row.appendChild(indexCell);
              row.appendChild(nameCell);
            }
            row.appendChild(pathCell);
            row.appendChild(fileNameCell);
            row.appendChild(actionCell);
            
            // 添加行到表格
            tableBody.appendChild(row);
          });
        }
      });
    }
    
    // 下載單個文件(使用新的帶序號的名稱)
    function downloadSingleFileWithNewName(url, newFileName) {
      if (!url || !newFileName) {
        showStatus('文件信息不完整', 'error');
        return;
      }
      
      showStatus(`正在下載: ${newFileName}`, 'loading');
      
      try {
        // 創建下載鏈接
        const link = document.createElement('a');
        link.href = url;
        link.download = newFileName;
        link.target = '_blank';
        
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        
        setTimeout(() => {
          showStatus(`文件已下載: ${newFileName}`, 'success');
        }, 500);
        
      } catch (error) {
        showStatus(`下載失敗: ${newFileName}`, 'error');
        console.error('下載失敗:', error);
      }
    }
    
    // 下載所有文件
    function downloadAllFiles() {
      if (!allFileUrls || allFileUrls.length === 0) {
        showStatus('沒有可下載的文件', 'error');
        return;
      }
      
      showStatus(`開始下載所有 ${allFileUrls.length} 個文件...`, 'loading');
      progressContainer.style.display = 'block';
      
      // 逐個下載文件,添加延遲避免瀏覽器限制
      allFileUrls.forEach((fileInfo, index) => {
        setTimeout(() => {
          if (fileInfo.url && fileInfo.newFileName) {
            downloadSingleFileWithNewName(fileInfo.url, fileInfo.newFileName);
          }
          
          // 更新進度
          updateProgress((index + 1) / allFileUrls.length * 100);
          
          // 最後一個文件
          if (index === allFileUrls.length - 1) {
            setTimeout(() => {
              showStatus('所有文件下載已啓動', 'success');
              updateProgress(0);
              progressContainer.style.display = 'none';
            }, 1000);
          }
        }, index * 300); // 間隔300ms
      });
    }
    
    // 下載所有文件為壓縮包
    function downloadAllAsZip() {
      if (!allFileUrls || allFileUrls.length === 0) {
        showStatus('沒有可下載的文件', 'error');
        return;
      }
      
      showStatus('正在下載並壓縮文件...', 'loading');
      progressContainer.style.display = 'block';
      
      const zip = new JSZip();
      let completed = 0;
      const totalFiles = allFileUrls.length;
      
      // 下載所有文件並添加到壓縮包
      allFileUrls.forEach((fileInfo, index) => {
        if (!fileInfo.url || !fileInfo.newFileName) {
          completed++;
          if (completed === totalFiles) {
            generateZipFile(zip);
          }
          return;
        }
        
        fetchFile(fileInfo.url)
          .then(blob => {
            completed++;
            
            // 使用帶序號的文件名添加到壓縮包
            // 按學生姓名創建文件夾
            const folder = zip.folder(fileInfo.name) || zip;
            folder.file(fileInfo.newFileName, blob);
            
            // 更新進度
            updateProgress(completed / totalFiles * 100);
            
            // 所有文件處理完成
            if (completed === totalFiles) {
              generateZipFile(zip);
            }
          })
          .catch(error => {
            console.error(`下載失敗: ${fileInfo.url}`, error);
            completed++;
            
            // 繼續處理其他文件
            if (completed === totalFiles) {
              generateZipFile(zip);
            }
          });
      });
    }
    
    // 獲取文件Blob
    function fetchFile(url) {
      return fetch(url)
        .then(response => {
          if (!response.ok) {
            throw new Error(`HTTP錯誤! 狀態碼: ${response.status}`);
          }
          return response.blob();
        });
    }
    
    // 生成壓縮文件
    function generateZipFile(zip) {
      showStatus('正在生成壓縮文件...', 'loading');
      
      zip.generateAsync({
        type: 'blob',
        compression: 'DEFLATE',
        compressionOptions: { level: 6 }
      }, metadata => {
        // 更新壓縮進度
        updateProgress(50 + metadata.percent / 2);
      })
      .then(content => {
        // 下載壓縮文件
        const zipFileName = `學生文件_${new Date().getFullYear()}${(new Date().getMonth()+1).toString().padStart(2,'0')}${new Date().getDate().toString().padStart(2,'0')}.zip`;
        saveAs(content, zipFileName);
        
        updateProgress(100);
        setTimeout(() => {
          progressContainer.style.display = 'none';
          showStatus(`壓縮包已生成: ${zipFileName}`, 'success');
          updateProgress(0);
        }, 500);
      })
      .catch(error => {
        showStatus(`壓縮失敗: ${error.message}`, 'error');
        progressContainer.style.display = 'none';
        updateProgress(0);
      });
    }
    
    // 更新進度條
    function updateProgress(percent) {
      if (progressBar) {
        progressBar.style.width = `${percent}%`;
      }
    }
    
    // 顯示狀態消息
    function showStatus(message, type) {
      if (statusMessage) {
        statusMessage.textContent = message;
        statusMessage.className = `status-message ${type}`;
        
        // 自動清除成功消息
        if (type === 'success') {
          setTimeout(() => {
            if (statusMessage) {
              statusMessage.className = 'status-message';
            }
          }, 3000);
        }
      }
    }
    
    // 清空所有數據
    function clearAllData() {
      excelData = [];
      allFileUrls = [];
      
      // 重置UI
      if (tableBody) tableBody.innerHTML = '';
      if (fileInfoDiv) fileInfoDiv.style.display = 'none';
      if (dataTable) dataTable.style.display = 'none';
      if (fileCountDisplay) fileCountDisplay.style.display = 'none';
      if (statusMessage) statusMessage.className = 'status-message';
      
      // 禁用按鈕
      if (downloadAllBtn) downloadAllBtn.disabled = true;
      if (downloadZipBtn) downloadZipBtn.disabled = true;
      
      // 重置文件輸入
      if (excelFileInput) excelFileInput.value = '';
      
      showStatus('數據已清空', 'success');
    }
  </script>
</body>
</html>