Files
ewoooc/templates/admin/business_intel.html
OoO f2aece5b71
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s
perf: 外部化觀測台圖表腳本
2026-05-18 09:30:34 +08:00

710 lines
23 KiB
HTML
Raw Permalink 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.
{% extends "ewoooc_base.html" %}
{% set active_page = 'obs_business_intel' %}
{% block title %}商業 AI 戰果室{% endblock %}
{% block extra_css %}
<style>
.biz-warroom {
--biz-ink: #2f211b;
--biz-muted: #8a6b5c;
--biz-card: rgba(255, 252, 247, 0.95);
--biz-line: rgba(201, 100, 66, 0.16);
--biz-alert: #b8442f;
--biz-good: #2f8f6b;
--biz-warn: #c98a2e;
position: relative;
overflow: hidden;
}
.biz-warroom::before {
content: "";
position: absolute;
inset: 0 0 auto auto;
width: min(560px, 58vw);
height: min(560px, 58vw);
background: radial-gradient(circle, rgba(201, 100, 66, 0.16), transparent 62%);
pointer-events: none;
transform: translate(26%, -34%);
}
.biz-hero {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.55fr);
gap: 1.15rem;
margin-bottom: 1.25rem;
}
.biz-command {
background:
linear-gradient(135deg, rgba(53, 35, 27, 0.96), rgba(126, 64, 42, 0.92)),
radial-gradient(circle at top right, rgba(255, 194, 115, 0.22), transparent 42%);
color: #fff8ee;
border-radius: 28px;
padding: clamp(1.35rem, 2.5vw, 2.25rem);
box-shadow: 0 28px 60px rgba(78, 45, 31, 0.24);
min-height: 320px;
}
.biz-kicker {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .35rem .65rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: #ffd7a2;
font-size: .82rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.biz-command h1 {
font-size: clamp(2.05rem, 4vw, 3.25rem);
line-height: 1.05;
letter-spacing: 0;
margin: 1rem 0 .85rem;
font-weight: 900;
}
.biz-command p {
max-width: 760px;
color: rgba(255, 248, 238, 0.78);
font-size: 1.02rem;
line-height: 1.75;
}
.biz-meta-row {
display: flex;
flex-wrap: wrap;
gap: .7rem;
margin-top: 1.4rem;
}
.biz-meta-pill {
display: inline-flex;
align-items: center;
gap: .45rem;
padding: .65rem .85rem;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 248, 238, 0.9);
font-weight: 800;
}
.biz-filter-card {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 28px;
padding: 1.2rem;
box-shadow: 0 18px 46px rgba(93, 57, 42, 0.12);
align-self: stretch;
}
.biz-filter-card label {
font-size: .78rem;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--biz-muted);
font-weight: 900;
}
.biz-filter-card .form-control {
border-radius: 16px;
border-color: rgba(201, 100, 66, 0.18);
min-height: 48px;
}
.biz-signal-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .9rem;
margin-bottom: 1.1rem;
}
.biz-signal {
position: relative;
overflow: hidden;
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 24px;
padding: 1.05rem;
box-shadow: 0 14px 34px rgba(93, 57, 42, 0.08);
}
.biz-signal::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 4px;
background: linear-gradient(90deg, #c96442, #f1b45a, #2f8f6b);
opacity: .82;
}
.biz-signal .label {
color: var(--biz-muted);
font-size: .8rem;
font-weight: 900;
letter-spacing: .04em;
}
.biz-signal .value {
color: var(--biz-ink);
font-size: clamp(1.7rem, 3vw, 2.45rem);
font-weight: 950;
letter-spacing: 0;
margin: .3rem 0 .05rem;
}
.biz-signal .note {
color: var(--biz-muted);
font-size: .86rem;
}
.biz-alert-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: .9rem;
padding: 1rem 1.15rem;
border-radius: 22px;
border: 1px solid rgba(184, 68, 47, 0.2);
background: linear-gradient(135deg, rgba(184, 68, 47, 0.12), rgba(255, 248, 238, 0.92));
color: var(--biz-alert);
margin-bottom: 1.1rem;
font-weight: 850;
}
.biz-layout {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(340px, .9fr);
gap: 1rem;
align-items: start;
}
.biz-panel {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 26px;
box-shadow: 0 14px 38px rgba(93, 57, 42, 0.08);
overflow: hidden;
margin-bottom: 1rem;
}
.biz-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.05rem 1.15rem .75rem;
border-bottom: 1px solid rgba(201, 100, 66, 0.1);
}
.biz-panel-head h3 {
margin: 0;
color: var(--biz-ink);
font-weight: 950;
letter-spacing: 0;
}
.biz-panel-head p {
margin: .25rem 0 0;
color: var(--biz-muted);
font-size: .88rem;
}
.biz-panel-body {
padding: 1rem 1.15rem 1.15rem;
}
.biz-strategy-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .75rem;
}
.biz-strategy-card {
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 20px;
padding: .95rem;
background: linear-gradient(135deg, #fffaf3, #fffdf9);
}
.biz-strategy-card .strategy {
color: var(--biz-ink);
font-weight: 950;
font-size: 1.02rem;
}
.biz-strategy-card .metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .45rem;
margin-top: .75rem;
}
.biz-mini-metric {
padding: .55rem;
border-radius: 14px;
background: rgba(201, 100, 66, 0.07);
}
.biz-mini-metric b {
display: block;
color: var(--biz-ink);
font-size: 1rem;
}
.biz-mini-metric span {
color: var(--biz-muted);
font-size: .72rem;
font-weight: 800;
}
.biz-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 .55rem;
}
.biz-table th {
color: var(--biz-muted);
font-size: .76rem;
letter-spacing: .04em;
text-transform: uppercase;
border: 0;
padding: 0 .7rem .15rem;
}
.biz-table td {
border: 0;
padding: .78rem .7rem;
background: rgba(255, 250, 243, 0.82);
vertical-align: middle;
}
.biz-table tr td:first-child {
border-radius: 16px 0 0 16px;
}
.biz-table tr td:last-child {
border-radius: 0 16px 16px 0;
}
.biz-decision-list {
display: grid;
gap: .75rem;
}
.biz-decision-card {
display: grid;
grid-template-columns: minmax(82px, .36fr) minmax(240px, 1.15fr) minmax(130px, .46fr);
gap: .8rem;
align-items: center;
padding: .95rem;
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 18px;
background:
linear-gradient(135deg, rgba(255, 252, 247, 0.96), rgba(255, 248, 241, 0.86)),
radial-gradient(circle, rgba(201, 100, 66, 0.12) 1px, transparent 1.2px);
background-size: auto, 13px 13px;
}
.biz-decision-time {
color: var(--biz-muted);
font-size: .82rem;
font-weight: 850;
}
.biz-decision-sku,
.biz-decision-name {
color: var(--biz-ink);
font-weight: 900;
}
.biz-decision-name {
line-height: 1.45;
}
.biz-decision-reason {
grid-column: 2 / 4;
color: var(--biz-muted);
font-size: .82rem;
line-height: 1.5;
padding-top: .55rem;
border-top: 1px dashed rgba(201, 100, 66, 0.18);
}
.biz-price-stack {
color: var(--biz-ink);
font-weight: 850;
}
.biz-price-stack small {
display: block;
color: var(--biz-muted);
font-weight: 700;
}
.biz-badge {
display: inline-flex;
align-items: center;
padding: .28rem .55rem;
border-radius: 999px;
font-size: .76rem;
font-weight: 900;
background: rgba(201, 100, 66, 0.11);
color: #8e3d28;
}
.biz-badge.good {
background: rgba(47, 143, 107, 0.12);
color: var(--biz-good);
}
.biz-badge.warn {
background: rgba(201, 138, 46, 0.14);
color: var(--biz-warn);
}
.biz-chart-shell {
height: 250px;
padding: .4rem;
}
.biz-empty {
padding: 1rem;
border-radius: 18px;
background: rgba(201, 100, 66, 0.06);
color: var(--biz-muted);
font-weight: 800;
}
@media (max-width: 1180px) {
.biz-hero,
.biz-layout {
grid-template-columns: 1fr;
}
.biz-signal-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.biz-command h1 {
font-size: 2.45rem;
}
.biz-signal-grid,
.biz-strategy-grid,
.biz-decision-card {
grid-template-columns: 1fr;
}
.biz-decision-reason {
grid-column: auto;
}
.biz-alert-strip {
align-items: flex-start;
flex-direction: column;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
{% import "admin/_observability_labels.html" as obs_label %}
{% set rec_total = rec_by_strategy|sum(attribute='count') if rec_by_strategy else 0 %}
{% set ns = namespace(conf_total=0, effective=0, backfired=0, neutral=0, verdict_total=0) %}
{% for r in rec_by_strategy %}
{% set ns.conf_total = ns.conf_total + ((r.avg_confidence or 0) * (r.count or 0)) %}
{% endfor %}
{% for v in verdict_stats %}
{% set label = v.verdict or '未分類' %}
{% set ns.verdict_total = ns.verdict_total + (v.count or 0) %}
{% if label == 'effective' or label == 'success' or label == 'positive' %}
{% set ns.effective = ns.effective + (v.count or 0) %}
{% elif label == 'backfired' or label == 'negative' or label == 'failed' %}
{% set ns.backfired = ns.backfired + (v.count or 0) %}
{% else %}
{% set ns.neutral = ns.neutral + (v.count or 0) %}
{% endif %}
{% endfor %}
{% set avg_conf = (ns.conf_total / rec_total) if rec_total else 0 %}
{% set effective_rate = ((ns.effective / ns.verdict_total) * 100) if ns.verdict_total else 0 %}
<div class="biz-warroom">
<div class="biz-hero">
<section class="biz-command">
<span class="biz-kicker"><i class="fas fa-store"></i> 商業情報</span>
<h1 class="biz-title">商業 AI 戰果室</h1>
<p>
這一頁不再只是資料列表,而是把價格建議、未跟進警示、閉環學習與競品監測收成一個商業決策控制台。
先看 AI 是否真的推動結果,再往下追每一筆策略與市場訊號。
</p>
<div class="biz-meta-row">
<span class="biz-meta-pill"><i class="fas fa-calendar-day"></i> 近 {{ days }} 天</span>
<span class="biz-meta-pill"><i class="fas fa-brain"></i> {{ rec_total }} 筆價格建議</span>
<span class="biz-meta-pill"><i class="fas fa-rotate"></i> {{ ns.verdict_total }} 筆閉環結果</span>
</div>
</section>
<aside class="biz-filter-card">
<form method="get">
<label for="days">觀測窗口</label>
<div class="d-flex gap-2 mt-2">
<input class="form-control" id="days" name="days" type="number" min="1" max="90" value="{{ days }}">
<button class="btn btn-primary" type="submit">更新</button>
</div>
</form>
<div class="mt-4">
<div class="text-muted small fw-bold text-uppercase">決策節奏</div>
<div class="h4 fw-black mb-1">先警示,再追因</div>
<p class="text-muted mb-0">未跟進高信心建議會被拉到第一層;其餘資訊按策略、閉環、競品三條線拆解。</p>
</div>
</aside>
</div>
{% if error %}
<div class="alert alert-danger"><i class="fas fa-triangle-exclamation me-2"></i>{{ error }}</div>
{% endif %}
<section class="biz-signal-grid">
<article class="biz-signal">
<div class="label">高信心未跟進</div>
<div class="value">{{ unfollowed_count }}</div>
<div class="note">信心分 >= 0.8 且仍未轉行動計畫</div>
</article>
<article class="biz-signal">
<div class="label">平均信心分</div>
<div class="value">{{ '%.0f'|format(avg_conf * 100) }}%</div>
<div class="note">依策略建議量加權</div>
</article>
<article class="biz-signal">
<div class="label">有效率</div>
<div class="value">{{ '%.0f'|format(effective_rate) }}%</div>
<div class="note">有效 / 已回收結論</div>
</article>
<article class="biz-signal">
<div class="label">競品監測</div>
<div class="value">{{ recent_competitor_prices|length }}</div>
<div class="note">近 24h 價格變動樣本</div>
</article>
</section>
{% if unfollowed_count > 0 %}
<section class="biz-alert-strip">
<div><i class="fas fa-bell me-2"></i>{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為行動計畫或標記原因。</div>
<span class="biz-badge warn">需人工決策</span>
</section>
{% endif %}
<div class="biz-layout">
<main>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>策略族群雷達</h3>
<p>把 AI 價格建議依策略類型聚合,快速判斷目前主攻降價、防守或毛利修復。</p>
</div>
<span class="biz-badge">{{ rec_by_strategy|length }} 類策略</span>
</div>
<div class="biz-panel-body">
{% if rec_by_strategy %}
<div class="biz-strategy-grid">
{% for r in rec_by_strategy %}
<article class="biz-strategy-card">
<div class="strategy">{{ obs_label.strategy(r.strategy) }}</div>
<div class="metrics">
<div class="biz-mini-metric"><b>{{ r.count }}</b><span>建議數</span></div>
<div class="biz-mini-metric"><b>{{ '%.0f'|format((r.avg_confidence or 0) * 100) }}%</b><span>信心</span></div>
<div class="biz-mini-metric"><b>{{ '%.1f'|format(r.avg_gap_pct or 0) }}%</b><span>價差</span></div>
</div>
<div class="mt-2 text-muted small">平均銷量變化 {{ '%.1f'|format(r.avg_sales_delta or 0) }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前觀測窗口沒有策略建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>最近 AI 價格建議</h3>
<p>保留決策原因與競品價差,方便直接追到 SKU 層級。</p>
</div>
<span class="biz-badge">Latest 20</span>
</div>
<div class="biz-panel-body">
{% if latest_recommendations %}
<div class="biz-decision-list">
{% for r in latest_recommendations %}
<article class="biz-decision-card">
<div>
<div class="biz-decision-time">{{ r.created_at or '-' }}</div>
<div class="biz-decision-sku">{{ r.sku }}</div>
</div>
<div>
<div class="biz-decision-name">{{ r.name or '-' }}</div>
<div class="mt-1">
<span class="biz-badge">{{ obs_label.strategy(r.strategy, '-') }}</span>
<span class="biz-badge {% if (r.confidence or 0) >= 0.8 %}good{% endif %}">{{ '%.0f'|format((r.confidence or 0) * 100) }}%</span>
</div>
</div>
<div class="biz-price-stack">
{{ r.momo_price or '-' }} / {{ r.pchome_price or '-' }}
<small>差距 {{ '%.1f'|format(r.gap_pct or 0) }}%</small>
</div>
<div class="biz-decision-reason">{{ r.reason or '尚無原因摘要' }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前沒有最新價格建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>閉環學習紀錄</h3>
<p>追蹤行動計畫到實際結果的真實效果,這是 AI 能不能變聰明的核心證據。</p>
</div>
<span class="biz-badge good">{{ loop_records|length }} 筆紀錄</span>
</div>
<div class="biz-panel-body table-responsive">
{% if loop_records %}
<table class="biz-table">
<thead>
<tr>
<th>計畫</th>
<th>SKU</th>
<th>狀態</th>
<th>建立 / 執行</th>
<th>結論</th>
<th>指標</th>
<th>變化</th>
</tr>
</thead>
<tbody>
{% for r in loop_records %}
<tr>
<td>#{{ r.plan_id }}<br><span class="text-muted small">{{ obs_label.plan_type(r.plan_type, '-') }}</span></td>
<td><strong>{{ r.sku }}</strong></td>
<td><span class="biz-badge {% if r.status == 'done' or r.status == 'completed' %}good{% elif r.status == 'pending' %}warn{% endif %}">{{ obs_label.status(r.status, '-') }}</span></td>
<td>{{ r.created_at or '-' }} / {{ r.executed_at or '-' }}</td>
<td>{{ obs_label.verdict(r.verdict, '-') }}</td>
<td>{{ obs_label.metric(r.metric_type, '-') }}<br><span class="text-muted small">{{ r.before or '-' }} → {{ r.after or '-' }}</span></td>
<td><strong>{{ '%.1f'|format(r.change_pct or 0) }}%</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚未形成行動計畫到實際結果的閉環紀錄。</div>
{% endif %}
</div>
</section>
</main>
<aside>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>結論戰果分布</h3>
<p>用結果反校正 AI 建議,不讓漂亮信心分掩蓋真實成效。</p>
</div>
</div>
<div class="biz-panel-body">
<div class="biz-chart-shell"><canvas id="verdictPieChart"></canvas></div>
{% if verdict_stats %}
<table class="biz-table mt-2">
<thead><tr><th>結論</th><th>數量</th><th>平均變化</th></tr></thead>
<tbody>
{% for v in verdict_stats %}
<tr><td>{{ obs_label.verdict(v.verdict) }}</td><td>{{ v.count }}</td><td>{{ '%.1f'|format(v.avg_delta or 0) }}%</td></tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無結論統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品比對品質</h3>
<p>先看比對是否可靠,再拿價格差做策略判斷。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if match_stats %}
<table class="biz-table">
<thead><tr><th>狀態</th><th>數量</th><th>候選數</th><th>分數</th></tr></thead>
<tbody>
{% for m in match_stats %}
<tr>
<td><span class="biz-badge {% if m.status == 'matched' or m.status == 'success' %}good{% elif m.status == 'failed' %}warn{% endif %}">{{ obs_label.status(m.status, '-') }}</span></td>
<td>{{ m.count }}</td>
<td>{{ '%.1f'|format(m.avg_candidates or 0) }}</td>
<td>{{ '%.2f'|format(m.avg_score or 0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無競品比對統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品 24h 價格脈動</h3>
<p>保留最新市場訊號,支援人工快速複核。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if recent_competitor_prices %}
<table class="biz-table">
<thead><tr><th>時間</th><th>SKU</th><th>PChome / momo</th><th>折扣</th></tr></thead>
<tbody>
{% for r in recent_competitor_prices %}
<tr>
<td>{{ r.crawled_at or '-' }}</td>
<td><strong>{{ r.sku }}</strong><br><span class="text-muted small">{{ r.product_name or '-' }}</span></td>
<td>{{ r.pchome_price or '-' }} / {{ r.momo_price or '-' }}<br><span class="text-muted small">差距 {{ r.gap or '-' }}</span></td>
<td>{{ '%.1f'|format(r.discount_pct or 0) }}%<br><span class="text-muted small">分數 {{ '%.2f'|format(r.match_score or 0) }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">近 24h 尚無競品價格樣本。</div>
{% endif %}
</div>
</section>
</aside>
</div>
<div class="text-muted small mt-3">資料來源AI 價格建議、行動計畫、實際結果、競品比對與競品價格歷史。</div>
</div>
{% endblock %}
{% block extra_js %}
<template id="obs-business-verdict-data">{{ verdict_stats | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}