- 新增 CSV 匯入匯出功能(所有頁籤) - 新增崗位清單頁籤(含欄位排序) - 新增管理者頁面(使用者 CRUD) - 新增事業體選項(SBU/MBU/HQBU/ITBU/HRBU/ACCBU) - 新增組織單位欄位(處級/部級/課級) - 崗位描述/備注改為條列式說明 - 新增 README.md 文件 - 新增開發指令記錄檔 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
247 lines
6.8 KiB
JavaScript
247 lines
6.8 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|