Files
ewoooc/web/static/js/page-auto-import.js
OoO 605250619c
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
Frontend V3 responsive production update
2026-05-12 18:27:29 +08:00

350 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* page-auto-import.js — Turn C
* 抽自 auto_import_index.html 原 inline <script>
* 邏輯不變Google Drive 設定 / 手動上傳 / 任務輪詢
*/
(function () {
'use strict';
// 載入配置
async function loadConfig() {
try {
const response = await fetch('/api/import_config');
const result = await response.json();
if (result.success) {
document.getElementById('folderPath').value = result.data.folder_path || '';
document.getElementById('filePattern').value = result.data.file_pattern || '';
}
} catch (error) {
console.error('載入配置失敗:', error);
}
}
// 儲存配置
async function saveConfig() {
const folderPath = document.getElementById('folderPath').value;
const filePattern = document.getElementById('filePattern').value;
try {
const response = await fetch('/api/import_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
folder_path: folderPath,
file_pattern: filePattern
})
});
const result = await response.json();
if (result.success) {
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>配置已儲存');
} else {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
}
} catch (error) {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>儲存配置失敗: ' + error.message);
}
}
// 測試連接
async function testConnection() {
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在測試 Google Drive 連接...');
try {
const response = await fetch('/api/test_drive_connection', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>' + result.message);
} else {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
}
} catch (error) {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>測試失敗: ' + error.message);
}
}
// 列出檔案
async function listFiles() {
const folderPath = document.getElementById('folderPath').value;
const filePattern = document.getElementById('filePattern').value;
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在列出 Google Drive 檔案...');
try {
const response = await fetch('/api/list_drive_files', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
folder_path: folderPath,
file_pattern: filePattern
})
});
const result = await response.json();
if (result.success) {
const fileList = result.data.map(f => `<li class="mb-1"><i class="fas fa-file-excel me-1"></i>${f.name}</li>`).join('');
showAlert('configAlert', 'success', `<i class="fas fa-check-circle me-2"></i>找到 ${result.count} 個檔案:<ul class="mt-2 mb-0">${fileList || '<li>無</li>'}</ul>`);
} else {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
}
} catch (error) {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>列出檔案失敗: ' + error.message);
}
}
// 手動匯入
async function manualImport() {
if (!confirm('確定要立即執行匯入嗎?')) {
return;
}
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在執行匯入,請稍候...');
try {
const response = await fetch('/api/manual_import', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>' + result.message);
setTimeout(() => loadJobs(), 1000);
} else {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
}
} catch (error) {
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>匯入失敗: ' + error.message);
}
}
// 載入任務列表
async function loadJobs() {
document.getElementById('jobsLoading').style.display = 'block';
document.getElementById('jobsEmpty').style.display = 'none';
document.getElementById('jobsTableContainer').style.display = 'none';
try {
const response = await fetch('/api/import_jobs?limit=50');
const result = await response.json();
document.getElementById('jobsLoading').style.display = 'none';
if (result.success && result.data.length > 0) {
document.getElementById('jobsTableContainer').style.display = 'block';
renderJobs(result.data);
} else {
document.getElementById('jobsEmpty').style.display = 'block';
}
} catch (error) {
document.getElementById('jobsLoading').style.display = 'none';
document.getElementById('jobsEmpty').style.display = 'block';
console.error('載入任務列表失敗:', error);
}
}
// 渲染任務列表
function renderJobs(jobs) {
const tbody = document.getElementById('jobsTableBody');
tbody.innerHTML = '';
jobs.forEach(job => {
const tr = document.createElement('tr');
const fileName = job.drive_file_name || 'N/A';
const errorMsg = job.error_message || '-';
tr.innerHTML = `
<td class="fw-bold">#${job.id}</td>
<td>
<i class="fas fa-file-excel text-success me-1"></i>${escapeHtml(fileName)}
</td>
<td>
<span class="badge badge-${job.status}">${getStatusText(job.status)}</span>
</td>
<td style="min-width: 150px;">
<div class="progress mb-1">
<div class="progress-bar" role="progressbar" style="width: ${job.progress_percent}%"
aria-valuenow="${job.progress_percent}" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
<small class="text-muted">${Math.round(job.progress_percent)}% - ${escapeHtml(job.current_step || '')}</small>
</td>
<td class="text-center">
<span class="badge bg-success">${job.success_rows || 0}</span> /
<span class="badge bg-secondary">${job.total_rows || 0}</span>
</td>
<td><small>${formatTime(job.started_at)}</small></td>
<td><small>${formatTime(job.completed_at)}</small></td>
<td class="error-cell"><small class="text-danger">${escapeHtml(errorMsg)}</small></td>
<td class="text-center">
${(job.status === 'downloading' || job.status === 'importing')
? `<button class="btn btn-sm btn-outline-danger" onclick="failJob(${job.id})">
<i class="fas fa-times"></i>
</button>`
: '-'}
</td>
`;
tbody.appendChild(tr);
});
}
// 轉義 HTML 特殊字元
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 取得狀態文字
function getStatusText(status) {
const statusMap = {
'pending': '⏳ 等待中',
'downloading': '⬇️ 下載中',
'importing': '📥 匯入中',
'completed': '✅ 已完成',
'failed': '❌ 失敗'
};
return statusMap[status] || status;
}
// 格式化時間
function formatTime(timeStr) {
if (!timeStr) return '-';
const date = new Date(timeStr);
return date.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// 顯示提示
function showAlert(elementId, type, message) {
const alertDiv = document.getElementById(elementId);
alertDiv.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
if (type === 'success' || type === 'danger') {
setTimeout(() => {
const alert = alertDiv.querySelector('.alert');
if (alert) {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}
}, 5000);
}
}
// 手動上傳檔案
async function uploadManualFile() {
const fileInput = document.getElementById('manualUploadFile');
const file = fileInput.files[0];
if (!file) {
showAlert('uploadAlert', 'warning', '<i class="fas fa-exclamation-triangle me-2"></i>請先選擇檔案');
return;
}
// 檔名驗證
if (!file.name.includes('即時業績') || !file.name.includes('當日')) {
if (!confirm('檔名似乎不符合格式(應包含「即時業績」和「當日」),確定要繼續嗎?')) {
return;
}
}
const formData = new FormData();
formData.append('file', file);
showAlert('uploadAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在上傳並匯入檔案,請稍候...');
try {
const response = await fetch('/api/import_excel', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'success') {
showAlert('uploadAlert', 'success',
`<i class="fas fa-check-circle me-2"></i>${result.message}<br>` +
`<small>資料表: ${result.table} | 共 ${result.rows} 筆資料</small>`
);
fileInput.value = '';
setTimeout(() => loadJobs(), 1000);
} else {
showAlert('uploadAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
}
} catch (error) {
showAlert('uploadAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>上傳失敗: ' + error.message);
}
}
// 初始化
loadConfig();
loadJobs();
// 每 10 秒自動刷新任務列表
setInterval(loadJobs, 10000);
// 重置卡住的任務
async function resetStuckJobs() {
if (!confirm('確定要重置所有卡住超過 1 小時的任務嗎?')) {
return;
}
try {
const response = await fetch('/api/reset_stuck_jobs', { method: 'POST' });
const result = await response.json();
if (result.success) {
alert(result.message);
loadJobs();
} else {
alert('重置失敗: ' + result.message);
}
} catch (error) {
alert('重置失敗: ' + error.message);
}
}
// 手動將任務標記為失敗
async function failJob(jobId) {
if (!confirm(`確定要取消任務 #${jobId} 嗎?`)) {
return;
}
try {
const response = await fetch(`/api/import_jobs/${jobId}/fail`, { method: 'POST' });
const result = await response.json();
if (result.success) {
alert(result.message);
loadJobs();
} else {
alert('取消失敗: ' + result.message);
}
} catch (error) {
alert('取消失敗: ' + error.message);
}
}
})();