Files
ewoooc/templates/ewoooc_base.html
OoO 539dea12c5
All checks were successful
CD Pipeline / deploy (push) Successful in 58s
套用全站 V3 頁面樣式護欄
2026-05-12 23:30:08 +08:00

535 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{#
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>