feat: align analysis pages to growth workflow
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 模型、權杖、資料庫、欄位、英文指標縮寫或內部錯誤作為使用者主訊息。 |
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>開架保養 & 臉部清潔(合併業績)</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>彩妝/指彩 & 精油擴香(合併業績)</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>私密保養 & 嬰幼洗沐(合併業績)</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>
|
||||
|
||||
@@ -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 logic:filters / dropdown search / charts / DataTable / Flatpickr 初始化 -->
|
||||
<!-- Page logic:filters / dropdown search / charts / Flatpickr 初始化 -->
|
||||
<script src="{{ url_for('static', filename='js/page-sales-analysis.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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/2:AI 深度分析中...';
|
||||
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(); // 載入即時趨勢(預設頁籤)
|
||||
|
||||
Reference in New Issue
Block a user