refactor(templates): 統一模板目錄並移除 fallback loader

ADR-017 Phase 3f-4:根目錄模板搬入 templates/,補 trends/login_history,移除 ChoiceLoader 根目錄 fallback,搬移 components,刪除 web/templates 下的空檔/死檔與 compose 舊模板 mount。
This commit is contained in:
OoO
2026-04-29 21:44:19 +08:00
parent 9528d6c23e
commit 53edcc0077
30 changed files with 227 additions and 1662 deletions

View File

@@ -1,197 +0,0 @@
<!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://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background: #f8f9fa;
color: #333;
margin: 0;
padding: 40px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 40px;
color: #1a1a1a;
}
.section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 1.5rem;
margin-bottom: 20px;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
align-items: end;
}
.asset-item {
text-align: center;
}
.preview-box {
background: #fff;
border: 1px solid #eee;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
/* 棋盤格背景以顯示透明度 */
background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.preview-box img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.format-links a {
display: inline-block;
margin: 5px;
text-decoration: none;
color: #4F46E5;
font-size: 0.9rem;
border: 1px solid #4F46E5;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.format-links a:hover {
background: #4F46E5;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>WOOO TECH 品牌資產庫</h1>
<div class="section">
<h2 class="section-title">1. 主品牌標誌 (Main Logo)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Main_Logo.svg" alt="Main Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Main_Logo.jpg" alt="Main Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
<!-- 其他格式僅提供下載 -->
<div class="asset-item">
<div style="padding: 20px;">
<strong>其他格式</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Main_Logo.eps" download>EPS</a>
<a href="/static/exports/WOOO_Main_Logo.pdf" download>PDF/AI</a>
<a href="/static/exports/WOOO_Main_Logo.tiff" download>TIFF</a>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">2. 玻璃質感版 (Glass Version)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Glass_Logo.svg" alt="Glass Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Glass_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Glass_Logo.jpg" alt="Glass Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Glass_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">3. 能量流動版 (Gradient Version)</h2>
<div class="asset-grid">
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Gradient_Logo.svg" alt="Gradient Logo">
</div>
<div>
<strong>SVG (向量)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Gradient_Logo.svg" download>下載 SVG</a>
</div>
</div>
</div>
<div class="asset-item">
<div class="preview-box">
<img src="/static/exports/WOOO_Gradient_Logo.jpg" alt="Gradient Logo JPG">
</div>
<div>
<strong>JPG (白底)</strong><br>
<div class="format-links">
<a href="/static/exports/WOOO_Gradient_Logo.jpg" download>下載 JPG</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,337 +0,0 @@
{#
WOOO 品牌載入動畫組件
使用方式:
1. 在頁面中 include 此組件:
{% include 'components/_loading.html' %}
2. 在 JavaScript 中控制顯示/隱藏:
showLoading('正在載入數據...') // 顯示
hideLoading() // 隱藏
3. 也可直接操作 DOM
document.getElementById('loadingOverlay').style.display = 'flex'; // 顯示
document.getElementById('loadingOverlay').style.display = 'none'; // 隱藏
#}
<!-- WOOO Loading Overlay -->
<div id="loadingOverlay" class="wooo-loading-overlay">
<div class="loading-logo-container">
<!-- 脈衝光暈 -->
<div class="logo-pulse"></div>
<!-- 外層旋轉光環 -->
<div class="logo-ring"></div>
<!-- 內層反向旋轉光環 -->
<div class="logo-ring-inner"></div>
<!-- 環繞粒子 -->
<div class="orbit-particles">
<div class="orbit-particle"></div>
<div class="orbit-particle"></div>
<div class="orbit-particle"></div>
<div class="orbit-particle"></div>
</div>
<!-- 閃爍星星 -->
<div class="sparkles">
<div class="sparkle"></div>
<div class="sparkle"></div>
<div class="sparkle"></div>
<div class="sparkle"></div>
<div class="sparkle"></div>
<div class="sparkle"></div>
</div>
<!-- LOGO -->
<div class="loading-logo">WOOO</div>
</div>
<div class="loading-text" id="loadingText">
<i class="fas fa-spinner fa-spin me-2"></i>正在載入數據...
</div>
<div class="loading-progress">
<div class="loading-progress-bar"></div>
</div>
<div class="loading-hint" id="loadingHint">
大量資料可能需要較長時間,請稍候
</div>
</div>
<style>
/* ============================================
WOOO 品牌載入動畫樣式
============================================ */
.wooo-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(240, 245, 255, 0.95) 100%);
z-index: 9999;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 25px;
backdrop-filter: blur(8px);
}
/* LOGO 動畫容器 */
.wooo-loading-overlay .loading-logo-container {
position: relative;
width: 160px;
height: 160px;
display: flex;
justify-content: center;
align-items: center;
}
.wooo-loading-overlay .loading-logo {
z-index: 3;
animation: wooo-cloud-float 3s ease-in-out infinite;
font-size: 2.5rem;
font-weight: 800;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 4px;
filter: drop-shadow(0 4px 15px rgba(79, 70, 229, 0.4));
}
/* 雲端飄動動畫 */
@keyframes wooo-cloud-float {
0%, 100% {
transform: translateY(0) translateX(0) scale(1);
}
25% {
transform: translateY(-15px) translateX(5px) scale(1.02);
}
50% {
transform: translateY(-8px) translateX(-3px) scale(1);
}
75% {
transform: translateY(-20px) translateX(3px) scale(1.01);
}
}
/* 外層旋轉光環 */
.wooo-loading-overlay .logo-ring {
position: absolute;
width: 150px;
height: 150px;
border: 4px solid transparent;
border-top-color: #4F46E5;
border-right-color: #7C3AED;
border-radius: 50%;
animation: wooo-ring-spin 2s linear infinite;
}
/* 內層反向旋轉光環 */
.wooo-loading-overlay .logo-ring-inner {
position: absolute;
width: 120px;
height: 120px;
border: 3px solid transparent;
border-bottom-color: #EC4899;
border-left-color: #F59E0B;
border-radius: 50%;
animation: wooo-ring-spin-reverse 1.5s linear infinite;
}
/* 脈衝光暈 */
.wooo-loading-overlay .logo-pulse {
position: absolute;
width: 100px;
height: 100px;
background: radial-gradient(circle, rgba(79, 70, 229, 0.3) 0%, transparent 70%);
border-radius: 50%;
animation: wooo-pulse-expand 2s ease-out infinite;
}
@keyframes wooo-ring-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes wooo-ring-spin-reverse {
0% { transform: rotate(0deg); }
100% { transform: rotate(-360deg); }
}
@keyframes wooo-pulse-expand {
0% {
transform: scale(0.8);
opacity: 1;
}
100% {
transform: scale(1.8);
opacity: 0;
}
}
/* 環繞粒子 */
.wooo-loading-overlay .orbit-particles {
position: absolute;
width: 160px;
height: 160px;
animation: wooo-orbit-rotate 4s linear infinite;
}
.wooo-loading-overlay .orbit-particle {
position: absolute;
width: 8px;
height: 8px;
background: linear-gradient(135deg, #4F46E5, #7C3AED);
border-radius: 50%;
box-shadow: 0 0 10px rgba(79, 70, 229, 0.8);
}
.wooo-loading-overlay .orbit-particle:nth-child(1) {
top: 0;
left: 50%;
transform: translateX(-50%);
}
.wooo-loading-overlay .orbit-particle:nth-child(2) {
top: 50%;
right: 0;
transform: translateY(-50%);
}
.wooo-loading-overlay .orbit-particle:nth-child(3) {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.wooo-loading-overlay .orbit-particle:nth-child(4) {
top: 50%;
left: 0;
transform: translateY(-50%);
}
@keyframes wooo-orbit-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 閃爍星星 */
.wooo-loading-overlay .sparkles {
position: absolute;
width: 180px;
height: 180px;
}
.wooo-loading-overlay .sparkle {
position: absolute;
width: 4px;
height: 4px;
background: #FCD34D;
border-radius: 50%;
animation: wooo-sparkle-twinkle 1.5s ease-in-out infinite;
}
.wooo-loading-overlay .sparkle:nth-child(1) { top: 10%; left: 20%; animation-delay: 0s; }
.wooo-loading-overlay .sparkle:nth-child(2) { top: 5%; right: 25%; animation-delay: 0.3s; }
.wooo-loading-overlay .sparkle:nth-child(3) { bottom: 15%; right: 15%; animation-delay: 0.6s; }
.wooo-loading-overlay .sparkle:nth-child(4) { bottom: 10%; left: 25%; animation-delay: 0.9s; }
.wooo-loading-overlay .sparkle:nth-child(5) { top: 30%; left: 5%; animation-delay: 1.2s; }
.wooo-loading-overlay .sparkle:nth-child(6) { top: 25%; right: 5%; animation-delay: 0.4s; }
@keyframes wooo-sparkle-twinkle {
0%, 100% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1);
opacity: 1;
}
}
/* 載入文字 */
.wooo-loading-overlay .loading-text {
font-size: 1.2rem;
color: #4F46E5;
font-weight: 600;
text-align: center;
letter-spacing: 0.5px;
}
.wooo-loading-overlay .loading-hint {
font-size: 0.85rem;
color: #6b7280;
text-align: center;
max-width: 300px;
}
/* 進度條 */
.wooo-loading-overlay .loading-progress {
width: 200px;
height: 4px;
background: rgba(79, 70, 229, 0.2);
border-radius: 2px;
overflow: hidden;
}
.wooo-loading-overlay .loading-progress-bar {
height: 100%;
background: linear-gradient(90deg, #4F46E5, #7C3AED, #4F46E5);
background-size: 200% 100%;
animation: wooo-progress-flow 1.5s linear infinite;
width: 100%;
}
@keyframes wooo-progress-flow {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
</style>
<script>
/**
* WOOO 載入動畫控制函數
*/
// 顯示載入動畫
function showLoading(text, hint) {
const overlay = document.getElementById('loadingOverlay');
const loadingText = document.getElementById('loadingText');
const loadingHint = document.getElementById('loadingHint');
if (text && loadingText) {
loadingText.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + text;
}
if (hint && loadingHint) {
loadingHint.textContent = hint;
}
if (overlay) {
overlay.style.display = 'flex';
}
}
// 隱藏載入動畫
function hideLoading() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
// 更新載入文字
function updateLoadingText(text) {
const loadingText = document.getElementById('loadingText');
if (loadingText && text) {
loadingText.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + text;
}
}
// 更新載入提示
function updateLoadingHint(hint) {
const loadingHint = document.getElementById('loadingHint');
if (loadingHint && hint) {
loadingHint.textContent = hint;
}
}
</script>

View File

@@ -1,189 +0,0 @@
{#
導航列組件
注意:目前已停用登入驗證,所有選單項目對所有人可見。
使用方式:
{% include 'components/_navbar.html' %}
#}
<nav class="navbar navbar-expand-xl navbar-dark bg-custom-dark fixed-top shadow-sm">
<div class="container">
<!-- 品牌 Logo -->
<a class="navbar-brand d-flex align-items-center" href="/">
<span class="d-none d-sm-inline fw-bold" style="letter-spacing: 2px;">WOOO</span>
</a>
<!-- 手機版選單按鈕 -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<!-- 商品看板 - 所有人可見 -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'dashboard' %}active{% endif %}" href="/">
<i class="fas fa-chart-line me-1"></i>商品看板
</a>
</li>
<!-- 活動看板 - 所有人可見 -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'edm' %}active{% endif %}" href="/edm">
<i class="fas fa-bullhorn me-1"></i>活動看板
</a>
</li>
<!-- 分析報表 -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if active_page in ['sales', 'daily_sales', 'monthly', 'growth'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-chart-bar me-1"></i>分析報表
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/sales_analysis">
<i class="fas fa-chart-bar me-2"></i>業績分析
</a>
</li>
<li>
<a class="dropdown-item" href="/daily_sales">
<i class="fas fa-calendar-day me-2"></i>當日業績
</a>
</li>
<li>
<a class="dropdown-item" href="/growth_analysis">
<i class="fas fa-chart-line me-2"></i>成長分析
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="/monthly_summary_analysis">
<i class="fas fa-table me-2"></i>月份總表數據分析
</a>
</li>
{% if metabase_url %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ metabase_url }}" target="_blank">
<i class="fas fa-chart-pie me-2"></i>自訂圖表 (Metabase)
<i class="fas fa-external-link-alt ms-1 small text-muted"></i>
</a>
</li>
{% endif %}
{% if grist_url %}
<li>
<a class="dropdown-item" href="{{ grist_url }}" target="_blank">
<i class="fas fa-table me-2"></i>資料協作 (Grist)
<i class="fas fa-external-link-alt ms-1 small text-muted"></i>
</a>
</li>
{% endif %}
</ul>
</li>
<!-- 廠商缺貨 -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'vendor_stockout' %}active{% endif %}" href="/vendor-stockout">
<i class="fas fa-box-open me-1"></i>廠商缺貨
</a>
</li>
<!-- AI 助手 -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if active_page in ['ai_recommend', 'ai_history'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-robot me-1"></i>AI 助手
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/ai_recommend">
<i class="fas fa-magic me-2"></i>智慧文案生成
</a>
</li>
<li>
<a class="dropdown-item" href="/ai_history">
<i class="fas fa-history me-2"></i>生成歷史記錄
</a>
</li>
</ul>
</li>
<!-- 雲端匯入 -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'auto_import' %}active{% endif %}" href="/auto_import">
<i class="fas fa-cloud-download-alt me-1"></i>雲端匯入
</a>
</li>
<!-- 系統管理 -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if active_page in ['settings', 'system_settings', 'logs', 'crawler', 'user_management'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-cog me-1"></i>系統管理
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/user_management">
<i class="fas fa-users-cog me-2"></i>用戶管理
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="/settings">
<i class="fas fa-robot me-2"></i>爬蟲管理
</a>
</li>
<li>
<a class="dropdown-item" href="/crawler_management">
<i class="fas fa-spider me-2"></i>爬蟲設定
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="/system_settings">
<i class="fas fa-sliders-h me-2"></i>系統設定
</a>
</li>
<li>
<a class="dropdown-item" href="/logs">
<i class="fas fa-file-alt me-2"></i>系統日誌
</a>
</li>
</ul>
</li>
</ul>
<!-- 右側區域 -->
<div class="d-flex align-items-center">
<!-- 時間顯示 -->
<span class="navbar-text text-light small me-3">
<i class="fas fa-clock me-1"></i>
<span id="navbar-datetime">{{ datetime_now or '' }}</span>
</span>
</div>
</div>
</div>
</nav>
<!-- 導航列時間更新腳本 -->
<script>
(function() {
function updateNavbarTime() {
const now = new Date();
const formatted = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
const el = document.getElementById('navbar-datetime');
if (el) el.textContent = formatted;
}
// 每秒更新時間
setInterval(updateNavbarTime, 1000);
})();
</script>

View File

@@ -1,250 +0,0 @@
<!-- cspell:ignore MOMO -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>營運成長報表 - MOMO 監控系統</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">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f6f9; }
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.card { border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.03); margin-bottom: 1.5rem; transition: all 0.3s ease; background: #fff; }
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
.card-header { background-color: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); font-weight: 700; color: #2c3e50; padding: 1.25rem; }
.kpi-card { position: relative; overflow: hidden; border: none; }
.kpi-card .icon-bg { position: absolute; right: -15px; bottom: -15px; font-size: 6rem; opacity: 0.15; transform: rotate(-15deg); pointer-events: none; }
.kpi-value { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 0.2rem; }
.kpi-label { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; }
.trend-up { color: #2ecc71; }
.trend-down { color: #e74c3c; }
</style>
</head>
<body class="bg-body-tertiary">
{% include 'components/_navbar.html' %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
<h4 class="mb-0 fw-bold text-dark"><i class="fas fa-rocket me-2 text-success"></i>營運成長策略報表</h4>
<span class="text-muted small">數據更新至: {{ chart_data.labels[-1] if chart_data.labels else '-' }}</span>
</div>
<!-- KPI Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card kpi-card bg-primary text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">YTD 本年度累計業績 ({{ kpi.current_year }})</div>
<div class="kpi-value">${{ "{:,.0f}".format(kpi.ytd_revenue) }}</div>
<div class="mt-2">
<span class="badge bg-white text-primary me-2">YoY Growth</span>
<span class="fw-bold {{ 'text-white' if kpi.ytd_growth >= 0 else 'text-warning' }}">
<i class="fas fa-{{ 'arrow-up' if kpi.ytd_growth >= 0 else 'arrow-down' }} me-1"></i>
{{ "{:+.1f}%".format(kpi.ytd_growth) }}
</span>
<span class="small text-white-50 ms-1">vs 去年同期</span>
</div>
<i class="fas fa-chart-line icon-bg"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card kpi-card bg-success text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">近30天平均客單價 (AOV)</div>
<div class="kpi-value">${{ "{:,.0f}".format(kpi.recent_aov) }}</div>
<div class="mt-2 small text-white-50">
真實訂單基礎 (Unique Order ID)
</div>
<i class="fas fa-shopping-cart icon-bg"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card kpi-card bg-info text-white h-100 shadow-sm">
<div class="card-body p-4">
<div class="kpi-label text-white-50">總訂單數 (Total Orders)</div>
<div class="kpi-value">{{ "{:,.0f}".format(kpi.total_orders) }}</div>
<div class="mt-2 small text-white-50">
全時期累計
</div>
<i class="fas fa-receipt icon-bg"></i>
</div>
</div>
</div>
</div>
<!-- Charts Row 1: Revenue & Growth -->
<div class="row mb-4">
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-chart-bar me-2"></i>月營收與年增率 (Revenue & YoY)</span>
</div>
<div class="card-body">
<div style="height: 350px;">
<canvas id="revenueChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-percentage me-2"></i>月增率分析 (MoM)
</div>
<div class="card-body">
<div style="height: 350px;">
<canvas id="momChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Row 2: AOV & Profit -->
<div class="row mb-4">
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-wallet me-2"></i>客單價趨勢 (AOV Trend)
</div>
<div class="card-body">
<div style="height: 300px;">
<canvas id="aovChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<i class="fas fa-hand-holding-usd me-2"></i>獲利能力分析 (Gross Margin %)
</div>
<div class="card-body">
<div style="height: 300px;">
<canvas id="marginChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Data Injection -->
<script id="chart-data" type="application/json">
{{ chart_data | tojson }}
</script>
<script>
const data = JSON.parse(document.getElementById('chart-data').textContent);
// 1. Revenue & YoY Chart (Mixed)
new Chart(document.getElementById('revenueChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: '月營收 ($)',
data: data.revenue,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
order: 2
},
{
label: 'YoY 年增率 (%)',
data: data.yoy,
type: 'line',
borderColor: '#ff6384',
borderWidth: 2,
yAxisID: 'y1',
order: 1,
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, title: {display: true, text: '金額 ($)'} },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
title: {display: true, text: '成長率 (%)'}
}
}
}
});
// 2. MoM Chart
new Chart(document.getElementById('momChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'MoM 月增率 (%)',
data: data.mom,
backgroundColor: (ctx) => {
const val = ctx.raw;
return val >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)';
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } }
}
});
// 3. AOV Chart
new Chart(document.getElementById('aovChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '平均客單價 ($)',
data: data.aov,
borderColor: '#36a2eb',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
// 4. Margin Rate Chart
new Chart(document.getElementById('marginChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '毛利率 (%)',
data: data.margin_rate,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
</script>
</body>
</html>