Files
ewoooc/templates/vendor_stockout_import_v2.html
OoO 8e9fb3dd17
All checks were successful
CD Pipeline / deploy (push) Successful in 1m46s
feat(frontend): 將 V2 設為正式預設介面
2026-05-01 00:28:56 +08:00

575 lines
18 KiB
HTML
Raw 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.
{% extends 'ewoooc_base.html' %}
{% block title %}EwoooC 缺貨匯入{% endblock %}
{% block extra_css %}
<style>
.stockout-import-stack {
display: grid;
gap: 22px;
}
.stockout-import-header,
.stockout-upload-panel,
.stockout-import-card {
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: 8px;
}
.stockout-import-header {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.7fr);
gap: 18px;
padding: 22px;
}
.stockout-import-eyebrow {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 7px;
margin-bottom: 10px;
padding: 4px 10px;
color: var(--momo-accent-strong);
background: var(--momo-accent-soft);
border-radius: var(--momo-radius-pill);
font-size: 11px;
font-weight: 800;
}
.stockout-import-title {
margin: 0;
color: var(--momo-text-primary);
font-size: 30px;
font-weight: 800;
letter-spacing: 0;
}
.stockout-import-subtitle {
max-width: 760px;
margin: 8px 0 0;
color: var(--momo-text-secondary);
font-size: 13px;
line-height: 1.7;
}
.stockout-import-actions,
.stockout-result-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.stockout-action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 36px;
padding: 8px 14px;
color: var(--momo-text-primary);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: 4px;
font-size: 12px;
font-weight: 800;
text-decoration: none;
transition: var(--momo-transition-base);
}
.stockout-action:hover {
color: var(--momo-text-primary);
border-color: var(--momo-border-strong);
transform: translateY(-1px);
}
.stockout-action.is-primary {
color: var(--momo-text-inverse);
background: var(--momo-ink);
border-color: var(--momo-ink);
}
.stockout-import-note {
display: grid;
align-content: center;
gap: 8px;
padding: 16px;
color: var(--momo-text-inverse);
background: var(--momo-ink);
border-radius: 8px;
}
.stockout-import-note-label {
color: rgba(250, 247, 240, 0.62);
font-size: 11px;
font-weight: 800;
}
.stockout-import-note-value {
color: var(--momo-text-inverse);
font-size: 18px;
font-weight: 800;
line-height: 1.35;
}
.stockout-upload-panel {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
gap: 0;
overflow: hidden;
}
.stockout-dropzone {
display: grid;
min-height: 330px;
cursor: pointer;
place-items: center;
padding: 30px;
background: var(--momo-bg-surface);
border: 0;
border-right: 1px solid var(--momo-border-light);
transition: var(--momo-transition-base);
}
.stockout-dropzone:hover,
.stockout-dropzone.is-dragover {
background: var(--momo-bg-paper);
}
.stockout-dropzone-inner {
max-width: 520px;
text-align: center;
}
.stockout-upload-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 58px;
height: 58px;
color: var(--momo-accent-strong);
background: var(--momo-accent-soft);
border-radius: 8px;
font-size: 24px;
}
.stockout-dropzone-title {
margin: 18px 0 8px;
color: var(--momo-text-primary);
font-size: 20px;
font-weight: 800;
}
.stockout-dropzone-subtitle {
margin: 0;
color: var(--momo-text-secondary);
font-size: 13px;
line-height: 1.7;
}
.stockout-import-side {
display: grid;
gap: 12px;
align-content: start;
padding: 18px;
background: var(--momo-bg-paper);
}
.stockout-import-card {
padding: 16px;
}
.stockout-card-label {
color: var(--momo-text-secondary);
font-size: 11px;
font-weight: 800;
}
.stockout-card-title {
margin-top: 8px;
color: var(--momo-text-primary);
font-size: 15px;
font-weight: 800;
line-height: 1.5;
}
.stockout-card-meta {
margin-top: 8px;
color: var(--momo-text-tertiary);
font-size: 12px;
line-height: 1.7;
}
.stockout-file-panel,
.stockout-progress-panel,
.stockout-result-panel,
.stockout-error-panel {
display: none;
padding: 18px;
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: 8px;
}
.stockout-file-panel.is-visible,
.stockout-progress-panel.is-visible,
.stockout-result-panel.is-visible,
.stockout-error-panel.is-visible {
display: block;
}
.stockout-file-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.stockout-file-name {
color: var(--momo-text-primary);
font-size: 14px;
font-weight: 800;
word-break: break-word;
}
.stockout-file-size {
color: var(--momo-text-tertiary);
font-size: 11px;
white-space: nowrap;
}
.stockout-progress-track {
height: 8px;
overflow: hidden;
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-pill);
}
.stockout-progress-bar {
width: 100%;
height: 100%;
background: var(--momo-accent);
animation: stockout-pulse 1.1s ease-in-out infinite alternate;
}
.stockout-result-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 14px;
}
.stockout-result-cell {
padding: 14px;
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: 6px;
}
.stockout-result-value {
color: var(--momo-text-primary);
font-size: 26px;
font-weight: 800;
line-height: 1;
}
.stockout-result-label {
margin-top: 8px;
color: var(--momo-text-secondary);
font-size: 11px;
}
.stockout-batch-id {
margin-top: 14px;
color: var(--momo-text-secondary);
font-size: 12px;
}
.stockout-error-panel {
color: var(--momo-danger);
background: rgba(214, 83, 68, 0.08);
border-color: rgba(214, 83, 68, 0.22);
}
@keyframes stockout-pulse {
from { opacity: 0.48; }
to { opacity: 1; }
}
@media (max-width: 980px) {
.stockout-import-header,
.stockout-upload-panel {
grid-template-columns: 1fr;
}
.stockout-dropzone {
border-right: 0;
border-bottom: 1px solid var(--momo-border-light);
}
}
@media (max-width: 720px) {
.stockout-import-title {
font-size: 26px;
}
.stockout-file-row {
flex-direction: column;
align-items: flex-start;
}
.stockout-result-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<div class="stockout-import-stack">
<section class="stockout-import-header">
<div>
<div class="stockout-import-eyebrow momo-mono">
<i class="fas fa-file-import"></i>
STOCKOUT IMPORT
</div>
<h1 class="stockout-import-title">Excel 匯入</h1>
<p class="stockout-import-subtitle">
上傳正式缺貨 Excel 後,系統會使用既有匯入 API 寫入 vendor_stockout完成後直接回傳批次編號與成功、重複、失敗筆數。
</p>
<div class="stockout-import-actions mt-3">
<a class="stockout-action" href="{{ url_for('vendor.index') }}">
<i class="fas fa-arrow-left"></i>
回總覽
</a>
<a class="stockout-action" href="{{ url_for('vendor.list_page') }}">
<i class="fas fa-table-list"></i>
缺貨清單
</a>
</div>
</div>
<aside class="stockout-import-note">
<div class="stockout-import-note-label momo-mono">資料原則</div>
<div class="stockout-import-note-value">只匯入使用者上傳的正式 Excel不預填任何資料列。</div>
</aside>
</section>
<section class="stockout-upload-panel" aria-label="Excel 匯入">
<button class="stockout-dropzone" id="stockoutDropzone" type="button">
<input id="stockoutFileInput" type="file" accept=".xlsx,.xls" hidden>
<span class="stockout-dropzone-inner">
<span class="stockout-upload-icon">
<i class="fas fa-cloud-arrow-up"></i>
</span>
<span class="stockout-dropzone-title d-block">選擇或拖曳 Excel 檔案</span>
<span class="stockout-dropzone-subtitle d-block">
支援 .xlsx / .xls。檔案送出後會寫入正式缺貨資料表請確認內容是可匯入的正式清單。
</span>
<span class="stockout-action is-primary mt-3">
<i class="fas fa-folder-open"></i>
選擇檔案
</span>
</span>
</button>
<aside class="stockout-import-side">
<div class="stockout-import-card">
<div class="stockout-card-label momo-mono">必要欄位</div>
<div class="stockout-card-title">來源供應商編號、來源供應商名稱、商品ID、商品名稱</div>
<div class="stockout-card-meta momo-mono">缺少必要欄位時API 會拒絕匯入並回傳錯誤原因。</div>
</div>
<div class="stockout-import-card">
<div class="stockout-card-label momo-mono">欄位範本</div>
<div class="stockout-card-title">下載只有正式欄位標題的空白範本</div>
<div class="stockout-card-meta">
<a class="stockout-action" href="{{ url_for('vendor.api_import_template') }}">
<i class="fas fa-download"></i>
下載範本
</a>
</div>
</div>
</aside>
</section>
<section class="stockout-file-panel" id="stockoutFilePanel">
<div class="stockout-file-row">
<div>
<div class="stockout-card-label momo-mono">已選擇檔案</div>
<div class="stockout-file-name" id="stockoutFileName"></div>
<div class="stockout-file-size momo-mono" id="stockoutFileSize"></div>
</div>
<button class="stockout-action is-primary" id="stockoutUploadButton" type="button">
<i class="fas fa-upload"></i>
開始匯入
</button>
</div>
</section>
<section class="stockout-progress-panel" id="stockoutProgressPanel">
<div class="stockout-card-label momo-mono mb-2">匯入中</div>
<div class="stockout-progress-track">
<div class="stockout-progress-bar"></div>
</div>
</section>
<section class="stockout-result-panel" id="stockoutResultPanel">
<div class="stockout-card-label momo-mono">匯入結果</div>
<div class="stockout-result-grid">
<div class="stockout-result-cell">
<div class="stockout-result-value momo-mono" id="stockoutTotalCount">0</div>
<div class="stockout-result-label">總筆數</div>
</div>
<div class="stockout-result-cell">
<div class="stockout-result-value momo-mono" id="stockoutSuccessCount">0</div>
<div class="stockout-result-label">成功匯入</div>
</div>
<div class="stockout-result-cell">
<div class="stockout-result-value momo-mono" id="stockoutDuplicateCount">0</div>
<div class="stockout-result-label">重複項目</div>
</div>
<div class="stockout-result-cell">
<div class="stockout-result-value momo-mono" id="stockoutFailedCount">0</div>
<div class="stockout-result-label">失敗</div>
</div>
</div>
<div class="stockout-batch-id momo-mono">批次編號 <span id="stockoutBatchId"></span></div>
<div class="stockout-import-actions mt-3">
<a class="stockout-action is-primary" href="{{ url_for('vendor.list_page') }}">
<i class="fas fa-table-list"></i>
查看缺貨清單
</a>
<button class="stockout-action" id="stockoutImportAgainButton" type="button">
<i class="fas fa-rotate-right"></i>
再次匯入
</button>
</div>
</section>
<section class="stockout-error-panel" id="stockoutErrorPanel">
<div class="stockout-card-label momo-mono">匯入失敗</div>
<div class="stockout-card-title" id="stockoutErrorMessage"></div>
</section>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const dropzone = document.getElementById('stockoutDropzone');
const fileInput = document.getElementById('stockoutFileInput');
const filePanel = document.getElementById('stockoutFilePanel');
const fileName = document.getElementById('stockoutFileName');
const fileSize = document.getElementById('stockoutFileSize');
const uploadButton = document.getElementById('stockoutUploadButton');
const progressPanel = document.getElementById('stockoutProgressPanel');
const resultPanel = document.getElementById('stockoutResultPanel');
const errorPanel = document.getElementById('stockoutErrorPanel');
const errorMessage = document.getElementById('stockoutErrorMessage');
const importAgainButton = document.getElementById('stockoutImportAgainButton');
let selectedFile = null;
function show(panel) {
panel.classList.add('is-visible');
}
function hide(panel) {
panel.classList.remove('is-visible');
}
function resetTransientPanels() {
hide(progressPanel);
hide(resultPanel);
hide(errorPanel);
}
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';
}
function handleFile(file) {
if (!file) return;
if (!file.name.match(/\.(xlsx|xls)$/i)) {
errorMessage.textContent = '請選擇 Excel 檔案 (.xlsx 或 .xls)';
show(errorPanel);
return;
}
selectedFile = file;
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
resetTransientPanels();
show(filePanel);
}
dropzone.addEventListener('click', function () {
fileInput.click();
});
fileInput.addEventListener('change', function (event) {
handleFile(event.target.files[0]);
});
dropzone.addEventListener('dragover', function (event) {
event.preventDefault();
dropzone.classList.add('is-dragover');
});
dropzone.addEventListener('dragleave', function () {
dropzone.classList.remove('is-dragover');
});
dropzone.addEventListener('drop', function (event) {
event.preventDefault();
dropzone.classList.remove('is-dragover');
handleFile(event.dataTransfer.files[0]);
});
uploadButton.addEventListener('click', async function () {
if (!selectedFile) return;
const formData = new FormData();
formData.append('file', selectedFile);
hide(filePanel);
hide(resultPanel);
hide(errorPanel);
show(progressPanel);
try {
const response = await fetchWithCSRF('/vendor-stockout/api/import/excel', {
method: 'POST',
body: formData
});
const result = await response.json();
hide(progressPanel);
if (result.success) {
document.getElementById('stockoutTotalCount').textContent = result.data.total_count;
document.getElementById('stockoutSuccessCount').textContent = result.data.success_count;
document.getElementById('stockoutDuplicateCount').textContent = result.data.duplicate_count;
document.getElementById('stockoutFailedCount').textContent = result.data.failed_count;
document.getElementById('stockoutBatchId').textContent = result.data.batch_id;
show(resultPanel);
} else {
errorMessage.textContent = result.message || '匯入失敗';
show(errorPanel);
}
} catch (error) {
hide(progressPanel);
errorMessage.textContent = '網路錯誤:' + error.message;
show(errorPanel);
}
});
importAgainButton.addEventListener('click', function () {
selectedFile = null;
fileInput.value = '';
hide(filePanel);
resetTransientPanels();
});
})();
</script>
{% endblock %}