This commit is contained in:
6
app.py
6
app.py
@@ -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,
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
29
templates/components/_legacy_bridge_panel.html
Normal file
29
templates/components/_legacy_bridge_panel.html
Normal 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>
|
||||
@@ -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
@@ -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> 剩 {{ 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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
118
web/static/css/page-legacy-bridge.css
Normal file
118
web/static/css/page-legacy-bridge.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user