/** * CSV 匯入匯出工具 * 提供 CSV 文件的匯入和匯出功能 */ const CSVUtils = { /** * 將數據匯出為 CSV 文件 * @param {Array} data - 數據陣列 * @param {String} filename - 文件名稱 * @param {Array} headers - CSV 標題行(可選) */ exportToCSV(data, filename, headers = null) { if (!data || data.length === 0) { alert('沒有資料可以匯出'); return; } // 如果沒有提供標題,從第一筆資料取得所有鍵 if (!headers) { headers = Object.keys(data[0]); } // 構建 CSV 內容 let csvContent = '\uFEFF'; // BOM for UTF-8 // 添加標題行 csvContent += headers.join(',') + '\n'; // 添加數據行 data.forEach(row => { const values = headers.map(header => { let value = this.getNestedValue(row, header); // 處理空值 if (value === null || value === undefined) { return ''; } // 轉換為字符串 value = String(value); // 如果包含逗號、引號或換行符,需要用引號包圍 if (value.includes(',') || value.includes('"') || value.includes('\n')) { value = '"' + value.replace(/"/g, '""') + '"'; } return value; }); csvContent += values.join(',') + '\n'; }); // 創建 Blob 並下載 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }, /** * 從 CSV 文件匯入數據 * @param {File} file - CSV 文件 * @param {Function} callback - 回調函數,接收解析後的數據 */ importFromCSV(file, callback) { if (!file) { alert('請選擇文件'); return; } if (!file.name.endsWith('.csv')) { alert('請選擇 CSV 文件'); return; } const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target.result; const data = this.parseCSV(text); if (data && data.length > 0) { callback(data); } else { alert('CSV 文件為空或格式錯誤'); } } catch (error) { console.error('CSV 解析錯誤:', error); alert('CSV 文件解析失敗: ' + error.message); } }; reader.onerror = () => { alert('文件讀取失敗'); }; reader.readAsText(file, 'UTF-8'); }, /** * 解析 CSV 文本 * @param {String} text - CSV 文本內容 * @returns {Array} 解析後的數據陣列 */ parseCSV(text) { // 移除 BOM if (text.charCodeAt(0) === 0xFEFF) { text = text.substr(1); } const lines = text.split('\n').filter(line => line.trim()); if (lines.length === 0) { return []; } // 第一行是標題 const headers = this.parseCSVLine(lines[0]); const data = []; // 解析數據行 for (let i = 1; i < lines.length; i++) { const values = this.parseCSVLine(lines[i]); if (values.length === headers.length) { const row = {}; headers.forEach((header, index) => { row[header] = values[index]; }); data.push(row); } } return data; }, /** * 解析單行 CSV * @param {String} line - CSV 行 * @returns {Array} 值陣列 */ parseCSVLine(line) { const values = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; const nextChar = line[i + 1]; if (char === '"') { if (inQuotes && nextChar === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { values.push(current); current = ''; } else { current += char; } } values.push(current); return values; }, /** * 獲取嵌套對象的值 * @param {Object} obj - 對象 * @param {String} path - 路徑(支援 a.b.c 格式) * @returns {*} 值 */ getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current ? current[key] : undefined; }, obj); }, /** * 創建 CSV 匯入按鈕 * @param {Function} onImport - 匯入成功的回調函數 * @returns {HTMLElement} 按鈕元素 */ createImportButton(onImport) { const input = document.createElement('input'); input.type = 'file'; input.accept = '.csv'; input.style.display = 'none'; input.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { this.importFromCSV(file, onImport); } }); const button = document.createElement('button'); button.className = 'btn btn-secondary'; button.innerHTML = '📥 匯入 CSV'; button.onclick = () => input.click(); const container = document.createElement('div'); container.style.display = 'inline-block'; container.appendChild(input); container.appendChild(button); return container; }, /** * 創建 CSV 匯出按鈕 * @param {Function} getData - 獲取數據的函數 * @param {String} filename - 文件名稱 * @param {Array} headers - CSV 標題 * @returns {HTMLElement} 按鈕元素 */ createExportButton(getData, filename, headers = null) { const button = document.createElement('button'); button.className = 'btn btn-secondary'; button.innerHTML = '📤 匯出 CSV'; button.onclick = () => { const data = getData(); this.exportToCSV(data, filename, headers); }; return button; } }; // 導出為全局變量 if (typeof window !== 'undefined') { window.CSVUtils = CSVUtils; }