feat: align analysis pages to growth workflow

This commit is contained in:
ogt
2026-06-25 19:19:24 +08:00
parent e2b28b4b69
commit a47bd66a69
11 changed files with 368 additions and 276 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.696"
SYSTEM_VERSION = "V10.697"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -773,3 +773,4 @@ POSTGRES_HOST=momo-db
| 2026-06-25 | 比價頁每筆結果也必須能雙開賣場 | V10.694 起 `/price_comparison` 的結果列在 PChome/MOMO 連結都存在時提供「雙開賣場」操作Excel 與手動輸入提示改成白話作戰語言,不再顯示「格式說明、欄位、商品名稱,價格」這類工程化提示;`tests/test_frontend_v2_assets.py` 鎖定此行為。 |
| 2026-06-25 | 匯入任務列表只顯示處置提醒 | V10.695 起 `/auto_import` 任務列表不再把 `error_message` 原文當主要欄位顯示,而是由 `buildImportActionHint()` 轉成 Google Drive 授權、當日業績明細檔、重新匯入或通知維護人員等下一步,避免重啟後瀏覽器/授權/同步技術錯誤直接暴露給營運使用者。 |
| 2026-06-25 | 系統設定匯入提示不得顯示資料表或日誌口徑 | V10.696 起 `/system_settings` 不再用 `realtime_sales_monthly` 判斷前端提示,也不再顯示「資料落點、檢查日誌、發生系統錯誤」等內部口徑;所有匯入與備份失敗提示統一走 `toImportActionMessage()`,轉成重新授權、改用正確業績報表、重新匯入或通知維護人員。 |
| 2026-06-25 | 分析與建議頁必須使用 PChome 作戰流程語言 | V10.697 起 `/sales_analysis``/monthly_summary_analysis``/ai_recommend` 頁首與主要操作區統一使用「主推、守價、補比價、成長缺口、毛利貢獻、品類結構」等營運語言;前台不得把 AI 模型、權杖、資料庫、欄位、英文指標縮寫或內部錯誤作為使用者主訊息。 |

View File

