535 lines
22 KiB
HTML
535 lines
22 KiB
HTML
{#
|
||
EwoooC base v3.0 — Production
|
||
─────────────────────────────────────────────────────────────
|
||
變更重點:
|
||
1. 移除原本 540 行 inline CSS 全站漸層覆蓋(page-header gradient、bg-primary 強蓋等)
|
||
2. 群組 accent 改由 _ewoooc_shell.html 設置 [data-page-group],自動切換暖色
|
||
3. 字型切到 v3 token 規範(Inter + JetBrains Mono + Noto Sans TC)
|
||
4. Bootstrap override 改為精準作用於 .momo-app 內部,不污染外部 widget
|
||
#}
|
||
<!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>
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Noto+Sans+TC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<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">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-tokens.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-tokens-v2-alias.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-shell.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-dotmatrix.css') }}">
|
||
{% if active_page|default('') in [
|
||
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',
|
||
'obs_host_health', 'obs_ai_calls', 'obs_budget',
|
||
'obs_promotion_review', 'obs_rag_queries', 'obs_quality_trend',
|
||
'obs_ppt_audit'
|
||
] %}
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/observability-system.css') }}">
|
||
{% endif %}
|
||
{% block extra_css %}{% endblock %}
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ewoooc-v3-page-guard.css') }}">
|
||
{% block extra_head %}{% endblock %}
|
||
|
||
<style>
|
||
/* ═══════════════════════════════════════════════════
|
||
* Bootstrap override — 限定在 .momo-app 內
|
||
* ═══════════════════════════════════════════════════ */
|
||
|
||
/* 主 accent 統一替換為群組色 */
|
||
.momo-app .btn-primary {
|
||
background: var(--momo-page-accent);
|
||
border-color: var(--momo-page-accent);
|
||
color: var(--momo-page-inverse);
|
||
box-shadow: none;
|
||
}
|
||
.momo-app .btn-primary:hover,
|
||
.momo-app .btn-primary:focus {
|
||
background: var(--momo-page-accent-dark);
|
||
border-color: var(--momo-page-accent-dark);
|
||
color: var(--momo-page-inverse);
|
||
}
|
||
|
||
.momo-app .btn-outline-primary {
|
||
color: var(--momo-page-accent-dark);
|
||
border-color: var(--momo-page-accent-line);
|
||
background: transparent;
|
||
}
|
||
.momo-app .btn-outline-primary:hover {
|
||
color: var(--momo-page-inverse);
|
||
background: var(--momo-page-accent);
|
||
border-color: var(--momo-page-accent);
|
||
}
|
||
|
||
.momo-app .text-primary {
|
||
color: var(--momo-page-accent-dark) !important;
|
||
}
|
||
|
||
.momo-app .bg-primary {
|
||
background: var(--momo-page-accent) !important;
|
||
color: var(--momo-page-inverse) !important;
|
||
}
|
||
|
||
.momo-app .border-primary {
|
||
border-color: var(--momo-page-accent-line) !important;
|
||
}
|
||
|
||
/* Card 統一風格 — 平面化 */
|
||
.momo-app .card {
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
box-shadow: none;
|
||
}
|
||
.momo-app .card-header {
|
||
background: transparent;
|
||
border-bottom: 1px solid var(--momo-border-light);
|
||
padding: var(--momo-space-3) var(--momo-space-4);
|
||
font-family: var(--momo-font-display);
|
||
font-weight: 700;
|
||
color: var(--momo-text-primary);
|
||
}
|
||
.momo-app .card-body {
|
||
padding: var(--momo-space-4);
|
||
}
|
||
|
||
/* Page header — 移除原本黑色/橘色漸層 hero */
|
||
.momo-app .page-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: var(--momo-space-4);
|
||
padding: var(--momo-space-4) 0 var(--momo-space-5);
|
||
border-bottom: 1px solid var(--momo-border-light);
|
||
margin-bottom: var(--momo-space-5);
|
||
background: transparent;
|
||
}
|
||
.momo-app .page-header h1,
|
||
.momo-app .page-header h2,
|
||
.momo-app .page-header h3 {
|
||
margin: 0;
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-display);
|
||
font-size: var(--momo-text-headline);
|
||
font-weight: 700;
|
||
letter-spacing: 0;
|
||
line-height: 1.2;
|
||
}
|
||
.momo-app .page-header p,
|
||
.momo-app .page-header small,
|
||
.momo-app .page-header .text-muted {
|
||
color: var(--momo-text-secondary);
|
||
}
|
||
|
||
/* Bootstrap badges → 暖色 tag */
|
||
.momo-app .badge.bg-primary {
|
||
background: var(--momo-tag-caramel-bg) !important;
|
||
color: var(--momo-tag-caramel-text) !important;
|
||
border: 1px solid var(--momo-tag-caramel-border);
|
||
font-weight: 600;
|
||
}
|
||
.momo-app .badge.bg-warning,
|
||
.momo-app .text-bg-warning {
|
||
background: var(--momo-tag-honey-bg) !important;
|
||
color: var(--momo-tag-honey-text) !important;
|
||
border: 1px solid var(--momo-tag-honey-border);
|
||
}
|
||
.momo-app .badge.bg-danger,
|
||
.momo-app .text-bg-danger {
|
||
background: var(--momo-tag-rust-bg) !important;
|
||
color: var(--momo-tag-rust-text) !important;
|
||
border: 1px solid var(--momo-tag-rust-border);
|
||
}
|
||
.momo-app .badge.bg-success,
|
||
.momo-app .text-bg-success {
|
||
background: var(--momo-tag-success-bg) !important;
|
||
color: var(--momo-tag-success-text) !important;
|
||
border: 1px solid var(--momo-tag-success-border);
|
||
}
|
||
.momo-app .badge.bg-info,
|
||
.momo-app .text-bg-info {
|
||
background: var(--momo-tag-olive-bg) !important;
|
||
color: var(--momo-tag-olive-text) !important;
|
||
border: 1px solid var(--momo-tag-olive-border);
|
||
}
|
||
.momo-app .badge.bg-dark,
|
||
.momo-app .badge.bg-secondary {
|
||
background: var(--momo-tag-ink-bg) !important;
|
||
color: var(--momo-tag-ink-text) !important;
|
||
border: 1px solid var(--momo-tag-ink-border);
|
||
}
|
||
.momo-app .badge {
|
||
font-family: var(--momo-font-mono);
|
||
font-weight: 600;
|
||
font-size: var(--momo-text-label);
|
||
letter-spacing: 0.02em;
|
||
padding: 3px 8px;
|
||
border-radius: var(--momo-radius-sm);
|
||
}
|
||
|
||
/* Table — 平面化 */
|
||
.momo-app .table {
|
||
color: var(--momo-text-primary);
|
||
margin-bottom: 0;
|
||
}
|
||
.momo-app .table > :not(caption) > * > * {
|
||
background: transparent;
|
||
border-bottom-color: var(--momo-border-light);
|
||
padding: var(--momo-space-3) var(--momo-space-3);
|
||
}
|
||
.momo-app .table thead th {
|
||
background: var(--momo-bg-paper) !important;
|
||
color: var(--momo-text-secondary) !important;
|
||
border-bottom: 1px solid var(--momo-border-light);
|
||
font-family: var(--momo-font-display);
|
||
font-size: var(--momo-text-label);
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
.momo-app .table tbody tr:hover {
|
||
background: var(--momo-page-accent-soft);
|
||
}
|
||
|
||
/* Form */
|
||
.momo-app .form-control,
|
||
.momo-app .form-select {
|
||
background: var(--momo-bg-elevated);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
color: var(--momo-text-primary);
|
||
font-size: var(--momo-text-body-sm);
|
||
box-shadow: none;
|
||
}
|
||
.momo-app .form-control:focus,
|
||
.momo-app .form-select:focus {
|
||
border-color: var(--momo-page-accent-line);
|
||
box-shadow: var(--momo-shadow-focus);
|
||
}
|
||
|
||
/* Progress bar */
|
||
.momo-app .progress {
|
||
background: var(--momo-bg-subtle);
|
||
border-radius: var(--momo-radius-pill);
|
||
height: 6px;
|
||
box-shadow: none;
|
||
}
|
||
.momo-app .progress-bar {
|
||
background: var(--momo-page-accent);
|
||
}
|
||
|
||
/* Filter section — 移除原本 gradient header */
|
||
.momo-app .filter-section {
|
||
padding: var(--momo-space-4);
|
||
background: var(--momo-bg-paper);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
color: var(--momo-text-primary);
|
||
}
|
||
.momo-app .filter-section h3,
|
||
.momo-app .filter-section h4,
|
||
.momo-app .filter-section h5 {
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-display);
|
||
font-weight: 700;
|
||
}
|
||
.momo-app .filter-section .form-label {
|
||
color: var(--momo-text-secondary);
|
||
font-family: var(--momo-font-display);
|
||
font-size: var(--momo-text-label);
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Nav pills / tabs */
|
||
.momo-app .nav-pills .nav-link.active,
|
||
.momo-app .nav-tabs .nav-link.active {
|
||
background: var(--momo-page-accent);
|
||
color: var(--momo-page-inverse);
|
||
border-color: var(--momo-page-accent);
|
||
}
|
||
.momo-app .nav-pills .nav-link,
|
||
.momo-app .nav-tabs .nav-link {
|
||
color: var(--momo-text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Pagination */
|
||
.momo-app .page-item.active .page-link {
|
||
background: var(--momo-page-accent);
|
||
border-color: var(--momo-page-accent);
|
||
color: var(--momo-page-inverse);
|
||
}
|
||
.momo-app .page-link {
|
||
color: var(--momo-text-secondary);
|
||
background: var(--momo-bg-elevated);
|
||
border-color: var(--momo-border-light);
|
||
}
|
||
|
||
/* Dropdown */
|
||
.momo-app .dropdown-menu {
|
||
background: var(--momo-bg-elevated);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
box-shadow: var(--momo-shadow-modal);
|
||
}
|
||
.momo-app .dropdown-item.active,
|
||
.momo-app .dropdown-item:active {
|
||
background: var(--momo-page-accent);
|
||
color: var(--momo-page-inverse);
|
||
}
|
||
.momo-app .dropdown-item:hover {
|
||
background: var(--momo-page-accent-soft);
|
||
color: var(--momo-page-accent-dark);
|
||
}
|
||
|
||
/* Modal */
|
||
.momo-app .modal-content {
|
||
background: var(--momo-bg-elevated);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-xl);
|
||
box-shadow: var(--momo-shadow-modal);
|
||
}
|
||
.momo-app .modal-header {
|
||
border-bottom-color: var(--momo-border-light);
|
||
}
|
||
.momo-app .modal-footer {
|
||
border-top-color: var(--momo-border-light);
|
||
}
|
||
|
||
/* Alert — 去飽和 */
|
||
.momo-app .alert {
|
||
border-radius: var(--momo-radius-md);
|
||
border-width: 1px;
|
||
font-size: var(--momo-text-body-sm);
|
||
}
|
||
.momo-app .alert-warning {
|
||
background: var(--momo-warning-bg);
|
||
border-color: var(--momo-warning-border);
|
||
color: var(--momo-warning-text);
|
||
}
|
||
.momo-app .alert-danger {
|
||
background: var(--momo-danger-bg);
|
||
border-color: var(--momo-danger-border);
|
||
color: var(--momo-danger-text);
|
||
}
|
||
.momo-app .alert-success {
|
||
background: var(--momo-success-bg);
|
||
border-color: var(--momo-success-border);
|
||
color: var(--momo-success-text);
|
||
}
|
||
.momo-app .alert-info {
|
||
background: var(--momo-info-bg);
|
||
border-color: var(--momo-info-border);
|
||
color: var(--momo-info-text);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="momo-v2-body {% if active_page|default('') in [
|
||
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',
|
||
'obs_host_health', 'obs_ai_calls', 'obs_budget',
|
||
'obs_promotion_review', 'obs_rag_queries', 'obs_quality_trend',
|
||
'obs_ppt_audit'
|
||
] %}momo-observability-mode{% endif %}">
|
||
|
||
{# 群組映射 — Jinja 計算 [data-page-group] #}
|
||
{% set _page = active_page|default('') %}
|
||
{% set _group_monitor = ['dashboard', 'edm', 'campaigns'] %}
|
||
{% set _group_analytics = ['sales', 'daily_sales', 'monthly', 'growth'] %}
|
||
{% set _group_ops = ['vendor_stockout', 'auto_import', 'market_intel'] %}
|
||
{% set _group_ai = ['ai_recommend', 'ai_history', 'ai_intelligence',
|
||
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',
|
||
'obs_host_health', 'obs_ai_calls', 'obs_budget',
|
||
'obs_promotion_review', 'obs_rag_queries', 'obs_quality_trend',
|
||
'obs_ppt_audit'] %}
|
||
{% set _group_system = ['settings', 'system_settings', 'logs', 'crawler',
|
||
'user_management', 'ai_automation_smoke'] %}
|
||
{% if _page in _group_monitor %}{% set _page_group = 'monitor' %}
|
||
{% elif _page in _group_analytics %}{% set _page_group = 'analytics' %}
|
||
{% elif _page in _group_ops %}{% set _page_group = 'ops' %}
|
||
{% elif _page in _group_ai %}{% set _page_group = 'ai' %}
|
||
{% elif _page in _group_system %}{% set _page_group = 'system' %}
|
||
{% else %}{% set _page_group = 'monitor' %}
|
||
{% endif %}
|
||
|
||
<div class="momo-app momo-shell" id="momo-shell"
|
||
data-active-page="{{ _page }}"
|
||
data-page-group="{{ _page_group }}">
|
||
|
||
{% include 'components/_ewoooc_shell.html' %}
|
||
|
||
{% set _next_run = next_run|default(None) %}
|
||
{% set _session_username = session.get('username') if session is defined else None %}
|
||
{% set _session_role = session.get('role') if session is defined else None %}
|
||
{% set _is_logged_in = session.get('logged_in') if session is defined else false %}
|
||
|
||
<section class="momo-main-shell">
|
||
<header class="momo-topbar">
|
||
<button class="momo-mobile-menu-button" type="button" data-momo-sidebar-toggle aria-label="開啟主選單">
|
||
<i class="fas fa-bars"></i>
|
||
</button>
|
||
|
||
<form class="momo-search-box" role="search" method="GET" action="/">
|
||
<i class="fas fa-search" aria-hidden="true"></i>
|
||
<input class="momo-search-input" type="search" name="q" value="{{ search_query|default('') }}" placeholder="搜尋商品名稱、編號、品牌">
|
||
<kbd class="momo-shortcut">⌘K</kbd>
|
||
</form>
|
||
|
||
<div class="momo-topbar-spacer"></div>
|
||
|
||
{% if _next_run %}
|
||
<div class="momo-topbar-pill">
|
||
<span class="momo-live-dot"></span>
|
||
<span>下次排程</span>
|
||
<strong>{{ _next_run }}</strong>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<a class="momo-icon-button momo-obs-link" href="/observability/overview" title="AI 觀測台" id="momo-obs-link">
|
||
<i class="fas fa-satellite-dish"></i>
|
||
<span id="momo-obs-badge" class="momo-obs-badge" hidden></span>
|
||
</a>
|
||
<button class="momo-icon-button" type="button" title="說明">
|
||
<i class="fas fa-circle-question"></i>
|
||
</button>
|
||
<button class="momo-icon-button" type="button" title="通知">
|
||
<i class="fas fa-bell"></i>
|
||
</button>
|
||
|
||
{% if _is_logged_in %}
|
||
<button class="momo-user-chip" type="button">
|
||
<span class="momo-avatar">{{ (_session_username or '已')[:1] }}</span>
|
||
<span class="momo-user-meta">
|
||
<span class="momo-user-name">{{ _session_username or '已登入' }}</span>
|
||
{% if _session_role %}
|
||
<span class="momo-user-role">{{ _session_role }}</span>
|
||
{% endif %}
|
||
</span>
|
||
<i class="fas fa-chevron-down" aria-hidden="true" style="font-size:10px;color:var(--momo-text-tertiary)"></i>
|
||
</button>
|
||
{% endif %}
|
||
</header>
|
||
|
||
<main class="momo-content">
|
||
{% block content %}{% block ewooo_content %}{% endblock %}{% endblock %}
|
||
</main>
|
||
</section>
|
||
</div>
|
||
|
||
<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_CODE_RE = /^[A-Za-z0-9_-]{4,}$/;
|
||
|
||
function toText(value) {
|
||
return value == null ? '' : String(value);
|
||
}
|
||
|
||
function isLikelyMomoCode(value) {
|
||
const cleaned = toText(value).trim();
|
||
if (!cleaned) return false;
|
||
const lowered = cleaned.toLowerCase();
|
||
if (['nan', 'none', 'null', 'undefined'].includes(lowered)) return false;
|
||
if (lowered.startsWith('momo_') || lowered.startsWith('manual_') || lowered.startsWith('pchome_')) return false;
|
||
return MOMO_CODE_RE.test(cleaned);
|
||
}
|
||
|
||
function extractIcode(url) {
|
||
const target = toText(url).trim();
|
||
if (!target) return '';
|
||
try {
|
||
const parsed = new URL(target, location.origin);
|
||
return toText(parsed.searchParams.get('i_code')).trim();
|
||
} catch (error) {
|
||
const match = /[?&]i_code=([^&#]+)/i.exec(target);
|
||
return match ? decodeURIComponent(match[1] || '').trim() : '';
|
||
}
|
||
}
|
||
|
||
function buildSafeMomoUrl(iCode) {
|
||
const cleaned = toText(iCode).trim();
|
||
if (!isLikelyMomoCode(cleaned)) return '';
|
||
return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(cleaned)}`;
|
||
}
|
||
|
||
document.addEventListener('click', function (event) {
|
||
const link = event.target.closest ? event.target.closest('a.momo-tracked-link') : null;
|
||
if (!link) return;
|
||
|
||
const href = toText(link.getAttribute('href')).trim();
|
||
const original = toText(link.dataset && link.dataset.momoOriginalUrl).trim();
|
||
const iCode = toText(link.dataset && (link.dataset.trackIcode || link.dataset.trackProductId)).trim()
|
||
|| extractIcode(original)
|
||
|| extractIcode(href);
|
||
const safeUrl = buildSafeMomoUrl(iCode);
|
||
|
||
if (safeUrl && (!href || href === '#' || /ec404/i.test(href) || /ec404/i.test(original))) {
|
||
event.preventDefault();
|
||
link.setAttribute('href', safeUrl);
|
||
window.open(safeUrl, link.getAttribute('target') || '_self', 'noopener,noreferrer');
|
||
}
|
||
}, true);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||
const fetchWithCSRF = (url, options = {}) => {
|
||
options.headers = { ...options.headers, 'X-CSRFToken': csrfToken };
|
||
return fetch(url, options);
|
||
};
|
||
|
||
(function () {
|
||
const shell = document.getElementById('momo-shell');
|
||
const toggle = document.querySelector('[data-momo-sidebar-toggle]');
|
||
const close = document.querySelector('[data-momo-sidebar-close]');
|
||
if (!shell || !toggle) return;
|
||
toggle.addEventListener('click', () => shell.classList.toggle('is-sidebar-open'));
|
||
if (close) close.addEventListener('click', () => shell.classList.remove('is-sidebar-open'));
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
(function() {
|
||
const link = document.getElementById('momo-obs-link');
|
||
const badge = document.getElementById('momo-obs-badge');
|
||
if (!link || !badge) return;
|
||
async function refresh() {
|
||
try {
|
||
const r = await fetch('/observability/api/health_indicator', { credentials: 'same-origin' });
|
||
if (!r.ok) return;
|
||
const d = await r.json();
|
||
if (!d.ok) return;
|
||
link.title = d.tooltip || 'AI 觀測台';
|
||
if (d.alert_count > 0) {
|
||
badge.textContent = d.alert_count;
|
||
badge.hidden = false;
|
||
link.classList.add('is-alert');
|
||
} else {
|
||
badge.hidden = true;
|
||
link.classList.remove('is-alert');
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
refresh();
|
||
setInterval(refresh, 60000);
|
||
})();
|
||
</script>
|
||
|
||
{% block extra_js %}{% endblock %}
|
||
{% block extra_scripts %}{% endblock %}
|
||
</body>
|
||
</html>
|