Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
354 lines
13 KiB
HTML
354 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>Excel 匯入 - 廠商缺貨系統</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<style>
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
|
min-height: 100vh;
|
|
padding-top: 70px;
|
|
}
|
|
|
|
.navbar-dark.bg-primary {
|
|
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
|
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
|
}
|
|
|
|
.navbar-dark .navbar-brand {
|
|
color: #ffffff !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.navbar-dark .navbar-nav .nav-link {
|
|
color: rgba(255, 255, 255, 0.9) !important;
|
|
font-weight: 500;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.navbar-dark .navbar-nav .nav-link:hover {
|
|
color: #ffffff !important;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.navbar-dark .navbar-nav .nav-link.active {
|
|
color: #ffffff !important;
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.navbar-dark .navbar-text {
|
|
color: rgba(255, 255, 255, 0.8) !important;
|
|
}
|
|
|
|
.navbar {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
|
|
}
|
|
|
|
.navbar.bg-custom-dark {
|
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
|
|
}
|
|
|
|
.upload-area {
|
|
border: 3px dashed #3498db;
|
|
border-radius: 20px;
|
|
padding: 3rem;
|
|
text-align: center;
|
|
background: white;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
border-color: #2980b9;
|
|
background: #f8f9fa;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #27ae60;
|
|
background: #d5f4e6;
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 5rem;
|
|
color: #3498db;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.btn-upload {
|
|
padding: 1rem 3rem;
|
|
font-size: 1.1rem;
|
|
border-radius: 50px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.progress {
|
|
height: 30px;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.result-card {
|
|
border-left: 5px solid #3498db;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- 導航列 -->
|
|
{% include 'components/_navbar.html' %}
|
|
|
|
<div class="container" style="padding: 2rem; max-width: 900px;">
|
|
<!-- 頁面標題 -->
|
|
<div class="card mb-4" style="border-radius: 16px;">
|
|
<div class="card-body">
|
|
<h2 class="mb-3">
|
|
<i class="fas fa-file-excel text-primary me-3"></i>Excel 匯入
|
|
</h2>
|
|
<p class="text-muted mb-0">上傳包含缺貨商品資料的 Excel 檔案,系統會自動檢測並匯入</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 上傳區域 -->
|
|
<div class="card mb-4" style="border-radius: 16px;">
|
|
<div class="card-body">
|
|
<div class="upload-area" id="uploadArea">
|
|
<input type="file" id="fileInput" accept=".xlsx,.xls" style="display: none;">
|
|
<div class="upload-icon">
|
|
<i class="fas fa-cloud-upload-alt"></i>
|
|
</div>
|
|
<h4 class="mb-3">拖曳檔案到此處或點擊選擇檔案</h4>
|
|
<p class="text-muted">支援格式: Excel (.xlsx, .xls)</p>
|
|
<button type="button" class="btn btn-primary btn-upload mt-3" id="selectFileBtn">
|
|
<i class="fas fa-folder-open me-2"></i>選擇檔案
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-3 text-center">
|
|
<a href="/vendor-stockout/api/import/template" class="btn btn-outline-secondary">
|
|
<i class="fas fa-download me-2"></i>下載 Excel 範本
|
|
</a>
|
|
</div>
|
|
|
|
<!-- 檔案資訊 -->
|
|
<div id="fileInfo" class="mt-4" style="display: none;">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-file me-2"></i>
|
|
<strong>已選擇檔案:</strong> <span id="fileName"></span>
|
|
<span class="badge bg-primary ms-2" id="fileSize"></span>
|
|
</div>
|
|
<button type="button" class="btn btn-success w-100 btn-upload" id="uploadBtn">
|
|
<i class="fas fa-upload me-2"></i>開始匯入
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 進度條 -->
|
|
<div id="progressArea" class="mt-4" style="display: none;">
|
|
<div class="progress">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar" style="width: 100%">
|
|
匯入中...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 匯入結果 -->
|
|
<div id="resultArea" style="display: none;">
|
|
<div class="card result-card" style="border-radius: 16px;">
|
|
<div class="card-body">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-check-circle text-success me-2"></i>匯入完成
|
|
</h5>
|
|
<div class="row mt-4">
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<h3 class="text-primary" id="totalCount">0</h3>
|
|
<small class="text-muted">總筆數</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<h3 class="text-success" id="successCount">0</h3>
|
|
<small class="text-muted">成功匯入</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<h3 class="text-warning" id="duplicateCount">0</h3>
|
|
<small class="text-muted">重複項目</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<h3 class="text-danger" id="failedCount">0</h3>
|
|
<small class="text-muted">失敗</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<p class="mb-2">
|
|
<i class="fas fa-barcode me-2"></i>
|
|
<strong>批次編號:</strong> <code id="batchId"></code>
|
|
</p>
|
|
</div>
|
|
<div class="mt-4 text-center">
|
|
<a href="/vendor-stockout/list" class="btn btn-primary me-2">
|
|
<i class="fas fa-list me-2"></i>查看缺貨清單
|
|
</a>
|
|
<button type="button" class="btn btn-outline-secondary" id="importAgainBtn">
|
|
<i class="fas fa-redo me-2"></i>再次匯入
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 錯誤訊息 -->
|
|
<div id="errorArea" style="display: none;">
|
|
<div class="alert alert-danger">
|
|
<h5><i class="fas fa-exclamation-circle me-2"></i>匯入失敗</h5>
|
|
<p class="mb-0" id="errorMessage"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const selectFileBtn = document.getElementById('selectFileBtn');
|
|
const fileInfo = document.getElementById('fileInfo');
|
|
const fileName = document.getElementById('fileName');
|
|
const fileSize = document.getElementById('fileSize');
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
const progressArea = document.getElementById('progressArea');
|
|
const resultArea = document.getElementById('resultArea');
|
|
const errorArea = document.getElementById('errorArea');
|
|
const importAgainBtn = document.getElementById('importAgainBtn');
|
|
|
|
let selectedFile = null;
|
|
|
|
// 點擊上傳區域
|
|
uploadArea.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
selectFileBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
fileInput.click();
|
|
});
|
|
|
|
// 檔案選擇
|
|
fileInput.addEventListener('change', (e) => {
|
|
handleFile(e.target.files[0]);
|
|
});
|
|
|
|
// 拖曳事件
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.add('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', () => {
|
|
uploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.remove('dragover');
|
|
handleFile(e.dataTransfer.files[0]);
|
|
});
|
|
|
|
// 處理檔案
|
|
function handleFile(file) {
|
|
if (!file) return;
|
|
|
|
if (!file.name.match(/\.(xlsx|xls)$/i)) {
|
|
alert('請選擇 Excel 檔案 (.xlsx 或 .xls)');
|
|
return;
|
|
}
|
|
|
|
selectedFile = file;
|
|
fileName.textContent = file.name;
|
|
fileSize.textContent = formatFileSize(file.size);
|
|
fileInfo.style.display = 'block';
|
|
resultArea.style.display = 'none';
|
|
errorArea.style.display = 'none';
|
|
}
|
|
|
|
// 上傳按鈕
|
|
uploadBtn.addEventListener('click', async () => {
|
|
if (!selectedFile) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', selectedFile);
|
|
|
|
// 取得 CSRF token
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
|
|
fileInfo.style.display = 'none';
|
|
progressArea.style.display = 'block';
|
|
errorArea.style.display = 'none';
|
|
resultArea.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('/vendor-stockout/api/import/excel', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
progressArea.style.display = 'none';
|
|
|
|
if (result.success) {
|
|
// 顯示成功結果
|
|
document.getElementById('totalCount').textContent = result.data.total_count;
|
|
document.getElementById('successCount').textContent = result.data.success_count;
|
|
document.getElementById('duplicateCount').textContent = result.data.duplicate_count;
|
|
document.getElementById('failedCount').textContent = result.data.failed_count;
|
|
document.getElementById('batchId').textContent = result.data.batch_id;
|
|
resultArea.style.display = 'block';
|
|
} else {
|
|
// 顯示錯誤
|
|
document.getElementById('errorMessage').textContent = result.message;
|
|
errorArea.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
progressArea.style.display = 'none';
|
|
document.getElementById('errorMessage').textContent = '網路錯誤: ' + error.message;
|
|
errorArea.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// 再次匯入
|
|
importAgainBtn.addEventListener('click', () => {
|
|
selectedFile = null;
|
|
fileInput.value = '';
|
|
fileInfo.style.display = 'none';
|
|
resultArea.style.display = 'none';
|
|
errorArea.style.display = 'none';
|
|
});
|
|
|
|
// 格式化檔案大小
|
|
function formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|