統一舊模板入口新版設計
All checks were successful
CD Pipeline / deploy (push) Successful in 57s

This commit is contained in:
OoO
2026-05-17 20:30:07 +08:00
parent 626a2ed50a
commit 7c37f066e8
19 changed files with 439 additions and 7956 deletions

6
app.py
View File

@@ -129,9 +129,9 @@ else:
TEMPLATE_DIR = os.path.join(BASE_DIR, 'templates')
STATIC_DIR = os.path.join(BASE_DIR, 'web/static')
# 檢查關鍵模板是否存在
if not os.path.exists(os.path.join(TEMPLATE_DIR, 'dashboard.html')):
sys_log.warning(f"[Web] [Template] ⚠️ 警告: 找不到 dashboard.html | Path: {TEMPLATE_DIR}")
# 檢查關鍵模板是否存在。正式商品看板已遷移到 V3 shell。
if not os.path.exists(os.path.join(TEMPLATE_DIR, 'dashboard_v2.html')):
sys_log.warning(f"[Web] [Template] ⚠️ 警告: 找不到 dashboard_v2.html | Path: {TEMPLATE_DIR}")
app = Flask(__name__,
template_folder=TEMPLATE_DIR,

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.151"
SYSTEM_VERSION = "V10.152"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -1,87 +1,22 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>權限不足 - WOOO TECH</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, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
{% set active_page = 'settings' %}
{% set legacy_bridge_title = '權限不足' %}
{% set legacy_bridge_kicker = 'ACCESS CONTROL' %}
{% set legacy_bridge_icon = 'fas fa-ban' %}
{% set legacy_bridge_heading = '權限不足' %}
{% set legacy_bridge_body = '這個頁面需要更高權限才能存取。若這是你的工作範圍,請改用具備授權的帳號重新登入。' %}
{% set legacy_bridge_target = '/' %}
{% set legacy_bridge_cta = '回到商品看板' %}
{% set legacy_bridge_secondary_target = '/settings' %}
{% set legacy_bridge_secondary_cta = '系統管理' %}
{% set legacy_bridge_meta = '403 / Forbidden' %}
{% extends "ewoooc_base.html" %}
.error-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
width: 100%;
max-width: 500px;
text-align: center;
padding: 3rem;
}
{% block title %}權限不足 - EwoooC{% endblock %}
.error-icon {
font-size: 5rem;
color: #dc3545;
margin-bottom: 1rem;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.error-code {
font-size: 4rem;
font-weight: 700;
color: #333;
margin-bottom: 0.5rem;
}
.error-title {
font-size: 1.5rem;
color: #666;
margin-bottom: 1rem;
}
.error-message {
color: #888;
margin-bottom: 2rem;
}
.btn-back {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 0.75rem 2rem;
font-weight: 600;
color: white;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.btn-back:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
color: white;
}
</style>
</head>
<body>
<div class="error-card">
<i class="fas fa-ban error-icon"></i>
<div class="error-code">403</div>
<div class="error-title">權限不足</div>
<p class="error-message">
您沒有權限存取此頁面。<br>
如需存取,請聯繫系統管理員。
</p>
<a href="{{ url_for('dashboard.index') }}" class="btn-back">
<i class="fas fa-home me-2"></i>返回首頁
</a>
</div>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,432 +1,14 @@
<!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>{% block title %}EwoooC{% endblock %}</title>
{#
Compatibility base.
Older internal test templates used `base.html`; the production design system now
lives in `ewoooc_base.html`, so this file forwards legacy block names into the
V3 shell instead of carrying a second visual language.
#}
{% extends "ewoooc_base.html" %}
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- 共用樣式 -->
<style>
:root {
--primary-gradient: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
--bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg-gradient);
min-height: 100vh;
padding-top: 70px;
}
/* 導航列樣式 */
.navbar {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.navbar.bg-custom-dark {
background: var(--primary-gradient) !important;
}
.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;
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-radius: 8px;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: all 0.2s;
}
.dropdown-item:hover {
background: #f8f9fa;
}
/* 使用者下拉選單 */
.user-dropdown .dropdown-toggle::after {
display: none;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
/* 頁面標題區塊 */
.page-header {
background: white;
padding: 1.5rem 2rem;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-bottom: 1.5rem;
}
.page-header h1 {
font-size: 1.75rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.25rem;
}
.page-header p {
color: #6c757d;
font-size: 0.95rem;
margin-bottom: 0;
}
/* 卡片樣式 */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
/* 按鈕樣式 */
.btn {
border-radius: 8px;
font-weight: 500;
}
/* Toast 通知 */
.toast-container {
z-index: 9999;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 導航列 -->
{% include 'components/_navbar.html' %}
<!-- WOOO 載入動畫 (可選:透過 block 控制是否顯示) -->
{% block loading_overlay %}
{% include 'components/_loading.html' %}
{% endblock %}
<!-- 主要內容 -->
<main class="{% block main_class %}container-fluid{% endblock %}" style="padding: 1.5rem;">
{% block content %}{% endblock %}
</main>
<!-- 頁尾 (可選) -->
{% block footer %}{% endblock %}
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- MOMO 404 防呆攔截:避免自動打開 EC404 頁面 -->
<script>
(function () {
if (window.__momoLinkGuardInstalled) {
return;
}
window.__momoLinkGuardInstalled = true;
const MOMO_HOSTS = new Set(['www.momoshop.com.tw', 'm.momoshop.com.tw']);
const MOMO_CODE_RE = /^[A-Za-z0-9_-]{4,}$/;
const toText = value => (value == null ? '' : String(value));
const hasMomoHost = function (url) {
const target = toText(url).trim();
if (!target) {
return false;
}
try {
const parsed = new URL(target, location.origin);
return MOMO_HOSTS.has((parsed.hostname || '').toLowerCase());
} catch (error) {
return /(^|\/)m(?:\.momoshop\.com\.tw|\.momoshop\.com\.tw)(\/|$)|www\.momoshop\.com\.tw/i.test(target);
}
};
const isBlockedMomoUrl = function (url) {
const lowered = toText(url).toLowerCase();
if (lowered.includes('ec404.html') || lowered.includes('/ecm/js/err404/')) {
return true;
}
try {
const parsed = new URL(url, location.origin);
const path = (parsed.pathname || '').toLowerCase();
if (!path.includes('goodsdetail')) {
return false;
}
const iCode = (parsed.searchParams.get('i_code') || '').trim();
if (iCode) {
return !isLikelyMomoIcode(iCode);
}
return !/\/goodsdetail\/[^/?#]+/i.test(path);
} catch (error) {
const hasGoodsDetail = /goodsdetail\.jsp/i.test(lowered);
if (!hasGoodsDetail) {
return false;
}
const hasIcode = /[?&]i_code=([^&#]+)/i.test(lowered);
if (!hasIcode) {
return true;
}
const match = /[?&]i_code=([^&#]+)/i.exec(lowered);
const extracted = match ? (match[1] || '').trim() : '';
return !isLikelyMomoIcode(extracted);
}
};
const isLikelyMomoIcode = function (value) {
const cleaned = toText(value).trim();
if (!cleaned) {
return false;
}
const lowered = cleaned.toLowerCase();
if (lowered === 'nan' || lowered === 'none' || lowered === 'null' || lowered === 'undefined') {
return false;
}
if (lowered.startsWith('momo_')) {
return false;
}
return MOMO_CODE_RE.test(cleaned);
};
const getIcodeFromUrl = function (url) {
const target = toText(url).trim();
if (!target) {
return '';
}
try {
const parsed = new URL(target, location.origin);
const iCode = parsed.searchParams.get('i_code');
if (iCode) {
return iCode.trim();
}
} catch (error) {
// ignore
}
const match = /[?&]i_code=([^&#]+)/i.exec(target);
return match ? decodeURIComponent(match[1] || '').trim() : '';
};
const buildSafeMomoUrl = function (iCode) {
const cleaned = toText(iCode).trim();
if (!isLikelyMomoIcode(cleaned)) {
return '';
}
return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(cleaned)}`;
};
const resolveSafeUrl = function (link, href) {
const original = toText(link.dataset && link.dataset.momoOriginalUrl).trim();
const explicit = toText(link.dataset && (link.dataset.trackIcode || link.dataset.trackProductId)).trim();
const fallbackCode = explicit || getIcodeFromUrl(original) || getIcodeFromUrl(href);
if (fallbackCode) {
return buildSafeMomoUrl(fallbackCode);
}
return '#';
};
const openMomoUrl = function (link, url) {
if (!url || url === '#') {
return;
}
const target = toText(link.getAttribute('target') || '_self').toLowerCase();
if (target === '_blank') {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
if (target === '_self' || target === '' ) {
window.location.href = url;
return;
}
window.open(url, target);
};
const trackMomoLink = function (payload) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const body = {
status: 'tracked',
...payload
};
const headers = {
'Content-Type': 'application/json'
};
if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
}
fetch('/api/track_momo_link', {
method: 'POST',
headers,
body: JSON.stringify(body),
keepalive: true
}).catch(() => {});
};
const guardMomoLink = function (event) {
if (event.defaultPrevented) {
return;
}
if (event.type === 'click' && event.button && event.button !== 0) {
return;
}
if (event.type === 'auxclick' && event.button !== 1) {
return;
}
const link = event.target.closest ? event.target.closest('a') : null;
if (!link) {
return;
}
const href = toText(link.getAttribute('href') || '').trim();
if (!href || href === '#') {
return;
}
const isTrackedClass = link.classList && link.classList.contains('momo-tracked-link');
const raw = toText(link.getAttribute('href') || '').trim();
const original = toText(link.dataset && link.dataset.momoOriginalUrl).trim();
if (!isTrackedClass && !hasMomoHost(raw) && !hasMomoHost(original)) {
return;
}
const blocked = isBlockedMomoUrl(href) || isBlockedMomoUrl(original);
const payload = {
url: original || href,
page: location.pathname,
source: toText(link.dataset && link.dataset.trackSource) || 'auto-guard',
platform: toText(link.dataset && link.dataset.trackPlatform) || 'momo',
product_id: toText(link.dataset && link.dataset.trackProductId) || '',
i_code: toText(link.dataset && link.dataset.trackIcode) || '',
product_name: toText(link.dataset && link.dataset.trackProductName) || '',
label: toText(link.textContent || '').trim(),
effective_url: href,
blocked: blocked
};
if (!blocked && !isTrackedClass) {
return;
}
if (blocked) {
event.preventDefault();
const safeUrl = resolveSafeUrl(link, href);
if (safeUrl && safeUrl !== href) {
link.setAttribute('href', safeUrl);
payload.effective_url = safeUrl;
openMomoUrl(link, safeUrl);
}
trackMomoLink(payload);
return;
}
trackMomoLink(payload);
};
document.addEventListener('click', guardMomoLink, true);
document.addEventListener('auxclick', guardMomoLink, true);
})();
</script>
<!-- 共用 JavaScript -->
<script>
// CSRF Token 設定 (供 AJAX 使用)
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// 全域 fetch 設定
const fetchWithCSRF = (url, options = {}) => {
options.headers = {
...options.headers,
'X-CSRFToken': csrfToken
};
return fetch(url, options);
};
// Toast 通知函數
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toast-container') || createToastContainer();
const toastId = 'toast-' + Date.now();
const bgClass = {
'success': 'bg-success',
'error': 'bg-danger',
'warning': 'bg-warning',
'info': 'bg-primary'
}[type] || 'bg-primary';
const toastHtml = `
<div id="${toastId}" class="toast ${bgClass} text-white" role="alert">
<div class="toast-body d-flex align-items-center">
<span>${message}</span>
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastEl = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastEl, { delay: 3000 });
toast.show();
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
}
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
return container;
}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
{% block title %}{% block legacy_title %}EwoooC{% endblock %}{% endblock %}
{% block extra_css %}{% block legacy_extra_css %}{% endblock %}{% endblock %}
{% block extra_head %}{% block legacy_extra_head %}{% endblock %}{% endblock %}
{% block content %}{% block legacy_content %}{% endblock %}{% endblock %}
{% block extra_js %}{% block legacy_extra_js %}{% endblock %}{% endblock %}
{% block extra_scripts %}{% block legacy_extra_scripts %}{% endblock %}{% endblock %}

View File

@@ -0,0 +1,29 @@
<section class="legacy-bridge">
<div class="legacy-bridge__panel">
<div class="legacy-bridge__copy">
<div class="legacy-bridge__kicker">
<i class="{{ legacy_bridge_icon|default('fas fa-arrow-right') }}" aria-hidden="true"></i>
<span>{{ legacy_bridge_kicker|default('MIGRATED PAGE') }}</span>
</div>
<h1>{{ legacy_bridge_heading|default(legacy_bridge_title|default('頁面已升級')) }}</h1>
<p>{{ legacy_bridge_body|default('這個舊入口已接到新版 EwoooC V3 設計系統。') }}</p>
<div class="momo-dot-divider is-accent is-soft" aria-hidden="true"></div>
<div class="legacy-bridge__actions">
<a class="btn btn-primary" href="{{ legacy_bridge_target|default('/') }}">
<i class="fas fa-arrow-right" aria-hidden="true"></i>
<span>{{ legacy_bridge_cta|default('前往新版頁面') }}</span>
</a>
{% if legacy_bridge_secondary_target|default('') %}
<a class="btn btn-outline-primary" href="{{ legacy_bridge_secondary_target }}">
<span>{{ legacy_bridge_secondary_cta|default('相關頁面') }}</span>
</a>
{% endif %}
</div>
</div>
<aside class="legacy-bridge__status" aria-label="頁面狀態">
<span class="legacy-bridge__status-label">STATUS</span>
<strong>V3 READY</strong>
<span>{{ legacy_bridge_meta|default('Design guard active') }}</span>
</aside>
</div>
</section>

View File

@@ -1,426 +1,22 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>爬蟲管理 - MOMO Pro System</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.crawler-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
transition: box-shadow 0.3s;
}
{% set active_page = 'settings' %}
{% set legacy_bridge_title = '爬蟲管理已整併' %}
{% set legacy_bridge_kicker = 'SYSTEM OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-gear' %}
{% set legacy_bridge_heading = '爬蟲管理已移到系統管理' %}
{% set legacy_bridge_body = '舊版 crawler_management.html 已停用;爬蟲啟停、排程資訊與系統設定統一收斂到新版系統管理頁。' %}
{% set legacy_bridge_target = '/settings' %}
{% set legacy_bridge_cta = '開啟系統管理' %}
{% set legacy_bridge_secondary_target = '/logs' %}
{% set legacy_bridge_secondary_cta = '查看日誌' %}
{% set legacy_bridge_meta = 'Legacy guard / crawler_management.html' %}
{% extends "ewoooc_base.html" %}
.crawler-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
{% block title %}爬蟲管理已整併 - EwoooC{% endblock %}
.crawler-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.crawler-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-active {
background: #10b981;
color: white;
}
.status-paused {
background: #f59e0b;
color: white;
}
.crawler-info {
color: #666;
font-size: 14px;
margin: 8px 0;
}
.crawler-controls {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e5e7eb;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #10b981;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.btn-secondary {
background: #6b7280;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.btn-secondary:hover {
background: #4b5563;
}
.pause-reason {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 10px;
margin-top: 10px;
border-radius: 4px;
}
.alert {
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-success {
background: #d1fae5;
border: 1px solid #10b981;
color: #065f46;
}
.alert-error {
background: #fee2e2;
border: 1px solid #ef4444;
color: #991b1b;
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-box {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #1f2937;
}
.stat-label {
color: #6b7280;
font-size: 14px;
margin-top: 5px;
}
.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;
}
/* Fixed Navbar Compensation */
body {
padding-top: 70px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 爬蟲管理</h1>
<p>管理系統爬蟲的啟用狀態和執行頻率</p>
</div>
<div id="alert-container"></div>
<div class="stats-summary">
<div class="stat-box">
<div class="stat-number" id="enabled-count">0</div>
<div class="stat-label">啟用中</div>
</div>
<div class="stat-box">
<div class="stat-number" id="paused-count">0</div>
<div class="stat-label">已暫停</div>
</div>
<div class="stat-box">
<div class="stat-number" id="total-count">0</div>
<div class="stat-label">爬蟲總數</div>
</div>
</div>
<div id="crawlers-container">
<!-- 爬蟲卡片將通過 JavaScript 動態插入 -->
</div>
<div style="margin-top: 30px; padding: 15px; background: #f3f4f6; border-radius: 8px;">
<h3 style="margin-top: 0;">💡 使用說明</h3>
<ul style="color: #6b7280; line-height: 1.8;">
<li>切換開關可以啟用或停用爬蟲</li>
<li>停用的爬蟲程式碼和資料會保留,隨時可以重新啟用</li>
<li>變更執行頻率後,需要重啟排程器服務才會生效</li>
<li>重啟排程器:<code>sudo systemctl restart momo-scheduler</code></li>
</ul>
</div>
</div>
<script>
// 載入爬蟲配置
async function loadCrawlers() {
try {
const response = await fetch('/api/crawlers');
const result = await response.json();
if (result.status === 'success') {
renderCrawlers(result.data);
updateStats(result.data);
} else {
showAlert('載入失敗: ' + result.message, 'error');
}
} catch (error) {
showAlert('載入爬蟲配置時發生錯誤', 'error');
console.error(error);
}
}
// 渲染爬蟲卡片
function renderCrawlers(crawlers) {
const container = document.getElementById('crawlers-container');
container.innerHTML = '';
for (const [key, info] of Object.entries(crawlers)) {
const card = createCrawlerCard(key, info);
container.appendChild(card);
}
}
// 創建爬蟲卡片
function createCrawlerCard(key, info) {
const card = document.createElement('div');
card.className = 'crawler-card';
const statusClass = info.enabled ? 'status-active' : 'status-paused';
const statusText = info.enabled ? '運行中' : '已暫停';
card.innerHTML = `
<div class="crawler-header">
<div>
<div class="crawler-title">${info.name}</div>
<span class="status-badge ${statusClass}">${statusText}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" ${info.enabled ? 'checked' : ''}
onchange="toggleCrawler('${key}', this.checked)">
<span class="slider"></span>
</label>
</div>
<div class="crawler-info">
<div>📝 ${info.description || 'N/A'}</div>
<div>⏰ 執行頻率:每 ${info.schedule_hours || 'N/A'} 小時</div>
${info.lpn_code ? `<div>🔖 活動代碼:${info.lpn_code}</div>` : ''}
${info.last_active_date ? `<div>📅 最後活動:${info.last_active_date}</div>` : ''}
</div>
${!info.enabled && info.pause_reason ? `
<div class="pause-reason">
<strong>⏸️ 暫停原因:</strong>${info.pause_reason}
${info.notes ? `<br><small>${info.notes}</small>` : ''}
</div>
` : ''}
<div class="crawler-controls">
${info.enabled ? `
<button class="btn-secondary" onclick="changeSchedule('${key}', ${info.schedule_hours})">
修改頻率
</button>
` : ''}
</div>
`;
return card;
}
// 更新統計資料
function updateStats(crawlers) {
const total = Object.keys(crawlers).length;
const enabled = Object.values(crawlers).filter(c => c.enabled).length;
const paused = total - enabled;
document.getElementById('enabled-count').textContent = enabled;
document.getElementById('paused-count').textContent = paused;
document.getElementById('total-count').textContent = total;
}
// 切換爬蟲狀態
async function toggleCrawler(key, enabled) {
let reason = '';
if (!enabled) {
reason = prompt('請輸入停用原因(可選):');
}
try {
const response = await fetch(`/api/crawlers/${key}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled, reason })
});
const result = await response.json();
if (result.status === 'success') {
showAlert(result.message, 'success');
loadCrawlers(); // 重新載入
} else {
showAlert('操作失敗: ' + result.message, 'error');
loadCrawlers(); // 恢復原狀
}
} catch (error) {
showAlert('操作時發生錯誤', 'error');
console.error(error);
loadCrawlers();
}
}
// 修改執行頻率
async function changeSchedule(key, currentHours) {
const newHours = prompt(`請輸入新的執行頻率(小時)\n目前:每 ${currentHours} 小時`, currentHours);
if (newHours === null || newHours === currentHours.toString()) {
return; // 使用者取消或未變更
}
try {
const response = await fetch(`/api/crawlers/${key}/schedule`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ schedule_hours: parseInt(newHours) })
});
const result = await response.json();
if (result.status === 'success') {
showAlert(result.message + '(需重啟排程器生效)', 'success');
loadCrawlers();
} else {
showAlert('更新失敗: ' + result.message, 'error');
}
} catch (error) {
showAlert('更新時發生錯誤', 'error');
console.error(error);
}
}
// 顯示提示訊息
function showAlert(message, type) {
const container = document.getElementById('alert-container');
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
container.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 5000);
}
// 頁面載入時執行
document.addEventListener('DOMContentLoaded', loadCrawlers);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,568 +1,22 @@
<!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>MOMO 限時搶購</title>
<meta http-equiv="refresh" content="300">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding-top: 70px;
}
{% set active_page = 'edm' %}
{% set legacy_bridge_title = '活動看板已升級' %}
{% set legacy_bridge_kicker = 'MIGRATED CAMPAIGN' %}
{% set legacy_bridge_icon = 'fas fa-bullhorn' %}
{% set legacy_bridge_heading = '請使用新版活動看板' %}
{% set legacy_bridge_body = '舊版 edm_dashboard.html 已停用,正式入口現在由 edm_dashboard_v2.html 與新版響應式表格負責。' %}
{% set legacy_bridge_target = '/edm' %}
{% set legacy_bridge_cta = '開啟活動看板' %}
{% set legacy_bridge_secondary_target = '/festival' %}
{% set legacy_bridge_secondary_cta = '節慶活動' %}
{% set legacy_bridge_meta = 'Legacy guard / edm_dashboard.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}活動看板已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
/* 統一使用 dashboard 的柔和色調與樣式 */
.bg-success-soft { background-color: rgba(25, 135, 84, 0.1) !important; }
.bg-secondary-soft { background-color: rgba(108, 117, 125, 0.15) !important; }
.bg-danger-soft { background-color: rgba(220, 53, 69, 0.1) !important; }
.table-container { background: white; border-radius: .5rem; box-shadow: 0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2); padding: 1.25rem; margin-bottom: 1.5rem; }
.nav-pills .nav-link.active { background-color: #0d6efd; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.nav-pills .nav-link { color: #495057; font-weight: 500; }
.product-link { text-decoration: none; color: #212529; font-weight: 600; display: block; line-height: 1.4; margin-bottom: 4px; }
.product-link:hover { color: #0d6efd; }
.product-thumb { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; border: 1px solid #dee2e6; margin-right: 12px; }
.price-up { color: #dc3545; font-weight: bold; }
.price-down { color: #198754; font-weight: bold; }
.price-current { font-weight: 700; font-size: 1.1em; }
.price-old { text-decoration: line-through; color: #999; font-size: 0.9em; margin-right: 4px; }
.table th { font-weight: 600; color: #495057; border-bottom-width: 1px; }
.badge-status { font-size: 0.75em; vertical-align: middle; margin-left: 6px; }
.cursor-pointer { cursor: pointer; }
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
<body class="bg-body-tertiary">
{% include 'components/_navbar.html' %}
<div class="container mb-5">
<!-- V-New: 動態頁籤導覽 -->
<div class="sub-nav-tabs mb-3">
{% for page in promo_pages %}
<a href="{{ page.url }}" class="btn {% if page.id == current_promo_page %}btn-primary{% else %}btn-outline-secondary{% endif %}">
{{ page.name }}
</a>
{% endfor %}
</div>
<div class="row mb-4">
<div class="col-md-8">
{# V-New: 使用動態頁面標題 #}
<h2>🔥 {{ page_title }}</h2>
<p class="text-muted">
活動時間: {{ activity_time }} |
最後更新: {{ last_update }} |
商品總數: {{ total_edm_products }}
</p>
</div>
<div class="col-md-4 text-end">
{# V-New: 根據不同頁面顯示不同按鈕 #}
{% if current_promo_page == 'edm' %}
<button class="btn btn-outline-primary" onclick="triggerEdmTask()">🔄 手動更新</button>
<button class="btn btn-outline-success" onclick="triggerNotification()">📢 發送通知</button>
{% elif current_promo_page == 'festival' %}
<button class="btn btn-outline-primary" onclick="triggerFestivalTask()">🔄 手動更新</button>
{% endif %}
</div>
</div>
{# V-New: 根據頁面類型讀取對應的排程統計 #}
{% set task_key = 'festival_task' if current_promo_page == 'festival' else 'edm_task' %}
{% set edm_stats_list = scheduler_stats.get(task_key, []) %}
{% if edm_stats_list %}
{% set latest_run = edm_stats_list[0] %}
<div class="alert alert-info small p-2 mb-3">
<i class="fas fa-info-circle me-1"></i>
<strong>排程統計:</strong>
上次執行於 {{ latest_run.last_run }}
共記錄 {{ latest_run.get('changed_records', 0) }} 筆異動。
狀態:
{% if latest_run.status == 'Success' %}
<span class="badge bg-success">成功</span>
{% else %}
<span class="badge bg-danger" title="{{ latest_run.get('error', '未知錯誤') }}">失敗</span>
{% endif %}
</div>
{% endif %}
<!-- 時段頁籤 -->
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
{% for slot, stats in slot_stats.items() %}
{% set slot_id = slugify(slot) %}
{% set pane_id = "pills-" ~ slot_id %}
{% set tab_id = pane_id ~ "-tab" %}
{% set target_selector = "#" ~ pane_id %}
{% set is_selected = 'true' if slot == active_tab else 'false' %}
{% set active_class = 'active' if slot == active_tab else '' %}
<li class="nav-item" role="presentation">
<button class="nav-link {{ active_class }} px-3 py-2"
id="{{ tab_id }}"
data-bs-toggle="pill"
data-bs-target="{{ target_selector }}"
type="button"
role="tab"
aria-controls="{{ pane_id }}"
aria-selected="{{ is_selected }}">
{{ slot }}
{# V-New: 改用 on_shelf 數量顯示,更直觀 #}
<span class="badge bg-white text-primary rounded-pill ms-2 border">{{ stats.on_shelf }}</span>
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="pills-tabContent">
{% for slot, stats in slot_stats.items() %}
{% set slot_id = slugify(slot) %}
{% set pane_id = "pills-" ~ slot_id %}
{% set items = grouped_items.get(slot, []) %}
<div class="tab-pane fade {% if slot == active_tab %}show active{% endif %}"
id="{{ pane_id }}" role="tabpanel">
<div class="table-container">
<!-- 該時段統計 -->
<div class="d-flex align-items-center mb-3 pb-2 border-bottom">
<h5 class="mb-0 me-3">商品列表 ({{ items|length }}筆)</h5>
<div class="small">
<span class="badge bg-primary me-1">新品: {{ stats['new'] }}</span>
<span class="badge bg-danger me-1">漲價: {{ stats['up'] }}</span>
<span class="badge bg-success me-1">降價: {{ stats['down'] }}</span>
<span class="badge bg-secondary">下架: {{ stats.get('delisted_last_run', 0) }}</span>
</div>
</div>
{% set current_endpoint = 'edm.' ~ current_promo_page ~ '_dashboard' %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr class="text-nowrap">
<th style="width: 10%;">分類</th>
<th style="width: 50%;">
{% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else ('default' if current_sort == 'name' and current_order == 'asc' else 'desc') %}
<a href="{{ url_for(current_endpoint, sort_by='name' if next_order_name != 'default' else 'default', order=next_order_name) }}" class="text-decoration-none text-muted">
商品資訊 {% if current_sort == 'name' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
</a>
</th>
<th style="width: 20%;" class="text-end">
{% set next_order_price = 'asc' if current_sort == 'price' and current_order == 'desc' else ('default' if current_sort == 'price' and current_order == 'asc' else 'desc') %}
<a href="{{ url_for(current_endpoint, sort_by='price' if next_order_price != 'default' else 'default', order=next_order_price) }}" class="text-decoration-none text-muted">
價格 {% if current_sort == 'price' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
</a>
</th>
<th style="width: 20%;" class="text-center">
{% if current_promo_page == 'edm' %}
{% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else ('default' if current_sort == 'remain_qty' and current_order == 'asc' else 'desc') %}
<a href="{{ url_for(current_endpoint, sort_by='remain_qty' if next_order_qty != 'default' else 'default', order=next_order_qty) }}" class="text-decoration-none text-muted">
倒數組數 {% if current_sort == 'remain_qty' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
</a>
{% else %}
狀態
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{% if item.main_category %}
{% set badge_attr = 'style="background-color: ' ~ item.category_color ~ '; color: #333;"' %}
<span class="badge" {{ badge_attr | safe }}>{{ item.main_category }}</span>
{% else %}
<span class="badge bg-light text-muted">未分類</span>
{% endif %}
</td>
<td>
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
{% if item.image_url %}
<img src="{{ item.image_url }}" class="product-thumb" alt="商品圖" loading="lazy" referrerpolicy="no-referrer">
{% else %}
<div class="product-thumb d-flex align-items-center justify-content-center bg-light text-muted small">無圖</div>
{% endif %}
</div>
<div class="flex-grow-1">
<a href="{{ item.safe_product_url or '#' }}" target="_blank" class="product-link momo-tracked-link" title="{{ item.name }}" data-momo-original-url="{{ item.safe_product_url or '#' }}"
data-track-platform="momo"
data-track-source="edm-dashboard-table"
data-track-product-id="{{ item.i_code }}"
data-track-icode="{{ item.i_code }}"
data-track-product-name="{{ item.name|e }}">
{{ item.name }}
</a>
<div class="d-flex align-items-center flex-wrap gap-2">
<small class="text-muted fw-bold cursor-pointer" data-icode="{{ item.i_code }}" onclick="copyToClipboard(event, this.dataset.icode, this)" title="點擊複製品號">
ID: {{ item.i_code }} <i class="far fa-copy ms-1"></i>
</small>
{% if item.status_change == 'NEW' %}
<span class="badge bg-primary badge-status">NEW</span>
{% elif item.status_change == 'PRICE_DOWN' %}
<span class="badge bg-success-soft text-success badge-status">降價</span>
{% elif item.status_change == 'PRICE_UP' %}
<span class="badge bg-danger-soft text-danger badge-status">漲價</span>
{% elif item.status_change == 'DELISTED' or item.status_change == 'SLOT_END' %}
<span class="badge bg-secondary-soft text-secondary badge-status">下架</span>
{% endif %}
{% if item.discount_text %}
<span class="badge bg-danger bg-opacity-75 badge-status">{{ item.discount_text }}</span>
{% endif %}
</div>
</div>
</div>
</td>
<td class="text-end">
{% if item.previous_price and item.price and item.previous_price != item.price %}
{% set diff = item.price - item.previous_price %}
{% set percent = ((diff|abs) / item.previous_price * 100) | round | int %}
{% if diff < 0 %}
<div>
<span class="badge bg-success-soft text-success mb-1">▼ {{ (diff|abs) | number_format }} ({{ percent }}%)</span><br>
<span class="price-old text-muted">${{ item.previous_price | number_format }}</span>
<span class="price-current text-success">${{ item.price | number_format }}</span>
</div>
{% else %}
<div>
<span class="badge bg-danger-soft text-danger mb-1">▲ {{ diff | number_format }} ({{ percent }}%)</span><br>
<span class="price-old text-muted">${{ item.previous_price | number_format }}</span>
<span class="price-current text-danger">${{ item.price | number_format }}</span>
</div>
{% endif %}
{% else %}
<span class="price-current text-dark">${{ (item.price | number_format) if item.price is not none else 'N/A' }}</span>
{% endif %}
</td>
<td class="text-center">
{% if current_promo_page == 'edm' %}
{% set tooltip_content %}
<div class='text-start p-1'>
<strong>📈 當日銷售歷程:</strong><br>
{% for h in item.qty_history %}
<small>{{ h.time }}</small> &nbsp; 剩 {{ h.qty }} 組<br>
{% endfor %}
</div>
{% endset %}
<div {% if item.qty_history and item.qty_history|length > 1 %}
data-bs-toggle="tooltip"
data-bs-html="true"
title="{{ tooltip_content }}"
{% endif %}>
{% if item.remain_qty is not none %}
<span class="badge bg-warning text-dark border border-warning">🔥 {{ item.remain_qty | number_format }}組</span>
{% else %}
<span class="text-muted small">-</span>
{% endif %}
</div>
{% else %}
<span class="badge bg-light text-dark border">活動中</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// V-New: 初始化 Bootstrap Tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// Helper function to get CSRF token
function getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
function triggerEdmTask() {
if(confirm('確定要手動執行 EDM 爬蟲嗎?')) {
fetch('/api/run_edm_task', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken()
}
})
.then(r => r.json())
.then(data => alert(data.message))
.catch(e => alert('錯誤: ' + e));
}
}
function triggerNotification() {
if(confirm('確定要發送比價通知嗎?')) {
fetch('/api/trigger_edm_notification', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken()
}
})
.then(r => r.json())
.then(data => alert(data.message))
.catch(e => alert('錯誤: ' + e));
}
}
// V-New: 觸發 Festival 爬蟲
function triggerFestivalTask() {
if(confirm('確定要手動執行「1.1狂歡購物節」爬蟲嗎?')) {
fetch('/api/run_festival_task', {
method: 'POST',
headers: {
'X-CSRFToken': getCSRFToken()
}
})
.then(r => r.json())
.then(data => alert(data.message))
.catch(e => alert('錯誤: ' + e));
}
}
// V-New: 複製品號功能
function copyToClipboard(event, text, element) {
event.stopPropagation();
const showFeedback = () => {
const originalHtml = element.innerHTML;
element.innerHTML = '已複製 <i class="fas fa-check"></i>';
element.classList.remove('text-muted');
element.classList.add('text-success');
setTimeout(() => {
element.innerHTML = originalHtml;
element.classList.remove('text-success');
element.classList.add('text-muted');
}, 1500);
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(showFeedback);
} else {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showFeedback();
} catch (err) { console.error('複製失敗', err); }
document.body.removeChild(textArea);
}
}
function trackMomoLinkClick(event) {
const link = event.target.closest('.momo-tracked-link');
if (!link) {
return;
}
const href = link.getAttribute('href') || '';
const originalHref = link.dataset.momoOriginalUrl || href;
if (!href || href === '#') {
return;
}
const payload = {
url: originalHref,
page: location.pathname,
source: link.dataset.trackSource || 'unknown',
platform: link.dataset.trackPlatform || 'momo',
product_id: link.dataset.trackProductId || '',
i_code: link.dataset.trackIcode || '',
product_name: link.dataset.trackProductName || '',
label: (link.textContent || '').trim(),
effective_url: href
};
const isBlocked = isBlockedMomoUrl(href);
if (isBlocked) {
console.warn('[EDM Dashboard] 嘗試打開 MOMO 404 網址', payload);
event.preventDefault();
const fallbackUrl = link.dataset.momoFallbackUrl || getSafeMomoFallbackUrl(link);
if (fallbackUrl && fallbackUrl !== '#' && fallbackUrl !== href) {
link.dataset.momoFallbackUrl = fallbackUrl;
link.setAttribute('href', fallbackUrl);
payload.effective_url = fallbackUrl;
openMomoUrl(link, fallbackUrl);
fetch('/api/track_momo_link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify(payload)
}).catch(() => {});
return;
}
}
fetch('/api/track_momo_link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify(payload)
}).catch(() => {});
}
function getSafeMomoFallbackUrl(link) {
const iCode = (link.dataset.trackIcode || link.dataset.trackProductId || '').trim();
if (!isLikelyMomoProductCode(iCode)) {
return '#';
}
return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(iCode)}`;
}
function isBlockedMomoUrl(url) {
const lowered = (url || '').toLowerCase();
if (lowered.includes('EC404.html') || lowered.includes('ec404')) {
return true;
}
try {
const parsed = new URL(url, location.origin);
const path = (parsed.pathname || '').toLowerCase();
if (!path.includes('goodsdetail')) {
return false;
}
const code = (parsed.searchParams.get('i_code') || '').trim();
if (code) {
return !isLikelyMomoProductCode(code);
}
return !/\/goodsdetail\/[^/?#]+/i.test(path);
} catch (error) {
if (!/goodsdetail\.jsp/i.test(lowered)) {
return false;
}
const hasCode = /[?&]i_code=([^&#]+)/i.test(lowered);
if (!hasCode) {
return true;
}
const match = /[?&]i_code=([^&#]+)/i.exec(lowered);
const code = match ? (match[1] || '').trim() : '';
return !isLikelyMomoProductCode(code);
}
}
function openMomoUrl(link, url) {
if (!url || url === '#') {
return;
}
const target = (link.getAttribute('target') || '_self').toLowerCase();
if (target === '_blank') {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
if (target === '_self' || target === '') {
window.location.href = url;
return;
}
window.open(url, target);
}
document.addEventListener('click', trackMomoLinkClick);
function isLikelyMomoProductCode(value) {
const cleaned = (value || '').trim();
if (!cleaned) {
return false;
}
const lowered = cleaned.toLowerCase();
if (lowered === 'nan' || lowered === 'none' || lowered === 'null' || lowered === 'undefined') {
return false;
}
if (lowered.startsWith('momo_') || lowered.startsWith('manual_') || lowered.startsWith('pchome_')) {
return false;
}
return /^[A-Za-z0-9_-]{4,}$/.test(cleaned);
}
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,76 +1,22 @@
<!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>發送歷史 - 廠商缺貨系統</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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '發送歷史已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-clock-rotate-left' %}
{% set legacy_bridge_heading = '請使用新版發送歷史' %}
{% set legacy_bridge_body = '舊版 history.html 已停用,正式入口現在使用新版廠商缺貨工具頁與一致的營運視覺規範。' %}
{% set legacy_bridge_target = '/vendor-stockout/history' %}
{% set legacy_bridge_cta = '開啟發送歷史' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/list' %}
{% set legacy_bridge_secondary_cta = '缺貨清單' %}
{% set legacy_bridge_meta = 'Legacy guard / history.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}發送歷史已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
</style>
</head>
<body>
{% include 'components/_navbar.html' %}
<div class="container-fluid" style="padding: 2rem;">
<div class="card" style="border-radius: 16px;">
<div class="card-body">
<h2 class="mb-4">
<i class="fas fa-history text-warning me-3"></i>發送歷史
</h2>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
此頁面功能將在 Phase 6 實作 (記錄查詢、統計圖表、重試機制)
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,353 +1,22 @@
<!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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = 'Excel 匯入已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-file-import' %}
{% set legacy_bridge_heading = '請使用新版雲端匯入入口' %}
{% set legacy_bridge_body = '舊版 import.html 已停用;廠商缺貨匯入與 Google Drive 自動匯入都已接到新版 V3 shell。' %}
{% set legacy_bridge_target = '/vendor-stockout/import' %}
{% set legacy_bridge_cta = '開啟廠商匯入' %}
{% set legacy_bridge_secondary_target = '/auto_import' %}
{% set legacy_bridge_secondary_cta = '雲端匯入' %}
{% set legacy_bridge_meta = 'Legacy guard / import.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}Excel 匯入已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,241 +1,22 @@
<!DOCTYPE html>
<html lang="zh-TW">
{% set active_page = 'settings' %}
{% set legacy_bridge_title = '系統維護中' %}
{% set legacy_bridge_kicker = 'SYSTEM MAINTENANCE' %}
{% set legacy_bridge_icon = 'fas fa-screwdriver-wrench' %}
{% set legacy_bridge_heading = '正在進行系統升級' %}
{% set legacy_bridge_body = '服務正在短暫維護與優化,完成後會恢復正常入口。這個頁面已套用新版字型、色彩與點陣規範。' %}
{% set legacy_bridge_target = '/health' %}
{% set legacy_bridge_cta = '檢查健康狀態' %}
{% set legacy_bridge_secondary_target = '/' %}
{% set legacy_bridge_secondary_cta = '回首頁' %}
{% set legacy_bridge_meta = '維護模式 / Asia Taipei' %}
{% extends "ewoooc_base.html" %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系統維護中 - WOOO TECH</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4F46E5;
--secondary: #7C3AED;
--accent: #06B6D4;
--bg-glass: rgba(255, 255, 255, 0.8);
--text-main: #1F2937;
}
{% block title %}系統維護中 - EwoooC{% endblock %}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
body {
font-family: 'Outfit', sans-serif;
background: #0f172a;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* Animated Mesh Gradient Background */
.bg-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 30%, rgba(79, 70, 229, 0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 70%, rgba(6, 182, 212, 0.15) 0%, transparent 40%),
radial-gradient(circle at 50% 50%, rgba(124, 58, 237, 0.1) 0%, transparent 60%);
z-index: 1;
}
.container {
position: relative;
z-index: 10;
background: var(--bg-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 32px;
padding: 60px 40px;
max-width: 550px;
width: 90%;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo-section {
margin-bottom: 40px;
}
.logo-section img {
height: 80px;
filter: drop-shadow(0 0 15px rgba(79, 70, 229, 0.3));
}
h1 {
color: var(--text-main);
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 24px;
letter-spacing: -0.025em;
}
p {
color: #4B5563;
font-size: 1.15rem;
line-height: 1.8;
margin-bottom: 12px;
font-weight: 400;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 10px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 12px 28px;
border-radius: 9999px;
font-weight: 600;
margin-top: 32px;
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
}
50% {
transform: scale(1.02);
box-shadow: 0 15px 25px -3px rgba(79, 70, 229, 0.6);
}
100% {
transform: scale(1);
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
}
}
.footer-time {
margin-top: 40px;
color: #9CA3AF;
font-size: 0.9rem;
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding-top: 20px;
}
/* Floating elements for "AI/Tech" vibe */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(60px);
z-index: 2;
opacity: 0.4;
animation: float 10s infinite alternate;
}
.orb-1 {
width: 300px;
height: 300px;
background: var(--primary);
top: -100px;
left: -100px;
}
.orb-2 {
width: 400px;
height: 400px;
background: var(--accent);
bottom: -150px;
right: -150px;
animation-delay: -5s;
}
@keyframes float {
from {
transform: translate(0, 0);
}
to {
transform: translate(50px, 50px);
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.8rem;
}
p {
font-size: 1rem;
}
.container {
padding: 40px 24px;
}
}
</style>
</head>
<body>
<div class="bg-gradient"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="container">
<div class="logo-section">
<img src="/static/images/logo.png" alt="WOOO TECH"
onerror="this.src='https://cdn-icons-png.flaticon.com/512/3252/3252971.png';">
</div>
<h1>正在進行系統升級</h1>
<p>為了提供更穩定的網站服務</p>
<p>系統正在進行必要的維護與優化</p>
<p>造成不便,敬請見諒</p>
<div class="status-badge">
<i class="fas fa-tools"></i>
<span>維護升級中</span>
</div>
<div class="footer-time" id="current-time"></div>
</div>
<script>
function updateTime() {
try {
const now = new Date();
const timeStr = now.toLocaleString('zh-TW', {
timeZone: 'Asia/Taipei',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
document.getElementById('current-time').textContent = '台北標準時間:' + timeStr;
} catch (e) {
console.error(e);
}
}
updateTime();
setInterval(updateTime, 1000);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,76 +1,22 @@
<!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>郵件發送 - 廠商缺貨系統</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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '郵件發送已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-paper-plane' %}
{% set legacy_bridge_heading = '請使用新版郵件發送' %}
{% set legacy_bridge_body = '舊版 send_email.html 已停用,正式入口現在由新版廠商缺貨工具頁負責,包含響應式排版與新版字型規範。' %}
{% set legacy_bridge_target = '/vendor-stockout/send-email' %}
{% set legacy_bridge_cta = '開啟郵件發送' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/vendor-management' %}
{% set legacy_bridge_secondary_cta = '廠商管理' %}
{% set legacy_bridge_meta = 'Legacy guard / send_email.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}郵件發送已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
</style>
</head>
<body>
{% include 'components/_navbar.html' %}
<div class="container-fluid" style="padding: 2rem;">
<div class="card" style="border-radius: 16px;">
<div class="card-body">
<h2 class="mb-4">
<i class="fas fa-paper-plane text-danger me-3"></i>郵件發送
</h2>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
此頁面功能將在 Phase 6 實作 (依廠商分組、郵件預覽、附件產生、SMTP 發送)
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -0,0 +1,118 @@
.legacy-bridge {
min-height: calc(100vh - var(--momo-topbar-height) - 48px);
display: grid;
align-items: center;
color: var(--momo-text-primary);
}
.legacy-bridge__panel {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(180px, 260px);
gap: var(--momo-space-5);
padding: clamp(var(--momo-space-5), 4vw, var(--momo-space-7));
border: 1px solid var(--momo-border-strong);
border-radius: var(--momo-radius-lg);
background:
radial-gradient(circle at 10px 10px, rgba(42, 37, 32, 0.12) 1px, transparent 1.4px) 0 0 / 16px 16px,
var(--momo-bg-surface);
}
.legacy-bridge__kicker {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: var(--momo-space-3);
color: var(--momo-page-accent-dark);
font-family: var(--momo-font-mono);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.legacy-bridge h1 {
max-width: 820px;
margin: 0;
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 800;
line-height: 1.12;
letter-spacing: 0;
}
.legacy-bridge p {
max-width: 760px;
margin: var(--momo-space-4) 0;
color: var(--momo-text-secondary);
font-size: clamp(1rem, 1.6vw, 1.16rem);
line-height: 1.8;
}
.legacy-bridge__actions {
display: flex;
flex-wrap: wrap;
gap: var(--momo-space-3);
margin-top: var(--momo-space-5);
}
.legacy-bridge__actions .btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding-inline: var(--momo-space-4);
}
.legacy-bridge__status {
align-self: stretch;
display: grid;
align-content: end;
gap: var(--momo-space-2);
min-height: 180px;
padding: var(--momo-space-4);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
background: var(--momo-bg-elevated);
}
.legacy-bridge__status-label {
color: var(--momo-text-tertiary);
font-family: var(--momo-font-mono);
font-size: var(--momo-text-label-tiny);
font-weight: 800;
letter-spacing: 0.08em;
}
.legacy-bridge__status strong {
color: var(--momo-text-primary);
font-family: var(--momo-font-mono);
font-size: var(--momo-text-headline);
font-weight: 800;
letter-spacing: 0;
}
.legacy-bridge__status span:last-child {
color: var(--momo-text-secondary);
font-size: var(--momo-text-body-sm);
}
@media (max-width: 760px) {
.legacy-bridge {
min-height: 0;
align-items: start;
}
.legacy-bridge__panel {
grid-template-columns: 1fr;
padding: var(--momo-space-5);
}
.legacy-bridge__status {
min-height: 120px;
}
.legacy-bridge__actions .btn {
width: 100%;
}
}

View File

@@ -1,128 +1,22 @@
<!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>發送歷史 - 廠商缺貨系統</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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '發送歷史已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-clock-rotate-left' %}
{% set legacy_bridge_heading = '請使用新版發送歷史' %}
{% set legacy_bridge_body = '舊版 web vendor history 已停用,正式入口已接到新版營運工具頁與一致的響應式佈局。' %}
{% set legacy_bridge_target = '/vendor-stockout/history' %}
{% set legacy_bridge_cta = '開啟發送歷史' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/send-email' %}
{% set legacy_bridge_secondary_cta = '郵件發送' %}
{% set legacy_bridge_meta = 'Legacy guard / vendor_stockout/history.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}發送歷史已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
<body>
<!-- 導航列 -->
{% include 'components/_navbar.html' %}
<div class="container-fluid" style="padding: 2rem;">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="fas fa-history text-warning me-3"></i>發送歷史
</h2>
<a href="/vendor-stockout" class="btn btn-outline-secondary">
<i class="fas fa-home me-2"></i>返回主頁
</a>
</div>
<div class="card" style="border-radius: 16px;">
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
此頁面功能將在 Phase 6 實作 (記錄查詢、統計圖表、重試機制)
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 更新導航列時鐘
function updateNavTime() {
const now = new Date();
const elem = document.getElementById('nav-time');
if (elem) {
elem.textContent = now.toLocaleString('zh-TW', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
updateNavTime();
setInterval(updateNavTime, 1000);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,397 +1,22 @@
<!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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '缺貨匯入已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-file-arrow-up' %}
{% set legacy_bridge_heading = '請使用新版缺貨匯入' %}
{% set legacy_bridge_body = '舊版 web vendor import 已停用,正式匯入頁已接到新版上傳區、表單與響應式規範。' %}
{% set legacy_bridge_target = '/vendor-stockout/import' %}
{% set legacy_bridge_cta = '開啟缺貨匯入' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/list' %}
{% set legacy_bridge_secondary_cta = '缺貨清單' %}
{% set legacy_bridge_meta = 'Legacy guard / vendor_stockout/import.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}缺貨匯入已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
.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;
}
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
<body>
<!-- 導航列 -->
{% include 'components/_navbar.html' %}
<div class="container" style="padding: 2rem; max-width: 900px;">
<!-- 頁面標題 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0">
<i class="fas fa-file-excel text-primary me-3"></i>Excel 匯入
</h2>
<p class="text-muted mb-0 mt-2">上傳包含缺貨商品資料的 Excel 檔案,系統會自動檢測並匯入</p>
</div>
<a href="/vendor-stockout" class="btn btn-outline-secondary">
<i class="fas fa-home me-2"></i>返回主頁
</a>
</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>
<script>
// 更新導航列時鐘
function updateNavTime() {
const now = new Date();
const elem = document.getElementById('nav-time');
if (elem) {
elem.textContent = now.toLocaleString('zh-TW', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
updateNavTime();
setInterval(updateNavTime, 1000);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

View File

@@ -1,319 +1,22 @@
<!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>廠商缺貨通知系統 - WOOO TECH</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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '廠商缺貨首頁已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-box-open' %}
{% set legacy_bridge_heading = '請使用新版廠商缺貨首頁' %}
{% set legacy_bridge_body = '這個 web/templates 舊入口已停用,正式頁面已接到新版營運視覺、響應式卡片與 V3 shell。' %}
{% set legacy_bridge_target = '/vendor-stockout' %}
{% set legacy_bridge_cta = '開啟廠商缺貨' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/list' %}
{% set legacy_bridge_secondary_cta = '缺貨清單' %}
{% set legacy_bridge_meta = 'Legacy guard / vendor_stockout/index.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}廠商缺貨首頁已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
.function-card {
border: none;
border-radius: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
height: 100%;
background: #fff;
}
.function-card:hover {
transform: translateY(-8px);
box-shadow: 0 16px 32px rgba(0,0,0,0.18);
}
.function-card .card-body {
padding: 2rem;
text-align: center;
padding-top: 70px;
}
.function-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
display: inline-block;
transition: transform 0.3s ease;
}
.function-card:hover .function-icon {
transform: scale(1.1) rotate(5deg);
}
.function-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: #2c3e50;
}
.function-description {
color: #6c757d;
font-size: 0.95rem;
margin-bottom: 1.5rem;
}
.btn-function {
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.3s ease;
}
.card-import { border-top: 4px solid #3498db; }
.card-import .function-icon { color: #3498db; }
.card-import .btn-function { background: #3498db; color: white; border: none; }
.card-import .btn-function:hover { background: #2980b9; transform: scale(1.05); }
.card-list { border-top: 4px solid #2ecc71; }
.card-list .function-icon { color: #2ecc71; }
.card-list .btn-function { background: #2ecc71; color: white; border: none; }
.card-list .btn-function:hover { background: #27ae60; transform: scale(1.05); }
.card-vendor { border-top: 4px solid #9b59b6; }
.card-vendor .function-icon { color: #9b59b6; }
.card-vendor .btn-function { background: #9b59b6; color: white; border: none; }
.card-vendor .btn-function:hover { background: #8e44ad; transform: scale(1.05); }
.card-history { border-top: 4px solid #f39c12; }
.card-history .function-icon { color: #f39c12; }
.card-history .btn-function { background: #f39c12; color: white; border: none; }
.card-history .btn-function:hover { background: #e67e22; transform: scale(1.05); }
.page-header {
background: white;
padding: 2rem;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2.5rem;
font-weight: 800;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.page-header p {
color: #6c757d;
font-size: 1.1rem;
margin-bottom: 0;
}
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
/* 使用者下拉選單 */
.user-dropdown .dropdown-toggle::after {
display: none;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
</style>
</head>
<body>
<!-- 導航列 (使用共用組件) -->
{% include 'components/_navbar.html' %}
<!-- 主要內容 -->
<div class="container-fluid" style="padding: 2rem;">
<!-- 頁面標題 -->
<div class="page-header">
<h1><i class="fas fa-box-open me-3"></i>廠商缺貨通知自動化系統</h1>
<p>管理缺貨商品資料、廠商聯絡資訊,並自動發送缺貨通知郵件</p>
</div>
<!-- 功能卡片 -->
<div class="row g-4 justify-content-center">
<!-- 缺貨清單 -->
<div class="col-md-6 col-lg-4">
<div class="card function-card card-list">
<div class="card-body">
<div class="function-icon">
<i class="fas fa-list-ul"></i>
</div>
<h3 class="function-title">缺貨清單</h3>
<p class="function-description">
查看、編輯、篩選已匯入的缺貨商品,支援批次操作、發送郵件與排序
</p>
<a href="/vendor-stockout/list" class="btn btn-function">
<i class="fas fa-search me-2"></i>查看清單
</a>
</div>
</div>
</div>
<!-- 廠商管理 -->
<div class="col-md-6 col-lg-4">
<div class="card function-card card-vendor">
<div class="card-body">
<div class="function-icon">
<i class="fas fa-building"></i>
</div>
<h3 class="function-title">廠商管理</h3>
<p class="function-description">
管理廠商基本資料與聯絡郵件,支援多個郵件地址設定
</p>
<a href="/vendor-stockout/vendor-management" class="btn btn-function">
<i class="fas fa-users-cog me-2"></i>管理廠商
</a>
</div>
</div>
</div>
<!-- 發送歷史 -->
<div class="col-md-6 col-lg-4">
<div class="card function-card card-history">
<div class="card-body">
<div class="function-icon">
<i class="fas fa-history"></i>
</div>
<h3 class="function-title">發送歷史</h3>
<p class="function-description">
查看郵件發送記錄、成功率統計,以及失敗項目的錯誤訊息
</p>
<a href="/vendor-stockout/history" class="btn btn-function">
<i class="fas fa-clock me-2"></i>查看記錄
</a>
</div>
</div>
</div>
</div>
<!-- 系統說明 -->
<div class="card mt-4" style="border-radius: 16px;">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-info-circle me-2 text-info"></i>系統使用說明
</h5>
<div class="row mt-3">
<div class="col-md-6">
<h6><i class="fas fa-arrow-right text-primary me-2"></i>基本流程</h6>
<ol class="small text-muted">
<li>下載 Excel 範本,填入缺貨商品資料</li>
<li>在「缺貨清單」使用 Excel 匯入功能上傳檔案</li>
<li>在「缺貨清單」中檢視並編輯資料</li>
<li>在「廠商管理」中設定廠商郵件</li>
<li>在「缺貨清單」勾選商品後發送郵件通知廠商</li>
<li>在「發送歷史」中查看執行結果</li>
</ol>
</div>
<div class="col-md-6">
<h6><i class="fas fa-arrow-right text-success me-2"></i>功能特色</h6>
<ul class="small text-muted">
<li>自動偵測重複資料並標記</li>
<li>支援一次匯入多筆缺貨記錄</li>
<li>依廠商分組自動產生郵件附件</li>
<li>郵件發送前可預覽內容</li>
<li>完整的操作記錄與錯誤追蹤</li>
<li>支援失敗郵件重新發送</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 更新導航列時鐘
function updateNavTime() {
const now = new Date();
const elem = document.getElementById('nav-time');
if (elem) {
elem.textContent = now.toLocaleString('zh-TW', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
updateNavTime();
setInterval(updateNavTime, 1000);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,509 +1,22 @@
<!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>郵件發送記錄 - 廠商缺貨系統</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;
}
{% set active_page = 'vendor_stockout' %}
{% set legacy_bridge_title = '郵件發送已升級' %}
{% set legacy_bridge_kicker = 'VENDOR OPERATIONS' %}
{% set legacy_bridge_icon = 'fas fa-paper-plane' %}
{% set legacy_bridge_heading = '請使用新版郵件發送' %}
{% set legacy_bridge_body = '舊版 web vendor send_email 已停用,正式入口已使用新版廠商工具頁與一致的按鈕、字型、色彩。' %}
{% set legacy_bridge_target = '/vendor-stockout/send-email' %}
{% set legacy_bridge_cta = '開啟郵件發送' %}
{% set legacy_bridge_secondary_target = '/vendor-stockout/history' %}
{% set legacy_bridge_secondary_cta = '發送歷史' %}
{% set legacy_bridge_meta = 'Legacy guard / vendor_stockout/send_email.html' %}
{% extends "ewoooc_base.html" %}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
{% block title %}郵件發送已升級 - EwoooC{% endblock %}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-legacy-bridge.css') }}">
{% endblock %}
.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;
}
.stats-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-sent { background: #d4edda; color: #155724; }
.status-failed { background: #f8d7da; color: #721c24; }
.status-pending { background: #fff3cd; color: #856404; }
.table-container {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
@media (max-width: 760px) {
.table-container {
padding: 12px;
}
.table-container .table-responsive {
overflow-x: visible;
}
.table-container table,
.table-container tbody,
.table-container tr,
.table-container td {
display: block;
width: 100%;
}
.table-container table {
min-width: 0;
border-collapse: separate;
border-spacing: 0;
}
.table-container thead {
display: none;
}
.table-container tbody {
display: grid;
gap: 10px;
}
.table-container tbody tr {
overflow: hidden;
border: 1px solid #e5dccd;
border-radius: 8px;
background: #fffaf1;
}
.table-container tbody td {
display: grid;
grid-template-columns: 5.8rem minmax(0, 1fr);
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid #e5dccd;
text-align: left !important;
overflow-wrap: anywhere;
}
.table-container tbody td:last-child {
border-bottom: 0;
}
.table-container tbody td::before {
color: #8b8174;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
font-weight: 800;
}
.table-container tbody td:nth-child(1)::before { content: "ID"; }
.table-container tbody td:nth-child(2)::before { content: "批次"; }
.table-container tbody td:nth-child(3)::before { content: "廠商"; }
.table-container tbody td:nth-child(4)::before { content: "收件"; }
.table-container tbody td:nth-child(5)::before { content: "主旨"; }
.table-container tbody td:nth-child(6)::before { content: "商品"; }
.table-container tbody td:nth-child(7)::before { content: "狀態"; }
.table-container tbody td:nth-child(8)::before { content: "時間"; }
.table-container tbody td[colspan] {
display: block;
}
.table-container tbody td[colspan]::before {
content: none;
}
}
</style>
</head>
<body>
<!-- 導航列 -->
{% include 'components/_navbar.html' %}
<div class="container-fluid" style="padding: 2rem;">
<!-- 標題 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="fas fa-paper-plane text-primary me-3"></i>郵件發送記錄
</h2>
<a href="/vendor-stockout" class="btn btn-outline-secondary">
<i class="fas fa-home me-2"></i>返回主頁
</a>
</div>
<!-- 統計卡片 -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">總發送數</div>
<h3 class="mb-0" id="statTotal">0</h3>
</div>
<div class="fs-1 text-primary">
<i class="fas fa-envelope"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">成功</div>
<h3 class="mb-0 text-success" id="statSuccess">0</h3>
</div>
<div class="fs-1 text-success">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">失敗</div>
<h3 class="mb-0 text-danger" id="statFailed">0</h3>
</div>
<div class="fs-1 text-danger">
<i class="fas fa-times-circle"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">成功率</div>
<h3 class="mb-0 text-info" id="statRate">0%</h3>
</div>
<div class="fs-1 text-info">
<i class="fas fa-chart-line"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 篩選區 -->
<div class="card mb-3" style="border-radius: 12px;">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label small">狀態</label>
<select class="form-select" id="filterStatus">
<option value="">全部</option>
<option value="sent">成功</option>
<option value="failed">失敗</option>
<option value="pending">待發送</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small">廠商代碼</label>
<input type="text" class="form-control" id="filterVendor" placeholder="輸入廠商代碼">
</div>
<div class="col-md-2">
<label class="form-label small">開始日期</label>
<input type="date" class="form-control" id="filterDateFrom">
</div>
<div class="col-md-2">
<label class="form-label small">結束日期</label>
<input type="date" class="form-control" id="filterDateTo">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary w-100" onclick="loadData()">
<i class="fas fa-search me-2"></i>查詢
</button>
</div>
</div>
</div>
</div>
<!-- 資料表格 -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>批次編號</th>
<th>廠商</th>
<th>收件者</th>
<th>主旨</th>
<th>商品數</th>
<th>狀態</th>
<th>發送時間</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="8" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>載入中...
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分頁 -->
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="text-muted">
<span id="totalCount">0</span> 筆記錄
</div>
<nav>
<ul class="pagination mb-0" id="pagination"></ul>
</nav>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentPage = 1;
const pageSize = 20;
// 載入統計資料
async function loadStats() {
try {
const response = await fetch('/vendor-stockout/api/email/stats');
const result = await response.json();
if (result.success) {
const data = result.data;
document.getElementById('statTotal').textContent = data.total_sent;
document.getElementById('statSuccess').textContent = data.success_count;
document.getElementById('statFailed').textContent = data.failed_count;
document.getElementById('statRate').textContent = data.success_rate + '%';
}
} catch (error) {
console.error('載入統計資料失敗:', error);
}
}
// 載入郵件記錄
async function loadData(page = 1) {
currentPage = page;
const status = document.getElementById('filterStatus').value;
const vendor = document.getElementById('filterVendor').value;
const dateFrom = document.getElementById('filterDateFrom').value;
const dateTo = document.getElementById('filterDateTo').value;
const params = new URLSearchParams({
page: page,
page_size: pageSize
});
if (status) params.append('status', status);
if (vendor) params.append('vendor_code', vendor);
if (dateFrom) params.append('date_from', dateFrom);
if (dateTo) params.append('date_to', dateTo);
try {
const response = await fetch(`/vendor-stockout/api/email/logs?${params}`);
const result = await response.json();
if (result.success) {
renderTable(result.data);
renderPagination(result.total, page);
document.getElementById('totalCount').textContent = result.total;
} else {
alert('查詢失敗: ' + result.message);
}
} catch (error) {
console.error('載入資料失敗:', error);
alert('載入資料失敗: ' + error.message);
}
}
// 渲染表格
function renderTable(data) {
const tbody = document.getElementById('tableBody');
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted">
<i class="fas fa-inbox me-2"></i>沒有記錄
</td>
</tr>
`;
return;
}
tbody.innerHTML = data.map(item => {
const statusClass = item.status === 'sent' ? 'status-sent' :
item.status === 'failed' ? 'status-failed' : 'status-pending';
const statusText = item.status === 'sent' ? '成功' :
item.status === 'failed' ? '失敗' : '待發送';
return `
<tr>
<td>${item.id}</td>
<td><small>${item.batch_id}</small></td>
<td>
<div><strong>${item.vendor_name}</strong></div>
<small class="text-muted">${item.vendor_code}</small>
</td>
<td><small>${item.recipient_email}</small></td>
<td style="max-width: 300px;">
<small>${item.subject}</small>
${item.error_message ? `<div class="text-danger small mt-1">❌ ${item.error_message}</div>` : ''}
</td>
<td>${item.product_count}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td><small>${item.sent_at || item.created_at}</small></td>
</tr>
`;
}).join('');
}
// 渲染分頁
function renderPagination(total, current) {
const totalPages = Math.ceil(total / pageSize);
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
// 上一頁
html += `
<li class="page-item ${current === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadData(${current - 1}); return false;">上一頁</a>
</li>
`;
// 頁碼
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= current - 2 && i <= current + 2)) {
html += `
<li class="page-item ${i === current ? 'active' : ''}">
<a class="page-link" href="#" onclick="loadData(${i}); return false;">${i}</a>
</li>
`;
} else if (i === current - 3 || i === current + 3) {
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
}
// 下一頁
html += `
<li class="page-item ${current === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadData(${current + 1}); return false;">下一頁</a>
</li>
`;
pagination.innerHTML = html;
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadStats();
loadData(1);
// 每 30 秒自動重新整理統計資料
setInterval(loadStats, 30000);
});
</script>
<script>
// 更新導航列時鐘
function updateNavTime() {
const now = new Date();
const elem = document.getElementById('nav-time');
if (elem) {
elem.textContent = now.toLocaleString('zh-TW', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
updateNavTime();
setInterval(updateNavTime, 1000);
</script>
</body>
</html>
{% block content %}
{% include 'components/_legacy_bridge_panel.html' %}
{% endblock %}

File diff suppressed because it is too large Load Diff