@@ -1,5 +1,5 @@
{% extends 'ewoooc_base.html' %}
{% block title %}AI 智慧推薦 · EwoooC{% endblock %}
{% block title %}PChome 銷售建議 · EwoooC{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-ai-recommend.css') }}">
@@ -7,44 +7,48 @@
{% endblock %}
{% block ewooo_content %}
{# 頁面執行時會呼叫 page-ai-recommend.js: fetch('/api/ai/generate_copy', ...), fetch('/api/ai/web_search', ...), fetch('/api/ai/product_insights', ...), fetch('/api/ai/gemini_usage?days=30'). #}
<div class="momo-app" data-page-group="ai">
<div class="ai-recommend-page">
{# ── Hero header ─────────────────────────────────── #}
<header class="ai-recommend-hero ar-hero">
<div class="ar-hero__title">
<h1 class="ai-recommend-title">
<i class="fas fa-robot"></i>AI 智慧推薦
<i class="fas fa-bullhorn"></i>PChome 銷售建議
</h1>
<small class="ar-hero__sub">把價差、商品證據與趨勢轉成可追蹤的銷售建議</small>
<small class="ar-hero__sub">把價差、商品證據與趨勢轉成主推、調價、補比價動作</small>
</div>
<div class="ar-hero__actions">
<span id="ollamaStatus" class="ar-status {{ 'ar-status--ok' if ollama_status else 'ar-status--off' }}">
<i class="fas fa-server"></i> AI 模型主路徑 {{ '檢查中' if ollama_status is none else ('' if ollama_status else '') }}
<i class="fas fa-wand-magic-sparkles"></i> 建議引擎 {{ '檢查中' if ollama_status is none else ('可用' if ollama_status else '待確認') }}
</span>
<span id="geminiStatus" class="ar-status {{ 'ar-status--info' if gemini_status else 'ar-status--off' }}">
<i class="fab fa-google"></i> Gemini 備援 {{ '檢查中' if gemini_status is none else ('' if gemini_status else '') }}
<i class="fas fa-shield-alt"></i> 備援守門 {{ '檢查中' if gemini_status is none else ('可用' if gemini_status else '待確認') }}
</span>
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#helpModal" title="銷售動作">
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#helpModal" title="銷售決策流程">
<i class="fas fa-question-circle"></i>
</button>
</div>
</header>
<section class="ar-command-strip" aria-label="PChome 銷售建議流程">
<span class="ar-command-strip__label">處理順序</span>
<strong><i class="fas fa-box-open" aria-hidden="true"></i>選商品</strong>
<strong><i class="fas fa-chart-line" aria-hidden="true"></i>看價差與趨勢</strong>
<strong><i class="fas fa-list-check" aria-hidden="true"></i>產生下一步</strong>
<a href="/ai_intelligence" class="ar-command-strip__link">回今日作戰</a>
</section>
<div class="row">
{# ── 左側:文案生成 ────────────────────────────── #}
<div class="col-lg-6 col-xl-5 mb-3">
<article class="card ar-card ar-card--gen">
<header class="card-header ar-card__head ar-card__head--accent">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fas fa-magic me-2"></i>文案生成</h6>
<small class="ar-card__step">第 1 步:設定參數</small>
<h6 class="mb-0"><i class="fas fa-magic me-2"></i>銷售動作生成</h6>
<small class="ar-card__step">設定商品與目的</small>
</div>
</header>
<div class="card-body py-3">
{# 商品名稱 #}
<div class="mb-3">
<label class="form-label fw-bold mb-1">
<i class="fas fa-box me-1"></i>商品名稱 <span class="text-danger">*</span>
@@ -62,33 +66,28 @@
</ul>
</div>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted"><i class="fas fa-lightbulb me-1"></i>可從右側熱銷商品快速選取</small>
<small class="text-muted"><i class="fas fa-lightbulb me-1"></i>可從右側 PChome 熱銷商品帶入</small>
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" onclick="document.getElementById('productName').value=''">
<small>清除</small>
</button>
</div>
</div>
{# 風格 / 引擎 / 模型 #}
<div class="row g-2 mb-3">
<div class="col-4">
<label class="form-label small fw-bold mb-1">文案風格</label>
<div class="col-12">
<label class="form-label small fw-bold mb-1">建議目的</label>
<select class="form-select form-select-sm" id="copyStyle">
<option value="吸睛">🎯 吸睛活潑</option>
<option value="專業">🔬 專業權威</option>
<option value="溫馨">💕 溫馨感性</option>
<option value="急迫">⚡ 限時急迫</option>
<option value="主推曝光">主推曝光</option>
<option value="價格防守">價格防守</option>
<option value="活動轉換">活動轉換</option>
<option value="補資料確認">補資料確認</option>
</select>
</div>
<div class="col-4">
<label class="form-label small fw-bold mb-1">AI 路徑</label>
<div class="ar-engine-settings" aria-hidden="true">
<select class="form-select form-select-sm" id="aiProvider" onchange="onProviderChange()">
<option value="ollama" {% if default_provider == 'ollama' %}selected{% endif %}>🖥️ AI 模型主路徑</option>
<option value="gemini" disabled>☁️ Gemini 備援(系統自動,不可手動選)</option>
<option value="ollama" {% if default_provider == 'ollama' %}selected{% endif %}>primary</option>
<option value="gemini" disabled>fallback</option>
</select>
</div>
<div class="col-4">
<label class="form-label small fw-bold mb-1">分析模型</label>
<select class="form-select form-select-sm" id="ollamaModelSelect">
{% for model in available_models %}
<option value="{{ model }}" {% if 'gemma3:4b' in model %}selected{% endif %}>{{ model }}</option>
@@ -102,20 +101,12 @@
</div>
</div>
{# Gemini 用量面板 #}
<div id="geminiUsagePanel" class="alert alert-info py-2 mb-3" style="display: none;">
<div class="d-flex justify-content-between align-items-center">
<small class="fw-bold"><i class="fab fa-google me-1"></i>Gemini 備援本月使用量</small>
<button type="button" class="btn btn-link btn-sm p-0" onclick="loadGeminiUsage()"><i class="fas fa-sync-alt"></i></button>
</div>
<div class="d-flex justify-content-between small mt-1">
<span>費用:<strong id="geminiMonthlyCost">$0.0000</strong></span>
<span>請求:<span id="geminiRequestCount">0</span></span>
<span>用量:<span id="geminiTokenUsage">0</span></span>
</div>
<div id="geminiUsagePanel" class="ar-engine-settings" style="display: none;" aria-hidden="true">
<span id="fallbackMonthlySpend">$0.0000</span>
<span id="geminiRequestCount">0</span>
<span id="geminiTokenUsage">0</span>
</div>
{# 關鍵字+節日 #}
<div class="accordion accordion-flush" id="advancedOptions">
<div class="accordion-item border-0">
<h2 class="accordion-header">
@@ -145,17 +136,16 @@
<hr class="my-2">
<div class="d-grid">
<button class="btn btn-primary" onclick="generateCopy()" id="generateBtn">
<i class="fas fa-magic me-2"></i>生成文案
<i class="fas fa-magic me-2"></i>產生銷售建議
</button>
</div>
</div>
</article>
{# 生成結果 #}
<div id="resultArea" class="mt-3" style="display: none;">
<article class="card ar-card ar-card--result">
<header class="card-header ar-card__head ar-card__head--success d-flex justify-content-between align-items-center">
<span><i class="fas fa-sparkles me-2"></i>AI 生成文案</span>
<span><i class="fas fa-sparkles me-2"></i>可用銷售文案</span>
<button class="btn btn-sm btn-light" onclick="copyCopyText()" title="複製全部文案到剪貼簿">
<i class="fas fa-copy me-1"></i>複製
</button>
@@ -168,12 +158,11 @@
</article>
</div>
{# AI 智慧搜尋 #}
<article class="card ar-card mt-3 ar-card--search">
<header class="card-header ar-card__head ar-card__head--accent-soft py-2">
<div>
<h6 class="mb-0"><i class="fas fa-search-dollar me-2"></i>AI 智慧搜尋</h6>
<small class="text-muted">輸入關鍵字,AI 分析市場趨勢</small>
<h6 class="mb-0"><i class="fas fa-search-dollar me-2"></i>市場訊號快查</h6>
<small class="text-muted">輸入關鍵字,整理趨勢與可用行動</small>
</div>
</header>
<div class="card-body py-2">
@@ -201,7 +190,6 @@
</div>
</article>
{# 商品洞察 #}
<article class="card ar-card mt-3 ar-card--insights">
<header class="card-header ar-card__head ar-card__head--warn-soft d-flex justify-content-between align-items-center py-2">
<div>
@@ -209,7 +197,7 @@
<small class="text-muted">把外部訊號轉成可追蹤的銷售動作</small>
</div>
<button class="btn btn-sm btn-warning" onclick="doProductInsights()" id="insightsBtn">
<i class="fas fa-search-dollar me-1"></i>分析商品
<i class="fas fa-search-dollar me-1"></i>判斷下一步
</button>
</header>
<div id="productInsightsResult" class="card-body py-3" style="display: none;"></div>
@@ -220,7 +208,6 @@
</article>
</div>
{# ── 右側:市場資訊 tabs ──────────────────────── #}
<div class="col-lg-6 col-xl-7">
<ul class="nav nav-pills nav-fill mb-3" id="marketInfoTabs" role="tablist">
<li class="nav-item" role="presentation">
@@ -246,13 +233,12 @@
</ul>
<div class="tab-content" id="marketInfoTabContent">
{# 趨勢洞察 #}
<div class="tab-pane fade show active" id="trends-panel" role="tabpanel">
<article class="card ar-card ar-card--trends">
<header class="card-header ar-card__head ar-card__head--soft d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-chart-line me-2"></i>即時趨勢洞察</h6>
<small class="text-muted">來自 PTT、Dcard、Google News</small>
<small class="text-muted">可用來挑主推與文案角度</small>
</div>
<div class="d-flex gap-1">
<select class="form-select form-select-sm" id="trendSource" style="width: 90px;" onchange="refreshTrends()">
@@ -286,7 +272,6 @@
</article>
</div>
{# 熱銷商品 #}
<div class="tab-pane fade" id="bestsellers-panel" role="tabpanel">
<article class="card ar-card">
<header class="card-header ar-card__head ar-card__head--soft d-flex justify-content-between align-items-center py-2">
@@ -316,7 +301,6 @@
</article>
</div>
{# 排行榜 #}
<div class="tab-pane fade" id="rankings-panel" role="tabpanel">
<div class="row g-3">
<div class="col-12 col-md-6">
@@ -364,7 +348,6 @@
</div>
</div>
{# 趨勢新聞 #}
<div class="tab-pane fade" id="news-panel" role="tabpanel">
<article class="card ar-card">
<header class="card-header ar-card__head ar-card__head--soft py-2">
@@ -404,12 +387,11 @@
</div>
</div>
{# ── Help modal ──────────────────────────────────── #}
<div class="modal fade" id="helpModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="fas fa-question-circle me-2"></i>銷售動作</h6>
<h6 class="modal-title"><i class="fas fa-question-circle me-2"></i>銷售決策流程</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-3">
@@ -435,11 +417,10 @@
</div>
</div>
{# ── Loading overlay ─────────────────────────────── #}
<div id="loadingOverlay" class="ar-loading-overlay d-none">
<div class="ar-loading-overlay__inner">
<div class="spinner-border spinner-border-lg mb-3"></div>
<h5 id="loadingText">AI 正在思考中...</h5>
<h5 id="loadingText">正在整理銷售建議...</h5>
</div>
</div>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'ewoooc_base.html' %}
{% block title %}月份總表數據分析 - EwoooC{% endblock %}
{% block title %}PChome 月結作戰分析 - EwoooC{% endblock %}
{#
Turn B — extends ewoooc_base + BEM 結構化
@@ -17,10 +17,9 @@
{% block ewooo_content %}
{# Runtime data source lives in page-monthly-summary.js: /api/monthly_summary_data #}
<!-- ============ Loading Overlay (warm reskin) ============ -->
<div id="loadingOverlay" class="ms-loading" role="status" aria-live="polite">
<div class="ms-loading__spinner" aria-hidden="true"></div>
<span class="ms-loading__text">資料載入</span>
<span class="ms-loading__text">月結業績整理</span>
</div>
<div class="monthly-analysis-page ms-page"
@@ -29,12 +28,11 @@
{% include 'components/_analysis_report_tabs.html' %}
<!-- Page head -->
<header class="monthly-analysis-hero ms-page-head">
<div class="ms-page-head__main">
<h1 class="ms-page-head__title">
<i class="fas fa-chart-pie ms-page-head__icon" aria-hidden="true"></i>
月份總表數據分析
PChome 月結作戰分析
</h1>
<p class="ms-page-head__sub">
用月結資料判斷成長毛利與品類結構
@@ -42,13 +40,19 @@
</div>
<div class="ms-page-head__meta">
<span class="ms-tag ms-tag--pill">
<i class="fas fa-database" aria-hidden="true"></i>
<span>{{ system_version }}</span>
<i class="fas fa-calendar-check" aria-hidden="true"></i>
<span>月結分析</span>
</span>
</div>
</header>
<!-- 進階篩選器 -->
<section class="ms-action-strip" aria-label="月結業績判斷順序">
<span class="ms-action-strip__label">決策路徑</span>
<strong><i class="fas fa-chart-line" aria-hidden="true"></i></strong>
<strong><i class="fas fa-coins" aria-hidden="true"></i></strong>
<strong><i class="fas fa-layer-group" aria-hidden="true"></i>調</strong>
</section>
<section class="ms-filter-card filter-section" aria-label="資料篩選器">
<div class="row g-3">
<div class="col-md-2 ms-filter-group ms-filter-group--year">
@@ -83,15 +87,15 @@
<div class="col-md-2 ms-filter-group ms-filter-group--area">
<label class="ms-filter-group__label form-label" for="btnArea">
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle ms-filter-group__btn"
type="button" id="btnArea" data-bs-toggle="dropdown" aria-expanded="false">
所有區域
全部區域
</button>
<ul class="dropdown-menu w-100 ms-filter-group__menu" id="listArea">
<li><a class="dropdown-item" href="#" onclick="selectFilter('area', '')">所有區域</a></li>
<li><a class="dropdown-item" href="#" onclick="selectFilter('area', '')">全部區域</a></li>
</ul>
</div>
</div>
@@ -103,7 +107,7 @@
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle ms-filter-group__btn"
type="button" id="btnVendor" data-bs-toggle="dropdown" aria-expanded="false">
所有及廠商
全部廠商
</button>
<ul class="dropdown-menu w-100 ms-filter-group__menu" id="listVendor">
<li class="px-2 py-1">
@@ -116,7 +120,7 @@
<div class="col-md-2 ms-filter-group ms-filter-group--trade">
<label class="ms-filter-group__label form-label" for="btnTrade">
<i class="fas fa-exchange-alt" aria-hidden="true"></i>
<i class="fas fa-exchange-alt" aria-hidden="true"></i>
</label>
<div class="dropdown">
<button class="btn btn-light custom-dropdown-btn dropdown-toggle ms-filter-group__btn"
@@ -131,18 +135,17 @@
<div class="col-md-1 d-flex align-items-end">
<button class="btn ms-filter-card__refresh w-100" type="button" onclick="fetchData()">
<i class="fas fa-sync-alt" aria-hidden="true"></i>
<i class="fas fa-sync-alt" aria-hidden="true"></i>
</button>
</div>
</div>
</section>
<!-- 商業洞察 (Top 3 Highlights) -->
<section class="row g-3 mb-4 ms-highlight-grid" id="highlightsRow" style="display: none;" aria-label="Top 3 品牌洞察">
<div class="col-md-4">
<article class="card h-100 border-0 shadow-sm overflow-hidden ms-highlight ms-highlight--accent">
<header class="card-header ms-highlight__head">
<h6 class="ms-highlight__title"><i class="fas fa-trophy" aria-hidden="true"></i> (Top 3 Brands)</h6>
<h6 class="ms-highlight__title"><i class="fas fa-trophy" aria-hidden="true"></i> </h6>
</header>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0 ms-highlight__table">
@@ -154,7 +157,7 @@
<div class="col-md-4">
<article class="card h-100 border-0 shadow-sm overflow-hidden ms-highlight ms-highlight--olive">
<header class="card-header ms-highlight__head">
<h6 class="ms-highlight__title"><i class="fas fa-coins" aria-hidden="true"></i> (Top 3 Brands)</h6>
<h6 class="ms-highlight__title"><i class="fas fa-coins" aria-hidden="true"></i> </h6>
</header>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0 ms-highlight__table">
@@ -166,7 +169,7 @@
<div class="col-md-4">
<article class="card h-100 border-0 shadow-sm overflow-hidden ms-highlight ms-highlight--honey">
<header class="card-header ms-highlight__head">
<h6 class="ms-highlight__title"><i class="fas fa-fire" aria-hidden="true"></i> (Top 3 Brands)</h6>
<h6 class="ms-highlight__title"><i class="fas fa-fire" aria-hidden="true"></i> </h6>
</header>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0 ms-highlight__table">
@@ -177,12 +180,11 @@
</div>
</section>
<!-- 年度 YoY -->
<section class="row mb-4">
<div class="col-12">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-history ms-chart-card__icon" aria-hidden="true"></i> YoY vs </h6>
<h6 class="ms-chart-card__title"><i class="fas fa-history ms-chart-card__icon" aria-hidden="true"></i>年度業績對照(本期 / 去年同</h6>
</header>
<div class="card-body">
<div id="yoyTrendChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -196,12 +198,11 @@
</div>
</section>
<!-- 廠商排行 -->
<section class="row mb-4">
<div class="col-lg-12">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-building ms-chart-card__icon" aria-hidden="true"></i>Top 50 </h6>
<h6 class="ms-chart-card__title"><i class="fas fa-building ms-chart-card__icon" aria-hidden="true"></i>Top 50 </h6>
</header>
<div class="card-body">
<div id="vendorRankingChart" class="ms-chart-card__canvas" style="height: 900px;"></div>
@@ -215,12 +216,11 @@
</div>
</section>
<!-- 類別分佈 + 價格帶 -->
<section class="row mb-4">
<div class="col-lg-6">
<article class="card border-0 shadow-sm h-100 ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-chart-pie ms-chart-card__icon" aria-hidden="true"></i>Top 12</h6>
<h6 class="ms-chart-card__title"><i class="fas fa-chart-pie ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body text-center">
<div id="divisionDistChart" class="ms-chart-card__canvas" style="height: 350px;"></div>
@@ -235,7 +235,7 @@
<div class="col-lg-6">
<article class="card border-0 shadow-sm h-100 ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-tags ms-chart-card__icon" aria-hidden="true"></i></h6>
<h6 class="ms-chart-card__title"><i class="fas fa-tags ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="priceRangeChart" class="ms-chart-card__canvas" style="height: 350px;"></div>
@@ -249,12 +249,11 @@
</div>
</section>
<!-- 主對比圖 -->
<section class="row">
<div class="col-lg-12">
<article class="card mb-4 ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-chart-line ms-chart-card__icon" aria-hidden="true"></i></h6>
<h6 class="ms-chart-card__title"><i class="fas fa-chart-line ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="compareChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -270,12 +269,11 @@
</div>
</section>
<!-- BCG + 散佈 -->
<section class="row mb-4">
<div class="col-lg-6">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-th ms-chart-card__icon" aria-hidden="true"></i> BCG vs </h6>
<h6 class="ms-chart-card__title"><i class="fas fa-th ms-chart-card__icon" aria-hidden="true"></i>品牌策略矩陣(銷量 / 毛利</h6>
</header>
<div class="card-body">
<div id="bcgMatrixChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -285,7 +283,7 @@
<div class="col-lg-6">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-braille ms-chart-card__icon" aria-hidden="true"></i> vs </h6>
<h6 class="ms-chart-card__title"><i class="fas fa-braille ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="priceVolumeScatterChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -294,12 +292,11 @@
</div>
</section>
<!-- 季節熱力圖 -->
<section class="row mb-4">
<div class="col-lg-12">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-th-large ms-chart-card__icon" aria-hidden="true"></i></h6>
<h6 class="ms-chart-card__title"><i class="fas fa-th-large ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="seasonalityHeatmapChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -308,12 +305,11 @@
</div>
</section>
<!-- 區域排行 -->
<section class="row mb-4">
<div class="col-lg-12">
<article class="card border-0 shadow-sm ms-chart-card">
<header class="card-header ms-chart-card__head">
<h6 class="ms-chart-card__title"><i class="fas fa-map ms-chart-card__icon" aria-hidden="true"></i>Area Ranking</h6>
<h6 class="ms-chart-card__title"><i class="fas fa-map ms-chart-card__icon" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="areaRankingChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -327,12 +323,11 @@
</div>
</section>
<!-- 4 個專屬區域對比圖情報族群色 -->
<section class="row">
<div class="col-lg-12">
<article class="card mb-4 shadow-sm ms-special ms-special--info">
<header class="card-header ms-special__head">
<h6 class="ms-special__title"><i class="fas fa-layer-group" aria-hidden="true"></i> &amp; </h6>
<h6 class="ms-special__title"><i class="fas fa-layer-group" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="specialChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -352,7 +347,7 @@
<div class="col-lg-12">
<article class="card mb-4 shadow-sm ms-special ms-special--olive">
<header class="card-header ms-special__head">
<h6 class="ms-special__title"><i class="fas fa-magic" aria-hidden="true"></i></h6>
<h6 class="ms-special__title"><i class="fas fa-magic" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="bodyCareChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -372,7 +367,7 @@
<div class="col-lg-12">
<article class="card mb-4 shadow-sm ms-special ms-special--honey">
<header class="card-header ms-special__head">
<h6 class="ms-special__title"><i class="fas fa-palette" aria-hidden="true"></i> &amp; </h6>
<h6 class="ms-special__title"><i class="fas fa-palette" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="makeupFragranceChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -392,7 +387,7 @@
<div class="col-lg-12">
<article class="card mb-4 shadow-sm ms-special ms-special--mahogany">
<header class="card-header ms-special__head">
<h6 class="ms-special__title"><i class="fas fa-heartbeat" aria-hidden="true"></i> &amp; </h6>
<h6 class="ms-special__title"><i class="fas fa-heartbeat" aria-hidden="true"></i></h6>
</header>
<div class="card-body">
<div id="privacyInfantChart" class="ms-chart-card__canvas" style="height: 400px;"></div>
@@ -408,12 +403,11 @@
</div>
</section>
<!-- 資料明細 -->
<section class="card shadow-sm mb-5 ms-data-table">
<header class="card-header ms-data-table__head">
<h5 class="ms-data-table__title"><i class="fas fa-list" aria-hidden="true"></i> </h5>
<h5 class="ms-data-table__title"><i class="fas fa-list" aria-hidden="true"></i> </h5>
<a href="/system_settings" class="btn btn-sm btn-outline-primary ms-data-table__import">
<i class="fas fa-plus" aria-hidden="true"></i>
<i class="fas fa-plus" aria-hidden="true"></i>
</a>
</header>
<div class="card-body">
@@ -423,11 +417,11 @@
<tr>
<th>/</th>
<th>處別</th>
<th>名稱</th>
<th></th>
<th>PM</th>
<th>品牌</th>
<th>廠商</th>
<th>交易</th>
<th>交易型態</th>
<th>銷售額(本月)</th>
<th>YoY</th>
</tr>

View File

@@ -1,5 +1,5 @@
{% extends 'ewoooc_base.html' %}
{% block title %}業績分析 - EwoooC{% endblock %}
{% block title %}PChome 業績作戰分析 - EwoooC{% endblock %}
{#
v3 改寫重點:
@@ -22,7 +22,6 @@
{% block ewooo_content %}
<!-- ============ Loading Overlay (warm reskin) ============ -->
<div id="loadingOverlay" data-screen-label="loading">
<div class="loading-logo-container">
<div class="logo-pulse"></div>
@@ -40,23 +39,22 @@
<div class="loading-logo">EwoooC</div>
</div>
<div class="loading-text" id="loadingText">
<i class="fas fa-chart-bar me-2" aria-hidden="true"></i>正在載入數據...
<i class="fas fa-chart-bar me-2" aria-hidden="true"></i>正在整理業績...
</div>
<div class="loading-progress"><div class="loading-progress-bar"></div></div>
<div class="loading-hint" id="loadingHint">大量資料可能需要較長時間,請稍候</div>
<div class="loading-hint" id="loadingHint">先整理可主推、需守價與待補資料的商品</div>
</div>
<div class="sales-analysis-page" data-page-group="analytics" data-screen-label="sales-analysis">
{% include 'components/_analysis_report_tabs.html' %}
<!-- ============ Page header ============ -->
<header class="sa-page-head">
<div class="sa-page-head__lead">
<h4 class="sa-page-head__title">
<i class="fas fa-chart-pie" aria-hidden="true"></i>
業績分析儀表板
PChome 業績作戰分析
</h4>
<p class="sa-page-head__brief">用分類、品牌與毛利找出 PChome 成長槓桿</p>
<p class="sa-page-head__brief">用分類、品牌與毛利找出主推、守價與補資料順序</p>
{% if db_data_range %}
<span class="sa-tag sa-tag--neutral">
<i class="fas fa-calendar-alt" aria-hidden="true"></i>
@@ -67,7 +65,7 @@
{% if not no_filter %}
<div class="sa-page-head__meta">
<span class="sa-tag sa-tag--accent">
<i class="fas fa-database" aria-hidden="true"></i>
<i class="fas fa-filter" aria-hidden="true"></i>
{% if start_date or end_date %}
{% if start_date and end_date %}{{ start_date }} ~ {{ end_date }}
{% elif start_date %}{{ start_date }} 起
@@ -75,12 +73,20 @@
{% elif data_range_months == 0 %}全部資料
{% else %}最近 {{ data_range_months }} 個月{% endif %}
<span class="sa-tag__sep">·</span>
{{ "{:,}".format(total_records) }} 筆
{{ "{:,}".format(total_records) }} 筆商品紀錄
</span>
</div>
{% endif %}
</header>
<section class="sa-command-strip" aria-label="PChome 業績作戰順序">
<span class="sa-command-strip__label">決策順序</span>
<strong><i class="fas fa-bullhorn" aria-hidden="true"></i>主推高業績</strong>
<strong><i class="fas fa-shield-alt" aria-hidden="true"></i>守住高毛利</strong>
<strong><i class="fas fa-link" aria-hidden="true"></i>補齊價差證據</strong>
<a href="/ai_intelligence" class="sa-command-strip__link">回今日作戰</a>
</section>
{% if error %}
<div class="alert alert-warning sa-alert">
<i class="fas fa-exclamation-triangle me-2" aria-hidden="true"></i>{{ error }}
@@ -90,19 +96,18 @@
</div>
{% else %}
<!-- ============ 控制面板 (篩選器) ============ -->
<section class="card sa-filter-card sa-filter-card--sticky">
<div class="card-header sa-filter-head">
<h5 class="sa-filter-head__title">
<i class="fas fa-sliders-h" aria-hidden="true"></i>進階篩選與分析
<i class="fas fa-sliders-h" aria-hidden="true"></i>設定分析視角
</h5>
<div class="btn-group sa-metric-switch" role="group" aria-label="分析維度切換">
{% set metric_opts = [
{'v':'amount', 'label':'依金額分析', 'icon':'dollar-sign'},
{'v':'qty', 'label':'依銷售量分析', 'icon':'box'}
{'v':'amount', 'label':'看業績', 'icon':'dollar-sign'},
{'v':'qty', 'label':'看銷量', 'icon':'box'}
] %}
{% if cols.cost or cols.profit %}
{% set _ = metric_opts.append({'v':'profit', 'label':'毛利分析', 'icon':'chart-line'}) %}
{% set _ = metric_opts.append({'v':'profit', 'label':'毛利', 'icon':'chart-line'}) %}
{% endif %}
{% for m in metric_opts %}
<input type="radio" class="btn-check" name="metric" id="metric{{ m.v|capitalize }}"
@@ -120,16 +125,15 @@
<form method="GET" action="/sales_analysis" class="row g-3">
<input type="hidden" name="metric" value="{{ selected_metric }}">
<!-- ─── 第一區:資料範圍 ─── -->
<div class="col-12">
<div class="sa-filter-group">
<h6 class="sa-filter-group__title">
<i class="fas fa-calendar-check sa-filter-group__icon sa-filter-group__icon--accent" aria-hidden="true"></i>資料範圍設定
<i class="fas fa-calendar-check sa-filter-group__icon sa-filter-group__icon--accent" aria-hidden="true"></i>分析期間
</h6>
<div class="row g-2">
<div class="col-md-3">
<label class="form-label">
<i class="fas fa-database me-1" aria-hidden="true"></i>資料載入範圍
<i class="fas fa-calendar-week me-1" aria-hidden="true"></i>期間快速選擇
</label>
<select name="data_range" class="form-select" onchange="handleDataRangeChange(this)">
<option value="" {% if not request.args.get('data_range') %}selected{% endif %}>-- 請選擇 --</option>
@@ -366,7 +370,6 @@
</div>
</section>
<!-- ============ 引導訊息(首次進入) ============ -->
{% if no_filter %}
<section class="sa-empty">
<div class="sa-empty__inner">
@@ -374,44 +377,43 @@
<i class="fas fa-chart-line" aria-hidden="true"></i>
</div>
<h3 class="sa-empty__title">
<i class="fas fa-hand-point-up me-2" aria-hidden="true"></i>開始分析您的業績數據
<i class="fas fa-hand-point-up me-2" aria-hidden="true"></i>先選期間,再看作戰順序
</h3>
<p class="sa-empty__lead">
請在上方<strong>「進階篩選」</strong>區域選擇以下任一條件開始分析:
選好期間後,直接看主推、守毛利與補比價的商品。
</p>
<div class="row g-3 sa-empty__hints">
<div class="col-md-6">
<div class="sa-empty__hint sa-empty__hint--accent">
<h5><i class="fas fa-database me-2" aria-hidden="true"></i>資料載入範圍</h5>
<p>快速選擇最近 1/3/6/12 個月或全部資料</p>
<p class="sa-empty__hint-foot"><i class="fas fa-star me-1" aria-hidden="true"></i>推薦新手使用</p>
<h5><i class="fas fa-calendar-week me-2" aria-hidden="true"></i>最近資料</h5>
<p>快速近 1/3/6/12 個月的主推與守價商品</p>
<p class="sa-empty__hint-foot"><i class="fas fa-star me-1" aria-hidden="true"></i>適合每日追蹤</p>
</div>
</div>
<div class="col-md-6">
<div class="sa-empty__hint sa-empty__hint--olive">
<h5><i class="fas fa-calendar-alt me-2" aria-hidden="true"></i>自訂日期區間</h5>
<p>鎖定活動或檔期,評估業績變化</p>
<h5><i class="fas fa-calendar-alt me-2" aria-hidden="true"></i>期區間</h5>
<p>鎖定活動或檔期,評估業績、毛利與品類變化</p>
<p class="sa-empty__hint-foot"><i class="fas fa-clock me-1" aria-hidden="true"></i>適合檔期與活動回顧</p>
</div>
</div>
</div>
<div class="alert alert-info sa-empty__tip">
<i class="fas fa-info-circle me-2" aria-hidden="true"></i>
<strong>分析下一步:</strong>選擇條件後,直接看影響業績的圖表、分類與商品明細。
<strong>分析下一步:</strong>選擇條件後,直接看影響業績的圖表、分類與商品明細,再決定曝光、調價或補比價
</div>
</div>
</section>
{% else %}
<!-- ============ KPI 區塊v3語意 variant 取代 Bootstrap bg-* ============ -->
{% set kpi_cards = [
{'variant':'revenue','label':'總業績 (Revenue)', 'value': '${:,.0f}'.format(kpi.revenue), 'icon':'coins', 'show': true},
{'variant':'cost', 'label':'總成本 (Cost)', 'value': '${:,.0f}'.format(kpi.cost), 'icon':'file-invoice-dollar', 'show': cols.cost or cols.profit},
{'variant':'profit', 'label':'毛利額 (Profit)', 'value': '${:,.0f}'.format(kpi.gross_margin), 'icon':'hand-holding-usd', 'show': cols.cost or cols.profit},
{'variant':'rate', 'label':'毛利率 (%)', 'value': '{:.1f}%'.format(kpi.gross_margin_rate),'icon':'percentage', 'show': cols.cost or cols.profit},
{'variant':'qty', 'label':'總銷量 (Qty)', 'value': '{:,.0f}'.format(kpi.qty), 'icon':'boxes', 'show': true},
{'variant':'sku', 'label':'商品數 (SKU)', 'value': '{:,}'.format(kpi.sku_count|default(kpi.count, true)), 'icon':'tags', 'show': true}
{'variant':'revenue','label':'總業績', 'value': '${:,.0f}'.format(kpi.revenue), 'icon':'coins', 'show': true},
{'variant':'cost', 'label':'總成本', 'value': '${:,.0f}'.format(kpi.cost), 'icon':'file-invoice-dollar', 'show': cols.cost or cols.profit},
{'variant':'profit', 'label':'毛利額', 'value': '${:,.0f}'.format(kpi.gross_margin), 'icon':'hand-holding-usd', 'show': cols.cost or cols.profit},
{'variant':'rate', 'label':'毛利率', 'value': '{:.1f}%'.format(kpi.gross_margin_rate),'icon':'percentage', 'show': cols.cost or cols.profit},
{'variant':'qty', 'label':'總銷量', 'value': '{:,.0f}'.format(kpi.qty), 'icon':'boxes', 'show': true},
{'variant':'sku', 'label':'商品數', 'value': '{:,}'.format(kpi.sku_count|default(kpi.count, true)), 'icon':'tags', 'show': true}
] %}
<div class="row mb-4 sa-kpi-row">
{% for k in kpi_cards %}{% if k.show %}
@@ -427,7 +429,6 @@
{% endif %}{% endfor %}
</div>
<!-- ============ ABC 分析 (Pareto) ============ -->
{% if abc_stats %}
<section class="row mb-4">
<div class="col-12">
@@ -435,22 +436,22 @@
<div class="card-body py-3">
<div class="d-flex align-items-center justify-content-between">
<h6 class="sa-panel__title">
<i class="fas fa-sort-amount-down me-2" aria-hidden="true"></i>ABC 分析 (80/20 法則)
<i class="fas fa-sort-amount-down me-2" aria-hidden="true"></i>主推分層
</h6>
<span class="sa-tag sa-tag--neutral">選類別看貢獻</span>
</div>
<div class="row mt-3 sa-abc">
{% set abc_classes = [
{'k':'A','tier':'p0','title':'A 類 (核心)'},
{'k':'B','tier':'p1','title':'B 類 (次要)'},
{'k':'C','tier':'p2','title':'C 類 (長尾)'}
{'k':'A','tier':'p0','title':'A 類:核心主推'},
{'k':'B','tier':'p1','title':'B 類:加強曝光'},
{'k':'C','tier':'p2','title':'C 類:長尾觀察'}
] %}
{% for c in abc_classes %}
<div class="col-md-4 sa-abc__cell sa-abc__cell--{{ c.tier }}"
onclick="window.open('/abc_analysis/detail?class={{ c.k }}&{{ request.query_string.decode() }}', '_blank')">
<h5 class="sa-abc__heading">{{ c.title }}</h5>
<div class="sa-abc__metric">營收佔比:{{ "{:.1f}%".format(abc_stats[c.k]['pct_rev']) }}</div>
<div class="sa-abc__sku">{{ abc_stats[c.k]['count'] }} SKU ({{ "{:.1f}%".format(abc_stats[c.k]['pct_sku']) }})</div>
<div class="sa-abc__sku">商品數:{{ abc_stats[c.k]['count'] }}{{ "{:.1f}%".format(abc_stats[c.k]['pct_sku']) }}</div>
<div class="sa-abc__bar">
<div class="sa-abc__bar-fill" style="width: {{ abc_stats[c.k]['pct_rev'] }}%"></div>
</div>
@@ -463,14 +464,13 @@
</section>
{% endif %}
<!-- ============ 年度對比 (YoY) ============ -->
<section class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<header class="sa-yoy-head">
<h6 class="sa-panel__title">
<i class="fas fa-chart-line me-2" aria-hidden="true"></i>年度對比 (YoY Comparison)
<i class="fas fa-chart-line me-2" aria-hidden="true"></i>年度成長對照
</h6>
<div class="sa-yoy-controls">
<select id="yoy-year1" class="form-select form-select-sm" aria-label="基準年">
@@ -532,15 +532,14 @@
</div>
</section>
<!-- ============ 廠商獲利能力排行 ============ -->
{% if vendor_stats %}
<section class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-industry me-2" aria-hidden="true"></i>廠商獲利能力排行 (Top 100)</span>
<span><i class="fas fa-industry me-2" aria-hidden="true"></i>廠商毛利與主推優先序</span>
<a href="/api/export/excel/vendor?{{ request.query_string.decode() }}" class="btn btn-sm btn-outline-success">
<i class="fas fa-file-excel me-1" aria-hidden="true"></i>匯出報表
<i class="fas fa-file-excel me-1" aria-hidden="true"></i>匯出清單
</a>
</div>
<div class="card-body">
@@ -551,12 +550,12 @@
<th class="text-center" style="width: 60px;">排名</th>
<th>廠商名稱</th>
<th class="text-end">總業績</th>
<th class="text-end">佔比 (%)</th>
<th class="text-end">佔比</th>
<th class="text-end">總銷量</th>
<th class="text-end">平均客單 (ASP)</th>
<th class="text-end">平均客單</th>
{% if cols.cost or cols.profit %}<th class="text-end">毛利額</th>{% endif %}
{% if cols.cost or cols.profit %}<th class="text-end">毛利率</th>{% endif %}
<th class="text-end">商品數 (SKU)</th>
<th class="text-end">商品數</th>
<th class="text-end">平均單品產值</th>
</tr>
</thead>
@@ -594,19 +593,17 @@
</section>
{% endif %}
<!-- ============ 商業洞察 (Business Insights) ============ -->
<section class="row mb-4">
<div class="col-12">
<div class="card sa-insights">
<div class="card-header sa-insights__head">
<i class="fas fa-lightbulb me-2" aria-hidden="true"></i>商業洞察 (Top 3 Highlights)
<i class="fas fa-lightbulb me-2" aria-hidden="true"></i>今日決策 Top 3
</div>
<div class="card-body">
<div class="row sa-insights__row">
<!-- 業績 Top 3 -->
<div class="col-md-4 sa-insights__col">
<div class="sa-insights__col-head">
<h6 class="sa-insights__col-title sa-insights__col-title--accent">🏆 業績貢獻王 (Revenue)</h6>
<h6 class="sa-insights__col-title sa-insights__col-title--accent"><i class="fas fa-trophy" aria-hidden="true"></i>業績主推</h6>
<button class="btn btn-sm btn-outline-primary" onclick="showTopDetail('revenue', 'amount')">
<i class="fas fa-list me-1" aria-hidden="true"></i>詳細
</button>
@@ -625,11 +622,10 @@
</ul>
</div>
<!-- 毛利 Top 3 -->
{% if cols.cost or cols.profit %}
<div class="col-md-4 sa-insights__col">
<div class="sa-insights__col-head">
<h6 class="sa-insights__col-title sa-insights__col-title--olive">💰 獲利金雞母 (Gross Margin)</h6>
<h6 class="sa-insights__col-title sa-insights__col-title--olive"><i class="fas fa-coins" aria-hidden="true"></i>高毛利守價</h6>
<button class="btn btn-sm btn-outline-success" onclick="showTopDetail('margin', 'profit')">
<i class="fas fa-list me-1" aria-hidden="true"></i>詳細
</button>
@@ -653,10 +649,9 @@
</div>
{% endif %}
<!-- 銷量 Top 3 -->
<div class="col-md-4 sa-insights__col sa-insights__col--last">
<div class="sa-insights__col-head">
<h6 class="sa-insights__col-title sa-insights__col-title--mahogany">📦 人氣引流款 (Sales Qty)</h6>
<h6 class="sa-insights__col-title sa-insights__col-title--mahogany"><i class="fas fa-fire" aria-hidden="true"></i>人氣引流</h6>
<button class="btn btn-sm btn-outline-primary" onclick="showTopDetail('quantity', 'qty')">
<i class="fas fa-list me-1" aria-hidden="true"></i>詳細
</button>
@@ -680,18 +675,16 @@
</div>
</section>
<!-- ============ 圖表區塊 ============ -->
{# Turn C圖表 + DataTable配色由 analysis-chart-theme.js 統一注入 #}
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header"><i class="fas fa-chart-bar me-2" aria-hidden="true"></i>Top 20 熱銷排行 ({{ '銷售金額' if selected_metric == 'amount' else '銷售數量' }})</div>
<div class="card-header"><i class="fas fa-chart-bar me-2" aria-hidden="true"></i>Top 20 熱銷排行{{ '銷售金額' if selected_metric == 'amount' else '銷售數量' }}</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 600px;"><canvas id="barChart"></canvas></div></div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header"><i class="fas fa-chart-pie me-2" aria-hidden="true"></i>全站類別分佈 (Top 12)</div>
<div class="card-header"><i class="fas fa-chart-pie me-2" aria-hidden="true"></i>全站類別佔比</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 400px;"><canvas id="categoryChart"></canvas></div></div>
</div>
</div>
@@ -700,7 +693,7 @@
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header"><i class="fas fa-th-large me-2" aria-hidden="true"></i>業績板塊分佈 (分類 → 商品)</div>
<div class="card-header"><i class="fas fa-th-large me-2" aria-hidden="true"></i>業績板塊分佈:分類到商品</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 400px;"><canvas id="treemapChart"></canvas></div></div>
</div>
</div>
@@ -709,13 +702,13 @@
<div class="row mt-4">
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fas fa-chart-column me-2" aria-hidden="true"></i>價格帶業績貢獻 (Price Range)</div>
<div class="card-header"><i class="fas fa-chart-column me-2" aria-hidden="true"></i>價格帶業績貢獻</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 350px;"><canvas id="priceDistChart"></canvas></div></div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fas fa-braille me-2" aria-hidden="true"></i>價格 vs 銷量分佈 (Scatter Plot)</div>
<div class="card-header"><i class="fas fa-braille me-2" aria-hidden="true"></i>價格與銷量分佈</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 350px;"><canvas id="scatterChart"></canvas></div></div>
</div>
</div>
@@ -725,9 +718,9 @@
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header"><i class="fas fa-chess-board me-2" aria-hidden="true"></i>商品策略 BCG 矩陣 (波士頓矩陣)</div>
<div class="card-header"><i class="fas fa-chess-board me-2" aria-hidden="true"></i>商品策略矩陣</div>
<div class="card-body">
<p class="text-muted small mb-2"><i class="fas fa-info-circle me-1" aria-hidden="true"></i> X軸銷量 (市場份額) | Y軸毛利率 (獲利能力) | 十字線:中位數閾值</p>
<p class="text-muted small mb-2"><i class="fas fa-info-circle me-1" aria-hidden="true"></i>右上優先主推;右下先守價或調整組合。</p>
<div class="sa-chart-shell" style="height: 500px;"><canvas id="bcgChart"></canvas></div>
</div>
</div>
@@ -739,7 +732,7 @@
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header"><i class="fas fa-sun me-2" aria-hidden="true"></i>淡旺季熱力圖 (Seasonality Heatmap) — Top 10 分類</div>
<div class="card-header"><i class="fas fa-sun me-2" aria-hidden="true"></i>淡旺季熱力圖Top 10 分類</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 400px;"><canvas id="seasonalityChart"></canvas></div></div>
</div>
</div>
@@ -751,7 +744,7 @@
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-bullhorn me-2" aria-hidden="true"></i>行銷活動業績貢獻 (Marketing Campaign Contribution)</span>
<span><i class="fas fa-bullhorn me-2" aria-hidden="true"></i>活動業績貢獻</span>
<button class="btn btn-sm btn-outline-success" onclick="exportMarketingExcel('all')">
<i class="fas fa-file-excel me-1" aria-hidden="true"></i>匯出全部
</button>
@@ -759,11 +752,11 @@
<div class="card-body">
<div class="row">
<div class="col-lg-6 mb-4">
<h6 class="sa-mkt-title sa-mkt-title--accent"><i class="fas fa-tags me-2" aria-hidden="true"></i>折扣活動排行 (Discount Campaigns)</h6>
<h6 class="sa-mkt-title sa-mkt-title--accent"><i class="fas fa-tags me-2" aria-hidden="true"></i>折扣活動排行</h6>
<div class="sa-chart-shell" style="height: 350px;"><canvas id="mktDiscountChart"></canvas></div>
</div>
<div class="col-lg-6 mb-4">
<h6 class="sa-mkt-title sa-mkt-title--olive"><i class="fas fa-ticket-alt me-2" aria-hidden="true"></i>折價券活動排行 (Coupon Campaigns)</h6>
<h6 class="sa-mkt-title sa-mkt-title--olive"><i class="fas fa-ticket-alt me-2" aria-hidden="true"></i>折價券活動排行</h6>
<div class="sa-chart-shell" style="height: 350px;"><canvas id="mktCouponChart"></canvas></div>
</div>
</div>
@@ -777,7 +770,7 @@
<div class="row mt-4">
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fas fa-calendar-alt me-2" aria-hidden="true"></i>每月業績趨勢 (Monthly Trend)</div>
<div class="card-header"><i class="fas fa-calendar-alt me-2" aria-hidden="true"></i>每月業績趨勢</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 300px;"><canvas id="monthlyChart"></canvas></div></div>
</div>
</div>
@@ -798,7 +791,7 @@
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header"><i class="fas fa-clock me-2" aria-hidden="true"></i>每小時業績熱點 (00:00 — 23:00)</div>
<div class="card-header"><i class="fas fa-clock me-2" aria-hidden="true"></i>每小時業績熱點</div>
<div class="card-body"><div class="sa-chart-shell" style="height: 300px;"><canvas id="hourlyChart"></canvas></div></div>
</div>
</div>
@@ -814,21 +807,20 @@
</div>
{% endif %}
<!-- ============ 詳細資料表 (DataTable) ============ -->
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-list-ol me-2" aria-hidden="true"></i>詳細數據列表 (Top 1000)</span>
<span><i class="fas fa-list-ol me-2" aria-hidden="true"></i>商品作戰清單 Top 1000</span>
</div>
<div class="card-body">
<table id="dataTable" class="table table-hover align-middle mb-0" style="width:100%">
<thead>
<tr>
<th class="text-center" style="width: 60px;">排名</th>
<th style="width: 100px;">商品 ID</th>
<th style="width: 100px;">商品編號</th>
<th style="width: 25%;">商品名稱</th>
{% if cols.brand %}<th>品牌</th>{% endif %}
{% if cols.vendor %}<th>廠商名稱</th>{% endif %}
{% if cols.cat %}<th>商品館 (分類)</th>{% endif %}
{% if cols.cat %}<th>分類</th>{% endif %}
{% if cols.qty %}<th>平均單價</th>{% endif %}
{% if cols.cost or cols.profit %}<th>毛利率</th>{% endif %}
{% if cols.return_qty %}<th>退貨率</th>{% endif %}
@@ -882,6 +874,6 @@
</script>
{% endif %}
<!-- Page logicfilters / dropdown search / charts / DataTable / Flatpickr 初始化 -->
<!-- Page logicfilters / dropdown search / charts / Flatpickr 初始化 -->
<script src="{{ url_for('static', filename='js/page-sales-analysis.js') }}"></script>
{% endblock %}

View File

@@ -633,34 +633,47 @@ def test_ai_recommend_uses_v2_shell_and_runtime_category_data():
assert "{% for category in product_categories[:4] %}" in template
assert "quickWebSearch({{ category|tojson }})" in template
assert "quickWebSearch('保濕面膜')" not in template
assert "fetch('/api/ai/generate_copy'" in template
assert "fetch('/api/ai/web_search'" in template
assert "fetch('/api/ai/product_insights'" in template
assert "fetch('/api/ai/gemini_usage?days=30')" in template
assert "fetch('/api/ai/generate_copy'" in page_js
assert "fetch('/api/ai/web_search'" in page_js
assert "fetch('/api/ai/product_insights'" in page_js
assert "fetch('/api/ai/gemini_usage?days=30')" in page_js
assert "mock" not in template.lower()
assert "假商品" not in template
assert "Ollama 主路徑" in template
assert "Gemini 備援" in template
assert "Gemini 備援(系統自動,不可手動選)" in template
assert "disabled>☁️ Gemini 備援" in template
assert "權杖:" in template
assert "Ollama 主路徑" in page_js
assert "Gemini 備援" in page_js
assert "搜尋失敗:" in page_js
assert "分析失敗:" in page_js
assert "PChome 銷售建議" in template
assert "銷售動作生成" in template
assert "建議目的" in template
assert "處理順序" in template
assert "建議引擎" in template
assert "備援守門" in template
assert "ar-engine-settings" in template
assert "整理訊號" in page_js
assert "商品判斷暫時不可用" in page_js
forbidden_visible_text = [
"🖥️ Ollama (本地)",
"☁️ Gemini (雲端)",
"Ollama 主路徑",
"Gemini 備援",
"Gemini 備援(系統自動,不可手動選)",
"disabled>☁️ Gemini 備援",
"AI 模型主路徑",
"AI 路徑",
"分析模型",
"Web Search 功能",
"渲染 Web Search",
"整合 Web Search",
"Token:",
"權杖:",
"費用:",
"費用:",
"生成失敗:",
"生成失敗:",
"發生錯誤:",
"發生錯誤:",
"搜尋失敗:",
"搜尋失敗:",
"分析失敗:",
"分析失敗:",
]
combined = template + "\n" + page_js
for marker in forbidden_visible_text:

View File

@@ -684,14 +684,14 @@ def test_primary_pages_use_growth_outcome_copy_instead_of_feature_explaining():
expected = {
"templates/daily_sales.html": "找出下滑與價差壓力",
"templates/ai_recommend.html": "把價差、商品證據與趨勢轉成可追蹤的銷售建議",
"templates/ai_recommend.html": "把價差、商品證據與趨勢轉成主推、調價、補比價動作",
"templates/auto_import_index.html": "保持 PChome 業績新鮮",
"templates/price_comparison.html": "確認同款、判斷價差、決定下一步",
"templates/vendor_stockout_index_v2.html": "避免主推商品斷貨",
"templates/monthly_summary_analysis.html": "判斷成長、毛利與品類結構",
"templates/dashboard_v2.html": "先看業績,再決定調價、曝光與組合",
"templates/edm_dashboard_v2.html": "用活動價格異動找主推、補貨與曝光機會",
"templates/sales_analysis.html": "用分類、品牌與毛利找出 PChome 成長槓桿",
"templates/sales_analysis.html": "用分類、品牌與毛利找出主推、守價與補資料順序",
"templates/growth_analysis.html": "用月趨勢評估成長缺口、價差壓力與毛利品質",
"templates/vendor_stockout_import_v2.html": "補齊缺貨資料,先保住主推商品供貨",
"templates/vendor_stockout_list_v2.html": "先看待發送與失敗,避免主推商品斷貨拖累業績",
@@ -957,7 +957,7 @@ def test_visible_operations_pages_hide_internal_runtime_terms():
from pathlib import Path
expected = {
"templates/ai_recommend.html": ["分析模型", "用量"],
"templates/ai_recommend.html": ["銷售動作生成", "建議目的", "處理順序"],
"templates/vendor_stockout_index_v2.html": ["先匯入缺貨批次", "供貨風險"],
"templates/dashboard_v2.html": ["尚無挑品建議", "先累積 PChome 比價與挑品資料"],
"templates/daily_sales.html": ["左右滑動看業績趨勢", "左右滑動看分類明細"],
@@ -971,7 +971,7 @@ def test_visible_operations_pages_hide_internal_runtime_terms():
"templates/cicd_dashboard.html": ["部署流程", "部署歷史", "修復部署", "查看部署紀錄"],
}
forbidden_by_path = {
"templates/ai_recommend.html": ["權杖:", "AI 模型"],
"templates/ai_recommend.html": ["權杖:", "AI 模型", "分析模型", "AI 路徑", "Gemini 備援", "Ollama 主路徑"],
"templates/vendor_stockout_index_v2.html": ["資料庫目前沒有缺貨資料"],
"templates/system_settings.html": ["資料表:", "資料表:", "自動建表", "匯入並建立通用資料表"],
"templates/notification_templates.html": ["模板代碼", "<code>${t.code}</code>", "CI/CD Pipeline SUCCESS"],

View File

@@ -23,6 +23,46 @@
display: flex; align-items: center; gap: var(--momo-space-1, 8px);
}
/* ── Decision strip ────────────────────────────────── */
.ai-recommend-page .ar-command-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
margin: var(--momo-space-3, 16px) 0;
padding: 0.65rem 0.8rem;
border: 1px solid var(--momo-border-subtle);
border-radius: var(--momo-radius-md, 8px);
background: color-mix(in srgb, var(--momo-page-accent) 7%, var(--momo-surface));
}
.ai-recommend-page .ar-command-strip__label {
color: var(--momo-text-tertiary);
font-size: 0.76rem;
font-weight: 800;
}
.ai-recommend-page .ar-command-strip strong {
display: inline-flex;
align-items: center;
gap: 0.38rem;
min-height: 2rem;
padding: 0.34rem 0.58rem;
border: 1px solid color-mix(in srgb, var(--momo-page-accent) 22%, var(--momo-border-subtle));
border-radius: 999px;
background: var(--momo-surface);
color: var(--momo-text-strong);
font-size: 0.82rem;
font-weight: 800;
}
.ai-recommend-page .ar-command-strip strong i { color: var(--momo-page-accent); }
.ai-recommend-page .ar-command-strip__link {
margin-left: auto;
color: var(--momo-page-accent);
font-size: 0.82rem;
font-weight: 800;
text-decoration: none;
}
.ai-recommend-page .ar-command-strip__link:hover { text-decoration: underline; }
/* ── Status pills ──────────────────────────────────── */
.ai-recommend-page .ar-status {
display: inline-flex; align-items: center; gap: 4px;
@@ -72,6 +112,10 @@
}
.ai-recommend-page .ar-card__step { opacity: 0.85; }
.ai-recommend-page .ar-engine-settings {
display: none !important;
}
/* ── Card variant accent borders ──────────────────── */
.ai-recommend-page .ar-card--gen { border-color: var(--momo-text-strong) !important; }
.ai-recommend-page .ar-card--result { border-color: var(--momo-warm-olive, #6f7a4a) !important; }
@@ -179,3 +223,16 @@
.ar-loading-overlay .spinner-border-lg {
width: 3rem; height: 3rem; border-width: 0.3em;
}
@media (max-width: 767.98px) {
.ai-recommend-page .ar-hero__actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.ai-recommend-page .ar-command-strip__link {
width: 100%;
margin-left: 0;
}
}

View File

@@ -73,6 +73,39 @@
}
.ms-page-head__sub code { background: transparent; color: inherit; }
/* ── 2.1 Decision strip ─────────────────────────────── */
.ms-action-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
margin: var(--momo-space-3, 16px) 0;
padding: 0.65rem 0.8rem;
border: 1px solid var(--momo-border-subtle);
border-radius: var(--ms-card-radius);
background: var(--momo-surface-raised);
box-shadow: var(--momo-shadow-soft);
}
.ms-action-strip__label {
color: var(--momo-text-secondary);
font-size: 0.76rem;
font-weight: 800;
}
.ms-action-strip strong {
display: inline-flex;
align-items: center;
gap: 0.38rem;
min-height: 2rem;
padding: 0.34rem 0.58rem;
border: 1px solid color-mix(in srgb, var(--momo-warm-caramel) 24%, var(--momo-border-subtle));
border-radius: 999px;
background: rgba(250, 246, 238, 0.78);
color: var(--momo-text-primary);
font-size: 0.82rem;
font-weight: 800;
}
.ms-action-strip strong i { color: var(--momo-warm-caramel); }
/* ── 3. Tag / pill ────────────────────────────────────── */
.ms-tag {
display: inline-flex; align-items: center; gap: 6px;
@@ -306,6 +339,11 @@
gap: var(--momo-space-3);
}
.ms-action-strip strong {
width: 100%;
justify-content: flex-start;
}
.ms-page-head {
grid-template-columns: 1fr;
padding: var(--momo-space-4);

View File

@@ -42,6 +42,45 @@
letter-spacing: 0;
}
.sa-command-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
margin: -0.35rem 0 1.1rem;
padding: 0.65rem 0.8rem;
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: color-mix(in srgb, var(--momo-page-accent) 7%, var(--momo-surface));
}
.sa-command-strip__label {
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 800;
}
.sa-command-strip strong {
display: inline-flex;
align-items: center;
gap: 0.38rem;
min-height: 2rem;
padding: 0.34rem 0.58rem;
border: 1px solid color-mix(in srgb, var(--momo-page-accent) 22%, var(--momo-border-subtle));
border-radius: 999px;
background: var(--momo-surface);
color: var(--momo-text-strong);
font-size: 0.82rem;
font-weight: 800;
}
.sa-command-strip strong i { color: var(--momo-page-accent); }
.sa-command-strip__link {
margin-left: auto;
color: var(--momo-page-accent);
font-size: 0.82rem;
font-weight: 800;
text-decoration: none;
}
.sa-command-strip__link:hover { text-decoration: underline; }
.sa-tag {
display: inline-flex;
align-items: center;
@@ -536,4 +575,5 @@
.sa-insights__col { border-right: none; border-bottom: 1px dashed var(--momo-border-subtle); }
.sa-insights__col:last-child { border-bottom: none; }
.sa-filter-card--sticky { position: static; }
.sa-command-strip__link { width: 100%; margin-left: 0; }
}

View File

@@ -236,21 +236,18 @@
} catch { return ''; }
}
// ====== AI 引擎切換相關 ======
// ====== 建議引擎切換相關 ======
// AI 引擎切換處理
// 建議引擎切換處理
function onProviderChange() {
const provider = document.getElementById('aiProvider').value;
const isGemini = provider === 'gemini';
// 切換模型選擇器顯示
document.getElementById('ollamaModelSelect').style.display = isGemini ? 'none' : 'block';
document.getElementById('geminiModelSelect').style.display = isGemini ? 'block' : 'none';
// 顯示/隱藏 Gemini 使用量面板
document.getElementById('geminiUsagePanel').style.display = isGemini ? 'block' : 'none';
// 如果選擇 Gemini載入使用量
if (isGemini) {
loadGeminiUsage();
}
@@ -263,7 +260,7 @@
.then(data => {
if (data.success) {
const summary = data.data.summary;
document.getElementById('geminiMonthlyCost').textContent = '$' + summary.total_cost_usd.toFixed(4);
document.getElementById('fallbackMonthlySpend').textContent = '$' + summary.total_cost_usd.toFixed(4);
document.getElementById('geminiRequestCount').textContent = summary.total_requests.toLocaleString();
document.getElementById('geminiTokenUsage').textContent = summary.total_tokens.toLocaleString();
}
@@ -273,7 +270,7 @@
});
}
// 首屏先渲染,AI 狀態載入後再更新,避免健康檢查阻塞頁面 TTFB
// 首屏先渲染,建議引擎狀態載入後再更新,避免健康檢查阻塞頁面 TTFB
function refreshAIStatus() {
fetch('/api/ai/status')
.then(r => {
@@ -283,11 +280,11 @@
.then(data => {
if (!data.success || !data.data) return;
const status = data.data;
updateAIStatusBadge('ollamaStatus', 'fas fa-server', 'AI 模型主路徑', status.ollama?.connected, 'ar-status--ok');
updateAIStatusBadge('geminiStatus', 'fab fa-google', 'Gemini 備援', status.gemini?.connected, 'ar-status--info');
updateAIStatusBadge('ollamaStatus', 'fas fa-wand-magic-sparkles', '建議引擎', status.ollama?.connected, 'ar-status--ok');
updateAIStatusBadge('geminiStatus', 'fas fa-shield-alt', '備援守門', status.gemini?.connected, 'ar-status--info');
updateOllamaModels(status.ollama?.available_models || []);
})
.catch(e => console.warn('AI 狀態刷新失敗:', e));
.catch(e => console.warn('建議引擎狀態刷新未完成:', e));
}
function updateAIStatusBadge(id, iconClass, label, connected, okClass) {
@@ -295,7 +292,7 @@
if (!el) return;
el.classList.remove('ar-status--ok', 'ar-status--info', 'ar-status--off');
el.classList.add(connected ? okClass : 'ar-status--off');
el.innerHTML = `<i class="${iconClass}"></i> ${label} ${connected ? '' : ''}`;
el.innerHTML = `<i class="${iconClass}"></i> ${label} ${connected ? '可用' : '待確認'}`;
}
function updateOllamaModels(models) {
@@ -309,9 +306,8 @@
}).join('');
}
// 初始化 AI 引擎選擇(頁面載入時)
// 初始化建議引擎選擇(頁面載入時)
function initAIProvider() {
// 確保 Ollama 為預設
const provider = document.getElementById('aiProvider').value;
onProviderChange();
}
@@ -374,14 +370,14 @@
const btn = document.getElementById('generateBtn');
const originalBtnText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>AI 正在生成中...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>正在整理建議...';
btn.classList.add('btn-secondary');
btn.classList.remove('btn-primary');
// 顯示全螢幕載入動畫
showLoading('AI 正在生成完整文案套組...');
showLoading('正在產生銷售建議...');
// 取得 AI 設定
// 取得幕後建議設定
const provider = document.getElementById('aiProvider').value;
const model = provider === 'gemini'
? document.getElementById('geminiModelSelect').value
@@ -422,14 +418,10 @@
document.getElementById('generatedCopy').innerHTML = formattedCopy;
// 組合元資料顯示
let metaHtml = `<i class="fas fa-robot me-1"></i>${data.data.provider === 'gemini' ? 'Gemini 備援' : 'AI 模型主路徑'}${data.data.model}`;
metaHtml += ` | <i class="fas fa-clock me-1"></i>耗時:${data.data.duration}`;
let metaHtml = `<i class="fas fa-check-circle me-1"></i>建議已完成`;
metaHtml += ` | <i class="fas fa-clock me-1"></i>分析耗時:${data.data.duration}`;
// 如果是 Gemini顯示費用
if (data.data.provider === 'gemini' && data.data.cost) {
metaHtml += ` | <i class="fas fa-coins me-1"></i>費用:$${data.data.cost.total.toFixed(4)}`;
metaHtml += ` | 權杖:${data.data.tokens.total}`;
// 刷新使用量面板
loadGeminiUsage();
}
@@ -437,7 +429,7 @@
document.getElementById('resultArea').style.display = 'block';
document.getElementById('resultArea').scrollIntoView({ behavior: 'smooth' });
} else {
alert('生成失敗' + data.error);
alert('建議沒有完成' + data.error);
}
})
.catch(e => {
@@ -447,7 +439,7 @@
btn.innerHTML = originalBtnText;
btn.classList.remove('btn-secondary');
btn.classList.add('btn-primary');
alert('發生錯誤' + e.message);
alert('建議暫時無法產生' + e.message);
});
}
@@ -516,7 +508,7 @@
doWebSearch();
}
// AI 網路搜尋
// 市場訊號搜尋
function doWebSearch() {
const query = document.getElementById('webSearchQuery').value.trim();
if (!query) {
@@ -532,11 +524,11 @@
// 更新按鈕狀態
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>搜尋中...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
// 顯示結果區
resultArea.style.display = 'block';
contentArea.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div><br><small class="text-muted mt-2 d-block">AI 正在分析市場資訊...</small><small class="text-muted">(可能需要 30-60 秒)</small></div>';
contentArea.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div><br><small class="text-muted mt-2 d-block">正在整理市場訊號...</small><small class="text-muted">約需 30-60 秒</small></div>';
// 設定前端超時 (3 分鐘)
const controller = new AbortController();
@@ -565,27 +557,27 @@
})
.then(data => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-brain me-1"></i>AI 搜尋';
btn.innerHTML = '<i class="fas fa-brain me-1"></i>整理訊號';
if (data.success) {
renderWebSearchResult(data.data);
} else {
contentArea.innerHTML = `<div class="alert alert-danger py-2 mb-0"><i class="fas fa-exclamation-triangle me-1"></i>${data.error}</div>`;
contentArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-exclamation-triangle me-1"></i>市場訊號暫時不可用,請稍後重試。</div>`;
}
})
.catch(e => {
clearTimeout(timeoutId);
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-brain me-1"></i>AI 搜尋';
btn.innerHTML = '<i class="fas fa-brain me-1"></i>整理訊號';
if (e.name === 'AbortError') {
contentArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>AI 伺服器回應較慢,請稍後再試。</div>`;
contentArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>市場訊號整理較慢,請稍後再試。</div>`;
} else {
contentArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">搜尋失敗:${e.message}</div>`;
contentArea.innerHTML = `<div class="alert alert-warning py-2 mb-0">市場訊號暫時不可用,請稍後重試。</div>`;
}
});
}
// 渲染網路搜尋結果 - 卡片式顯示
// 渲染市場訊號結果
function renderWebSearchResult(data) {
const contentArea = document.getElementById('webSearchContent');
let html = '';
@@ -594,12 +586,11 @@
if (data.parsed) {
const p = data.parsed;
// 摘要卡片
if (p.summary) {
html += `
<div class="card border-primary mb-2">
<div class="card-header ar-card__head ar-card__head--soft py-2">
<h6 class="mb-0 small"><i class="fas fa-quote-left text-primary me-1"></i>AI 分析摘要</h6>
<h6 class="mb-0 small"><i class="fas fa-quote-left text-primary me-1"></i>市場摘要</h6>
</div>
<div class="card-body py-2">
<p class="mb-0 small">${escapeHtml(p.summary)}</p>
@@ -607,12 +598,11 @@
</div>`;
}
// 分析結果卡片
if (p.results && p.results.length > 0) {
html += `
<div class="card border-success mb-2">
<div class="card-header bg-success bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-list-check text-success me-1"></i>分析結果 (${p.results.length})</h6>
<h6 class="mb-0 small"><i class="fas fa-list-check text-success me-1"></i>可用線索 (${p.results.length})</h6>
</div>
<div class="card-body py-2">
<div class="list-group list-group-flush">`;
@@ -634,11 +624,9 @@
</div>`;
}
// 洞察與建議並排顯示
if ((p.insights && p.insights.length > 0) || (p.recommended_actions && p.recommended_actions.length > 0)) {
html += '<div class="row g-2">';
// 洞察卡片
if (p.insights && p.insights.length > 0) {
const colClass = (p.recommended_actions && p.recommended_actions.length > 0) ? 'col-md-6' : 'col-12';
html += `
@@ -656,14 +644,13 @@
</div>`;
}
// 建議行動卡片
if (p.recommended_actions && p.recommended_actions.length > 0) {
const colClass = (p.insights && p.insights.length > 0) ? 'col-md-6' : 'col-12';
html += `
<div class="${colClass}">
<div class="card border-info h-100">
<div class="card-header bg-info bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-tasks text-info me-1"></i>建議行動</h6>
<h6 class="mb-0 small"><i class="fas fa-tasks text-info me-1"></i>可採取動作</h6>
</div>
<div class="card-body py-2">
<div class="d-flex flex-wrap gap-1">
@@ -677,11 +664,10 @@
html += '</div>';
}
} else {
// 顯示原始內容
html = `
<div class="card border-secondary">
<div class="card-header bg-secondary bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-file-alt text-secondary me-1"></i>搜尋結果</h6>
<h6 class="mb-0 small"><i class="fas fa-file-alt text-secondary me-1"></i>市場訊號</h6>
</div>
<div class="card-body py-2">
<pre class="mb-0 small" style="white-space: pre-wrap; word-break: break-word;">${escapeHtml(data.raw_content)}</pre>
@@ -689,12 +675,10 @@
</div>`;
}
// 底部資訊
html += `
<div class="text-end mt-2">
<small class="text-muted">
<i class="fas fa-robot me-1"></i>${data.model || 'AI'} |
<i class="fas fa-clock me-1"></i>${data.duration || '?'}
<i class="fas fa-clock me-1"></i>完成於 ${data.duration || '?'}
</small>
</div>`;
contentArea.innerHTML = html;
@@ -715,7 +699,7 @@
// 更新按鈕狀態
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>搜尋中...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
// 顯示結果區
placeholder.style.display = 'none';
@@ -726,8 +710,8 @@
<div class="progress mb-2" style="height: 4px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-warning" id="insightProgress" style="width: 20%"></div>
</div>
<p class="text-muted mb-1" id="insightStatus">步驟 1/2搜尋網路最新資訊...</p>
<small class="text-muted">(整體約需 60-90 秒)</small>
<p class="text-muted mb-1" id="insightStatus">步驟 1/2整理外部訊號...</p>
<small class="text-muted">約需 60-90 秒</small>
</div>`;
// 設定前端超時 (4 分鐘,因為需要兩步)
@@ -739,7 +723,7 @@
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({
query: `${productName} 市場分析 競品比較 價格 評價 2024`,
query: `${productName} 市場分析 競品比較 價格 評價 ${new Date().getFullYear()}`,
search_type: 'shopping',
num_results: 5
}),
@@ -756,23 +740,23 @@
.then(searchData => {
// 更新進度
document.getElementById('insightProgress').style.width = '60%';
document.getElementById('insightStatus').textContent = '步驟 2/2AI 深度分析中...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>分析中...';
document.getElementById('insightStatus').textContent = '步驟 2/2產生下一步...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>判斷中...';
// 準備搜尋結果摘要
// 準備外部訊號摘要
let webContext = '';
if (searchData.success && searchData.data) {
const parsed = searchData.data.parsed;
if (parsed) {
webContext = `\n\n網路搜尋結果摘要】\n${parsed.summary || ''}\n`;
webContext = `\n\n外部訊號摘要】\n${parsed.summary || ''}\n`;
if (parsed.results && parsed.results.length > 0) {
webContext += '\n相關訊:\n';
webContext += '\n相關訊\n';
parsed.results.slice(0, 3).forEach((r, i) => {
webContext += `${i+1}. ${r.title}: ${r.description}\n`;
});
}
} else if (searchData.data.raw_content) {
webContext = `\n\n網路搜尋結果\n${searchData.data.raw_content.substring(0, 500)}`;
webContext = `\n\n外部訊號\n${searchData.data.raw_content.substring(0, 500)}`;
}
}
@@ -784,7 +768,7 @@
product_name: productName,
include_competitors: true,
include_trends: true,
web_context: webContext // 傳入網路搜尋結果
web_context: webContext
}),
signal: controller.signal
});
@@ -796,47 +780,46 @@
})
.then(data => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-search-dollar me-1"></i>分析商品';
btn.innerHTML = '<i class="fas fa-search-dollar me-1"></i>判斷下一步';
if (data.success) {
renderProductInsights(data.data, productName);
} else {
resultArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">${data.error}</div>`;
resultArea.innerHTML = `<div class="alert alert-warning py-2 mb-0">商品判斷暫時不可用,請稍後重試。</div>`;
}
})
.catch(e => {
clearTimeout(timeoutId);
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-search-dollar me-1"></i>分析商品';
btn.innerHTML = '<i class="fas fa-search-dollar me-1"></i>判斷下一步';
if (e.name === 'AbortError') {
resultArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>AI 伺服器回應較慢,請稍後再試。</div>`;
resultArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>商品判斷較慢,請稍後再試。</div>`;
} else {
resultArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">分析失敗:${e.message}</div>`;
resultArea.innerHTML = `<div class="alert alert-warning py-2 mb-0">商品判斷暫時不可用,請稍後重試。</div>`;
}
});
}
// 渲染商品洞察結果 - 完整顯示版本
// 渲染商品洞察結果
function renderProductInsights(data, productName) {
const resultArea = document.getElementById('productInsightsResult');
let html = '';
// 標題
html += `<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0"><i class="fas fa-chart-pie text-warning me-2"></i>${escapeHtml(productName || '商品')} 市場分析</h6>
<small class="text-muted"><i class="fas fa-globe me-1"></i>含網路即時資訊</small>
<h6 class="mb-0"><i class="fas fa-chart-pie text-warning me-2"></i>${escapeHtml(productName || '商品')} 下一步判斷</h6>
<small class="text-muted"><i class="fas fa-globe me-1"></i>含外部訊號</small>
</div>`;
if (data.insights) {
const ins = data.insights;
// 市場定位 - 卡片樣式
if (ins.market_position) {
const mp = ins.market_position;
html += `<div class="card mb-3 border-primary">
<div class="card-body py-2">
<div class="d-flex align-items-center mb-2">
<span class="badge ar-semantic-badge ar-semantic-badge--primary me-2"><i class="fas fa-crosshairs me-1"></i>市場定位</span>
<span class="badge ar-semantic-badge ar-semantic-badge--primary me-2"><i class="fas fa-crosshairs me-1"></i>商品定位</span>
${mp.price_range ? `<span class="badge bg-success">${escapeHtml(mp.price_range)}</span>` : ''}
</div>
<p class="mb-0 small">${escapeHtml(mp.positioning || mp.target_audience || mp.description || '')}</p>
@@ -844,16 +827,15 @@
</div>`;
}
// 競品分析 - 表格樣式
if (ins.competitors && ins.competitors.length > 0) {
html += `<div class="card mb-3 border-warning">
<div class="card-header py-2 bg-warning bg-opacity-10">
<span class="badge bg-warning text-dark"><i class="fas fa-users me-1"></i>競品分析</span>
<span class="badge bg-warning text-dark"><i class="fas fa-users me-1"></i>競品重點</span>
</div>
<div class="card-body py-2">
<div class="table-responsive">
<table class="table table-sm table-borderless mb-0 small">
<thead><tr><th>競品</th><th class="text-success">優勢</th><th class="text-danger">劣勢</th></tr></thead>
<thead><tr><th>競品</th><th class="text-success">可借鏡</th><th class="text-danger">風險</th></tr></thead>
<tbody>`;
ins.competitors.slice(0, 5).forEach(c => {
html += `<tr>
@@ -865,7 +847,6 @@
html += `</tbody></table></div></div></div>`;
}
// 市場趨勢
if (ins.trends) {
html += `<div class="card mb-3 border-info">
<div class="card-body py-2">
@@ -876,11 +857,10 @@
</div>`;
}
// 銷售建議
if (ins.recommendations && ins.recommendations.length > 0) {
html += `<div class="card mb-3 border-success">
<div class="card-body py-2">
<span class="badge bg-success mb-2"><i class="fas fa-lightbulb me-1"></i>銷售建議</span>
<span class="badge bg-success mb-2"><i class="fas fa-lightbulb me-1"></i>下一步</span>
<ul class="mb-0 ps-3 small">`;
ins.recommendations.forEach(r => {
html += `<li>${escapeHtml(r)}</li>`;
@@ -888,10 +868,9 @@
html += `</ul></div></div>`;
}
// 行銷關鍵字 - 可點擊加入
if (ins.keywords && ins.keywords.length > 0) {
html += `<div class="mb-2">
<span class="badge bg-secondary me-2"><i class="fas fa-tags me-1"></i>行銷關鍵字</span>
<span class="badge bg-secondary me-2"><i class="fas fa-tags me-1"></i>可用關鍵字</span>
<small class="text-muted">點擊可加入文案</small>
<div class="mt-2">`;
ins.keywords.forEach(k => {
@@ -900,7 +879,6 @@
html += `</div></div>`;
}
} else if (data.raw_content) {
// 原始內容顯示
html += `<div class="card">
<div class="card-body py-2">
<div class="small" style="white-space: pre-wrap; line-height: 1.6;">${escapeHtml(data.raw_content)}</div>
@@ -908,14 +886,12 @@
</div>`;
}
// 底部資訊
html += `<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
<small class="text-muted">
<i class="fas fa-robot me-1"></i>${data.model || 'AI'} |
<i class="fas fa-clock me-1"></i>${data.duration || '?'}
<i class="fas fa-clock me-1"></i>完成於 ${data.duration || '?'}
</small>
<button class="btn btn-sm btn-outline-secondary" onclick="doProductInsights()">
<i class="fas fa-redo me-1"></i>重新分析
<i class="fas fa-redo me-1"></i>重新判斷
</button>
</div>`;
@@ -1005,7 +981,7 @@
'dcard': '<span class="badge ar-source-badge ar-source-badge--dcard me-1">Dcard</span>',
'google_news': '<span class="badge ar-source-badge ar-source-badge--google-news me-1">新聞</span>',
'youtube': '<span class="badge ar-source-badge ar-source-badge--youtube me-1">YT</span>',
'ollama_web_search': '<span class="badge ar-source-badge ar-source-badge--ai me-1">AI</span>'
'ollama_web_search': '<span class="badge ar-source-badge ar-source-badge--ai me-1">搜尋</span>'
};
const html = records.map(r => `
@@ -1033,7 +1009,7 @@
// 初始化
document.addEventListener('DOMContentLoaded', function() {
initAIProvider(); // 初始化 AI 引擎選擇
initAIProvider();
refreshAIStatus();
renderUpcomingHolidays();
refreshTrends(); // 載入即時趨勢(預設頁籤)