Files
ewoooc/templates/ai_intelligence.html
OoO 56ebba045b
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
V10.612 讓價格參考表優先讀外部報價層
2026-06-16 09:43:49 +08:00

1593 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'ewoooc_base.html' %}
{% block title %}PChome 業績成長自動化作戰系統 · EwoooC{% endblock %}
{% block extra_css %}
<style>
.ai-intel-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.ai-intel-hero {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
align-items: center;
padding: 22px;
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
background:
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.12) 1px, transparent 1px),
linear-gradient(135deg, rgba(242, 178, 90, 0.28), rgba(255, 255, 255, 0.92) 42%, rgba(172, 92, 58, 0.12));
background-size: 18px 18px, auto;
box-shadow: var(--momo-shadow-soft);
}
.ai-intel-hero::after {
content: "";
position: absolute;
inset: auto 20px 18px auto;
width: 132px;
height: 132px;
border: 1px solid rgba(42, 37, 32, 0.12);
border-radius: 50%;
background: repeating-linear-gradient(
90deg,
rgba(42, 37, 32, 0.08) 0,
rgba(42, 37, 32, 0.08) 1px,
transparent 1px,
transparent 8px
);
opacity: 0.72;
pointer-events: none;
}
.ai-intel-title {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: clamp(1.45rem, 2vw, 2.15rem);
font-weight: 800;
letter-spacing: 0;
}
.ai-intel-title i {
color: var(--momo-warm-rust);
}
.ai-intel-badge,
.ai-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(42, 37, 32, 0.14);
border-radius: 999px;
background: rgba(255, 255, 255, 0.68);
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 0.78rem;
font-weight: 800;
padding: 5px 10px;
}
.ai-status-badge.is-success {
border-color: rgba(40, 128, 80, 0.24);
background: rgba(232, 247, 238, 0.88);
color: #216542;
}
.ai-status-badge.is-error {
border-color: rgba(188, 75, 49, 0.26);
background: rgba(255, 241, 237, 0.9);
color: #9b3d2b;
}
.ai-intel-subtitle {
position: relative;
z-index: 1;
margin: 8px 0 0;
color: var(--momo-text-muted);
font-size: 0.93rem;
}
.ai-intel-actions {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
max-width: 620px;
}
.ai-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 38px;
border-radius: 8px;
font-weight: 800;
white-space: nowrap;
}
.ai-action-btn.btn-outline-danger {
color: var(--momo-warm-rust);
border-color: rgba(172, 92, 58, 0.44);
}
.ai-action-btn.btn-outline-primary {
color: var(--momo-accent-strong);
border-color: rgba(42, 37, 32, 0.24);
}
.ai-action-btn.btn-outline-warning {
color: #805313;
border-color: rgba(242, 178, 90, 0.66);
}
.ai-intel-page #kpiRow .card,
.ai-panel {
border: 1px solid var(--momo-border-subtle) !important;
border-radius: 8px;
background: rgba(255, 255, 255, 0.84);
box-shadow: var(--momo-shadow-soft);
}
.ai-intel-page #kpiRow .card-body {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 116px;
}
.ai-intel-page #kpiRow .fs-2 {
color: var(--momo-text-strong) !important;
font-family: var(--momo-font-mono);
font-size: 2rem !important;
line-height: 1.05;
}
.ai-intel-page #kpiRow .small {
color: var(--momo-text-muted) !important;
font-weight: 700;
}
.ai-intel-page #kpiHighRiskCard.border-danger {
background: linear-gradient(160deg, rgba(255, 245, 240, 0.98), rgba(255, 255, 255, 0.9));
border-color: rgba(188, 75, 49, 0.48) !important;
}
.ai-panel .card-header,
.ai-panel .card-footer {
border-color: var(--momo-border-subtle) !important;
background: rgba(255, 255, 255, 0.78) !important;
}
.ai-panel-title {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: 0.95rem;
font-weight: 800;
}
.ai-panel-title i {
color: var(--momo-warm-caramel) !important;
}
.ai-panel .form-select,
.ai-panel .form-control {
border-color: var(--momo-border-subtle);
border-radius: 8px;
color: var(--momo-text-strong);
font-size: 0.82rem;
}
.ai-legend {
border-bottom: 1px solid var(--momo-border-subtle);
}
.ai-table-scroll {
overflow-x: auto;
overflow-y: auto;
max-height: 520px;
}
.ai-intel-page .table {
--bs-table-hover-bg: rgba(242, 178, 90, 0.12);
color: var(--momo-text-strong);
}
.ai-intel-page .table thead th {
border-bottom: 1px solid var(--momo-border-strong);
background: rgba(250, 247, 240, 0.96) !important;
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 800;
}
.ai-intel-page .table tbody td {
border-color: rgba(42, 37, 32, 0.08);
}
.ai-recs-scroll {
overflow-y: auto;
max-height: 568px;
}
.ai-intel-page #aiRecsList > .border {
border-color: var(--momo-border-subtle) !important;
border-radius: 8px !important;
background: rgba(250, 247, 240, 0.54);
}
.growth-ops-grid {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.7fr);
gap: 14px;
}
.ops-flow {
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(255, 255, 255, 0.82);
box-shadow: var(--momo-shadow-soft);
padding: 14px;
}
.ops-flow-head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.ops-flow-note {
color: var(--momo-text-muted);
font-size: 0.78rem;
font-weight: 800;
}
.ops-flow-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.ops-flow-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 10px;
align-items: start;
width: 100%;
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
background: rgba(250, 247, 240, 0.52);
color: var(--momo-text-strong);
padding: 11px;
text-align: left;
transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease;
}
.ops-flow-item:hover,
.ops-flow-item:focus {
border-color: rgba(172, 92, 58, 0.34);
background: rgba(255, 255, 255, 0.86);
transform: translateY(-1px);
}
.ops-flow-step {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 999px;
background: rgba(172, 92, 58, 0.12);
color: var(--momo-warm-rust);
font-family: var(--momo-font-mono);
font-size: 0.76rem;
font-weight: 900;
}
.ops-flow-title {
margin: 0;
color: var(--momo-text-strong);
font-size: 0.86rem;
font-weight: 900;
line-height: 1.25;
}
.ops-flow-copy,
.ops-flow-target {
margin: 4px 0 0;
color: var(--momo-text-muted);
font-size: 0.74rem;
line-height: 1.4;
}
.ops-flow-target {
color: var(--momo-warm-rust);
font-weight: 900;
}
.growth-metric-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.growth-metric {
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(250, 247, 240, 0.62);
padding: 10px;
}
.growth-metric strong {
display: block;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 1.35rem;
line-height: 1.1;
}
.growth-metric span {
color: var(--momo-text-muted);
font-size: 0.75rem;
font-weight: 800;
}
.growth-source-note {
margin-top: 10px;
color: var(--momo-text-muted);
font-size: 0.82rem;
line-height: 1.55;
}
.growth-action-hint {
margin: 10px 0 0;
border: 1px solid rgba(172, 92, 58, 0.16);
border-radius: 8px;
background: rgba(242, 178, 90, 0.14);
color: var(--momo-text-strong);
font-size: 0.82rem;
font-weight: 800;
line-height: 1.5;
padding: 9px 10px;
}
.growth-data-summary {
margin: 8px 0 0;
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 8px;
background: rgba(255, 255, 255, 0.68);
color: var(--momo-text-muted);
font-size: 0.78rem;
line-height: 1.45;
padding: 8px 10px;
}
.growth-source-list {
display: grid;
gap: 8px;
margin-top: 10px;
}
.growth-source-chip {
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
padding: 8px 10px;
}
.growth-source-chip.is-active {
border-color: rgba(42, 134, 96, 0.24);
background: rgba(235, 248, 241, 0.78);
}
.growth-source-name {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--momo-text-strong);
font-size: 0.8rem;
font-weight: 900;
}
.growth-source-status {
border-radius: 999px;
background: rgba(42, 37, 32, 0.08);
color: var(--momo-text-muted);
font-size: 0.68rem;
font-weight: 900;
padding: 3px 7px;
white-space: nowrap;
}
.growth-source-chip.is-active .growth-source-status {
background: rgba(42, 134, 96, 0.14);
color: #1f6d4c;
}
.growth-source-detail {
margin: 5px 0 0;
color: var(--momo-text-muted);
font-size: 0.74rem;
line-height: 1.4;
}
.growth-list {
display: grid;
gap: 8px;
max-height: 292px;
overflow-y: auto;
padding-right: 4px;
}
.growth-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: start;
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.78);
padding: 10px 12px;
}
.growth-item-title {
margin: 0;
color: var(--momo-text-strong);
font-size: 0.9rem;
font-weight: 800;
line-height: 1.35;
}
.growth-item-meta,
.growth-item-reason {
margin: 4px 0 0;
color: var(--momo-text-muted);
font-size: 0.78rem;
line-height: 1.45;
}
.growth-action-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 92px;
border: 1px solid rgba(42, 37, 32, 0.12);
border-radius: 999px;
background: rgba(242, 178, 90, 0.2);
color: var(--momo-text-strong);
font-size: 0.74rem;
font-weight: 900;
padding: 5px 8px;
text-align: center;
white-space: nowrap;
}
.offer-dryrun-grid {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.55fr);
gap: 14px;
}
.offer-dryrun-field {
display: grid;
gap: 8px;
}
.offer-dryrun-field label {
color: var(--momo-text-strong);
font-size: 0.78rem;
font-weight: 900;
}
.offer-dryrun-field textarea {
min-height: 132px;
resize: vertical;
border-color: var(--momo-border-subtle);
border-radius: 8px;
font-size: 0.8rem;
}
.offer-dryrun-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.offer-dryrun-result {
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.76);
min-height: 216px;
padding: 10px;
}
.offer-dryrun-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
border-bottom: 1px solid rgba(42, 37, 32, 0.08);
padding: 8px 0;
}
.offer-dryrun-row:last-child {
border-bottom: 0;
}
.offer-dryrun-title {
margin: 0;
color: var(--momo-text-strong);
font-size: 0.82rem;
font-weight: 900;
line-height: 1.35;
}
.offer-dryrun-meta,
.offer-dryrun-reason {
margin: 3px 0 0;
color: var(--momo-text-muted);
font-size: 0.74rem;
line-height: 1.4;
}
.offer-status-pill {
align-self: start;
border-radius: 999px;
background: rgba(42, 37, 32, 0.08);
color: var(--momo-text-muted);
font-size: 0.68rem;
font-weight: 900;
padding: 4px 8px;
white-space: nowrap;
}
.offer-status-pill.is-ready {
background: rgba(42, 134, 96, 0.14);
color: #1f6d4c;
}
.offer-status-pill.is-blocked {
background: rgba(188, 78, 67, 0.14);
color: #94372d;
}
@media (max-width: 992px) {
.ai-intel-hero {
grid-template-columns: 1fr;
}
.ai-intel-actions {
justify-content: flex-start;
max-width: none;
}
}
@media (max-width: 576px) {
.ai-intel-hero {
padding: 18px;
}
.ai-action-btn {
width: 100%;
}
}
@media (max-width: 768px) {
.ai-panel .card-header {
align-items: flex-start !important;
flex-direction: column;
gap: 10px;
}
.ai-panel .card-header > .d-flex {
display: grid !important;
grid-template-columns: 1fr;
width: 100%;
}
.ai-panel .card-header .form-select,
.ai-panel .card-header .form-control {
width: 100% !important;
}
.ai-table-scroll {
max-height: none;
overflow: visible;
}
#competitorTable,
#competitorTable tbody,
#competitorTable tr,
#competitorTable td {
display: block;
width: 100% !important;
}
#competitorTable thead {
display: none;
}
#competitorTable tr {
padding: 0.85rem 0.95rem;
border-top: 1px solid rgba(42, 37, 32, 0.08);
}
#competitorTable tr:first-child {
border-top: 0;
}
#competitorTable td {
display: grid;
grid-template-columns: 5.8rem minmax(0, 1fr);
gap: 0.65rem;
align-items: start;
padding: 0.36rem 0 !important;
border: 0 !important;
text-align: left !important;
overflow-wrap: anywhere;
white-space: normal;
}
#competitorTable td::before {
color: var(--momo-text-muted);
font-family: var(--momo-font-mono);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
#competitorTable td[colspan] {
display: block;
}
#competitorTable td[colspan]::before {
content: none;
display: none;
}
#competitorTable td:nth-child(1)::before { content: "商品"; }
#competitorTable td:nth-child(2)::before { content: "MOMO"; }
#competitorTable td:nth-child(3)::before { content: "PChome"; }
#competitorTable td:nth-child(4)::before { content: "價差"; }
#competitorTable td:nth-child(5)::before { content: "狀態"; }
#competitorTable td:nth-child(6)::before { content: "可信度"; }
#competitorTable td:nth-child(7)::before { content: "更新"; }
.ai-panel .card-footer {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.growth-ops-grid,
.offer-dryrun-grid,
.growth-metric-row,
.ops-flow-grid {
grid-template-columns: 1fr;
}
.growth-item,
.offer-dryrun-row {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<div class="ai-intel-page">
<!-- ── 頁首 ── -->
<section class="ai-intel-hero">
<div>
<h1 class="ai-intel-title">
<i class="fas fa-brain"></i>
PChome 業績成長自動化作戰系統
<span class="ai-intel-badge">AI 競情中樞</span>
</h1>
<p class="ai-intel-subtitle">把 PChome 後台業績、MOMO 外部價格參考與商品對應狀態整理成每天可處理的作戰清單。</p>
</div>
<div class="ai-intel-actions">
<span id="lastUpdateBadge" class="ai-status-badge">
<i class="fas fa-sync me-1"></i>載入中...
</span>
<button class="btn btn-outline-danger btn-sm ai-action-btn" id="btnTrigger" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>整理建議
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnPickList" onclick="generatePickList()" title="依目前業績與比價資料整理商品處理清單">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生作戰商品
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="btnBackfill" onclick="backfillPchomeMatches()" title="替還不能比價的 PChome 商品尋找 MOMO 參考">
<i class="fas fa-magnifying-glass-chart me-1"></i>補商品對應
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard()" title="重新載入畫面資料">
<i class="fas fa-redo me-1"></i>重新整理
</button>
</div>
</section>
<!-- ── 今日作戰入口 ── -->
<section class="ops-flow" aria-label="今日作戰入口">
<div class="ops-flow-head">
<span class="ai-panel-title">
<i class="fas fa-route"></i>今日作戰入口
</span>
<span class="ops-flow-note">依清單、對應、比價、備援資料的順序處理</span>
</div>
<div class="ops-flow-grid">
<button type="button" class="ops-flow-item" onclick="scrollToPanel('growthOpsPanel')">
<span class="ops-flow-step">1</span>
<span>
<span class="ops-flow-title">看作戰清單</span>
<span class="ops-flow-copy d-block">先看高業績商品與建議動作。</span>
<span class="ops-flow-target d-block">PChome 成長作戰</span>
</span>
</button>
<button type="button" class="ops-flow-item" onclick="backfillPchomeMatches()">
<span class="ops-flow-step">2</span>
<span>
<span class="ops-flow-title">補商品對應</span>
<span class="ops-flow-copy d-block">待補對應太多時,先接上外部參考價。</span>
<span class="ops-flow-target d-block">啟動補商品對應</span>
</span>
</button>
<button type="button" class="ops-flow-item" onclick="scrollToPanel('externalPricePanel')">
<span class="ops-flow-step">3</span>
<span>
<span class="ops-flow-title">看可比價商品</span>
<span class="ops-flow-copy d-block">只看已確認同款的 MOMO 價格參考。</span>
<span class="ops-flow-target d-block">MOMO 外部價格參考</span>
</span>
</button>
<button type="button" class="ops-flow-item" onclick="scrollToPanel('externalOfferDryRunPanel')">
<span class="ops-flow-step">4</span>
<span>
<span class="ops-flow-title">預檢備援資料</span>
<span class="ops-flow-copy d-block">自動來源缺資料時,再檢查 CSV 可不可用。</span>
<span class="ops-flow-target d-block">外部報價預檢</span>
</span>
</button>
</div>
</section>
<!-- ── PChome 成長作戰清單 ── -->
<section class="card shadow-sm ai-panel" id="growthOpsPanel">
<div class="card-header d-flex justify-content-between align-items-center py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-compass"></i>PChome 成長作戰
<small class="text-muted fw-normal ms-2">先處理最可能影響業績的商品</small>
</span>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadGrowthOps(true)">
<i class="fas fa-redo me-1"></i>更新清單
</button>
</div>
<div class="card-body">
<div class="growth-ops-grid">
<div>
<div class="growth-metric-row">
<div class="growth-metric">
<strong id="growthCandidateCount"></strong>
<span>作戰商品</span>
</div>
<div class="growth-metric">
<strong id="growthMappedCount"></strong>
<span>可直接比價</span>
</div>
<div class="growth-metric">
<strong id="growthNeedsMapping"></strong>
<span>待補對應</span>
</div>
</div>
<p class="growth-action-hint" id="growthActionHint">正在判斷今天優先處理順序...</p>
<p class="growth-data-summary" id="growthDataSourceSummary">資料來源整理中...</p>
<p class="growth-source-note" id="growthSourceNote">來源整理中...</p>
<div class="growth-source-list" id="growthSourceReadiness">
<div class="growth-source-chip">
<div class="growth-source-name">
<span>外部資料來源</span>
<span class="growth-source-status">整理中</span>
</div>
<p class="growth-source-detail">正在確認哪些來源可進作戰清單。</p>
</div>
</div>
</div>
<div>
<div class="growth-list" id="growthOpsList">
<div class="text-center py-4 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>整理作戰清單中...
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── 外部報價 CSV 預檢 ── -->
<section class="card shadow-sm ai-panel" id="externalOfferDryRunPanel">
<div class="card-header d-flex justify-content-between align-items-center py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-file-circle-check"></i>外部報價預檢
<small class="text-muted fw-normal ms-2">只做檢查,不會匯入資料</small>
</span>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="fillExternalOfferSample()">
<i class="fas fa-table me-1"></i>填入範例
</button>
</div>
<div class="card-body">
<div class="offer-dryrun-grid">
<div class="offer-dryrun-field">
<label for="externalOfferCsvFile">CSV 檔案</label>
<input class="form-control form-control-sm" type="file" id="externalOfferCsvFile" accept=".csv,text/csv">
<label for="externalOfferCsvText">或貼上 CSV 內容</label>
<textarea class="form-control" id="externalOfferCsvText" spellcheck="false"
placeholder="資料來源,外部商品ID,商品名稱,售價,資料時間,取得方式,PChome商品ID,同款狀態,資料可信度"></textarea>
<button class="btn btn-primary btn-sm ai-action-btn" id="btnExternalOfferDryRun" onclick="previewExternalOfferCsv()">
<i class="fas fa-magnifying-glass me-1"></i>預檢 CSV
</button>
</div>
<div>
<div class="offer-dryrun-summary">
<div class="growth-metric">
<strong id="offerDryRunReady"></strong>
<span>可使用</span>
</div>
<div class="growth-metric">
<strong id="offerDryRunReview"></strong>
<span>待確認</span>
</div>
<div class="growth-metric">
<strong id="offerDryRunBlocked"></strong>
<span>不能使用</span>
</div>
</div>
<div class="offer-dryrun-result" id="offerDryRunResult">
<div class="text-center py-4 text-muted">
<i class="fas fa-circle-info d-block mb-2"></i>
上傳或貼上 CSV 後,先檢查資料品質。
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── KPI 卡片 ── -->
<div class="row g-3 mb-4" id="kpiRow">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-primary" id="kpiSkus"></div>
<div class="small text-muted mt-1"><i class="fas fa-box me-1"></i>監控商品數</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpiCompetitors"></div>
<div class="small text-muted mt-1">
<i class="fas fa-store me-1"></i>可直接比價
<span id="kpiMatchRate" class="text-muted" style="font-size:0.7rem"></span>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<!-- 高風險卡 — 數值來自全量 CTE非前端截斷的 200 筆 -->
<div class="card border-0 shadow-sm h-100" id="kpiHighRiskCard">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-danger" id="kpiHighRisk"></div>
<div class="small text-muted mt-1">
<i class="fas fa-exclamation-triangle me-1"></i>需檢查價格
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-info" id="kpiAiRecs"></div>
<div class="small text-muted mt-1"><i class="fas fa-robot me-1"></i>作戰建議紀錄</div>
</div>
</div>
</div>
</div>
<!-- ── 主體分兩欄(競品比價 + AI 決策) ── -->
<div class="row g-3">
<!-- ── 左:外部價格參考 ── -->
<div class="col-xl-7">
<div class="card shadow-sm h-100 ai-panel" id="externalPricePanel">
<div class="card-header d-flex justify-content-between align-items-center py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-balance-scale text-warning me-2"></i>MOMO 外部價格參考
</span>
<div class="d-flex gap-2 align-items-center">
<select class="form-select form-select-sm" id="riskFilter" onchange="filterTable()" style="width:100px">
<option value="all">全部</option>
<option value="HIGH">高風險</option>
<option value="MED">中風險</option>
<option value="LOW">低風險</option>
</select>
<input type="text" class="form-control form-control-sm" id="searchInput"
placeholder="搜尋商品..." oninput="filterTable()" style="width:130px">
</div>
</div>
<!-- 熱力圖圖例 -->
<div class="px-3 pt-2 pb-1 d-flex gap-3 small text-muted ai-legend" style="font-size:0.73rem">
<span><span style="display:inline-block;width:12px;height:12px;background:#fee2e2;border-radius:2px" class="me-1"></span>PChome 貴 &gt;20%</span>
<span><span style="display:inline-block;width:12px;height:12px;background:#fef9c3;border-radius:2px" class="me-1"></span>PChome 貴 10~20%</span>
<span><span style="display:inline-block;width:12px;height:12px;background:#dcfce7;border-radius:2px" class="me-1"></span>PChome 便宜</span>
</div>
<div class="card-body p-0 ai-table-scroll">
<table class="table table-sm table-hover mb-0 align-middle" id="competitorTable">
<thead class="table-light sticky-top" style="font-size:0.78rem;">
<tr>
<th class="ps-3" style="min-width:200px">商品</th>
<th class="text-end" style="min-width:75px">MOMO</th>
<th class="text-end" style="min-width:75px">PChome</th>
<th class="text-end" style="min-width:70px">價差</th>
<th style="min-width:90px">比價狀態</th>
<th class="text-center" style="min-width:55px">可信度</th>
<th class="text-muted" style="min-width:80px">更新</th>
</tr>
</thead>
<tbody id="competitorTbody">
<tr><td colspan="7" class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>載入中...
</td></tr>
</tbody>
</table>
</div>
<div class="card-footer bg-white py-2 d-flex justify-content-between small text-muted">
<span id="compCount"></span>
<span id="compSourceSummary">僅顯示已確認同款的商品</span>
</div>
</div>
</div>
<!-- ── 右:作戰建議紀錄 ── -->
<div class="col-xl-5">
<div class="card shadow-sm h-100 ai-panel">
<div class="card-header py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-robot text-danger me-2"></i>作戰建議紀錄
<small class="text-muted fw-normal ms-2">挑品、比價與人工覆核</small>
</span>
</div>
<div class="card-body p-0 ai-recs-scroll">
<div id="aiRecsList" class="p-2">
<div class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>載入中...
</div>
</div>
</div>
<div class="card-footer bg-white py-2 d-flex justify-content-between small text-muted">
<span id="aiRecsCount"></span>
<span>可手動產生作戰商品</span>
</div>
</div>
</div>
</div><!-- /row -->
<!-- ── Trigger 進度 Toast ── -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
<div id="triggerToast" class="toast align-items-center text-white border-0" role="alert">
<div class="d-flex">
<div class="toast-body" id="triggerToastMsg">
<i class="fas fa-bolt me-1"></i>分析已啟動...
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
</div><!-- /ai-intel-page -->
<script>
// ── 全域資料 ────────────────────────────────────────
let allCompetitors = [];
// ── 頁面載入 ────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadDashboard();
loadGrowthOps();
});
async function loadDashboard() {
try {
const res = await fetch('/api/ai/icaim/dashboard');
const data = await res.json();
if (!data.success) throw new Error(data.error || '載入失敗');
renderKPIs(data.stats);
allCompetitors = data.competitors;
renderCompetitorTable(allCompetitors);
renderAiRecs(data.ai_recs);
document.getElementById('lastUpdateBadge').innerHTML =
'<i class="fas fa-check-circle me-1"></i>上次更新 ' + new Date().toLocaleTimeString('zh-TW');
document.getElementById('lastUpdateBadge').className = 'ai-status-badge is-success';
} catch (e) {
document.getElementById('lastUpdateBadge').innerHTML =
'<i class="fas fa-exclamation-circle me-1"></i>載入失敗';
document.getElementById('lastUpdateBadge').className = 'ai-status-badge is-error';
console.error(e);
}
}
// ── KPIhigh_risk_count 來自後端全量 CTE─────────
function renderKPIs(stats) {
document.getElementById('kpiSkus').textContent = (stats.total_skus || 0).toLocaleString();
document.getElementById('kpiCompetitors').textContent = (stats.valid_competitor_prices || 0).toLocaleString();
document.getElementById('kpiAiRecs').textContent = (stats.total_ai_recs || 0).toLocaleString();
document.getElementById('kpiMatchRate').textContent = stats.match_rate ? `(${stats.match_rate}%)` : '';
renderCompetitorSourceSummary(stats);
const hr = stats.high_risk_count || 0;
document.getElementById('kpiHighRisk').textContent = hr;
// 高風險卡:數值 > 0 加紅底強調
document.getElementById('kpiHighRiskCard').className =
hr > 0
? 'card border-2 border-danger shadow-sm h-100'
: 'card border-0 shadow-sm h-100';
}
function formatMoney(value) {
const num = Number(value || 0);
return 'NT$ ' + Math.round(num).toLocaleString();
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[ch]));
}
function scrollToPanel(panelId) {
const panel = document.getElementById(panelId);
if (!panel) return;
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
async function loadGrowthOps(forceRefresh = false) {
const list = document.getElementById('growthOpsList');
if (forceRefresh) {
list.innerHTML = `<div class="text-center py-4 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>更新作戰清單中...
</div>`;
}
try {
const url = '/api/ai/pchome-growth/opportunities?limit=8' + (forceRefresh ? '&refresh=1' : '');
const res = await fetch(url);
const data = await res.json();
if (!data.success) throw new Error(data.error || '讀取失敗');
const stats = data.stats || {};
document.getElementById('growthCandidateCount').textContent = (stats.candidate_count || 0).toLocaleString();
document.getElementById('growthMappedCount').textContent = (stats.mapped_count || 0).toLocaleString();
document.getElementById('growthNeedsMapping').textContent = (stats.needs_mapping_count || 0).toLocaleString();
renderGrowthActionHint(stats);
renderGrowthDataSourceSummary(stats);
const scope = data.source_scope || {};
const active = (scope.active_external_sources || []).join('、') || '尚未接入';
const paused = (scope.paused_external_sources || []).join('、') || '無';
document.getElementById('growthSourceNote').textContent =
`業績來源:${scope.primary_sales_source || 'PChome 後台業績'}。外部價格先看:${active}。暫停來源:${paused}`;
renderGrowthSourceReadiness((scope.source_readiness || {}).sources || []);
renderGrowthOps(data.opportunities || []);
} catch (error) {
console.error(error);
renderGrowthActionHint({ candidate_count: 0, mapped_count: 0, needs_mapping_count: 0 });
renderGrowthDataSourceSummary({});
list.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-exclamation d-block mb-2"></i>
PChome 成長作戰清單暫時無法讀取,請稍後再試。
</div>`;
}
}
function renderCompetitorSourceSummary(stats) {
const target = document.getElementById('compSourceSummary');
if (!target) return;
const counts = stats.competitor_data_source_counts || {};
const entries = Object.entries(counts)
.filter(([, count]) => Number(count || 0) > 0)
.map(([label, count]) => `${label} ${Number(count).toLocaleString()}`);
target.textContent = entries.length
? `資料來源:${entries.join('、')}`
: '僅顯示已確認同款的商品';
}
function renderGrowthActionHint(stats) {
const hint = document.getElementById('growthActionHint');
if (!hint) return;
const candidateCount = Number(stats.candidate_count || 0);
const mappedCount = Number(stats.mapped_count || 0);
const needsMapping = Number(stats.needs_mapping_count || 0);
if (!candidateCount) {
hint.textContent = '目前沒有可處理的作戰商品,請先確認 PChome 業績資料已更新。';
return;
}
if (needsMapping > 0 && mappedCount === 0) {
hint.textContent = `今天先補商品對應:目前 ${needsMapping.toLocaleString()} 件高業績商品還不能直接比價。`;
return;
}
if (needsMapping > 0) {
hint.textContent = `先處理 ${mappedCount.toLocaleString()} 件可直接比價商品,再補 ${needsMapping.toLocaleString()} 件商品對應。`;
return;
}
hint.textContent = '目前商品都可直接比價,先檢查售價偏高或可以放大價格優勢的項目。';
}
function renderGrowthDataSourceSummary(stats) {
const summary = document.getElementById('growthDataSourceSummary');
if (!summary) return;
const counts = stats.external_data_source_counts || {};
const entries = Object.entries(counts)
.filter(([, count]) => Number(count || 0) > 0)
.map(([label, count]) => `${label} ${Number(count).toLocaleString()}`);
if (!entries.length) {
summary.textContent = '資料來源:目前作戰清單尚未接到外部參考價,請先補商品對應。';
return;
}
summary.textContent = `資料來源:${entries.join('、')}`;
}
function renderGrowthSourceReadiness(sources) {
const box = document.getElementById('growthSourceReadiness');
if (!box) return;
if (!sources.length) {
box.innerHTML = `<div class="growth-source-chip">
<div class="growth-source-name">
<span>外部資料來源</span>
<span class="growth-source-status">未確認</span>
</div>
<p class="growth-source-detail">目前還沒有可顯示的來源狀態。</p>
</div>`;
return;
}
box.innerHTML = sources.slice(0, 3).map((source) => {
const usable = Number(source.usable_offer_count || 0);
const detail = usable > 0
? `${source.data_quality_label || '資料可用'},目前有 ${usable.toLocaleString()} 筆可用資料。`
: `${source.plain_state || source.plain_note || '等待資料接入。'}`;
const activeClass = source.status_code === 'active' ? ' is-active' : '';
return `<div class="growth-source-chip${activeClass}">
<div class="growth-source-name">
<span>${escapeHtml(source.display_name || '未命名來源')}</span>
<span class="growth-source-status">${escapeHtml(source.status_label || '待確認')}</span>
</div>
<p class="growth-source-detail">${escapeHtml(detail)}</p>
</div>`;
}).join('');
}
function renderGrowthOps(rows) {
const list = document.getElementById('growthOpsList');
if (!rows.length) {
list.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-info d-block mb-2"></i>
目前沒有足夠資料,請先確認 PChome 業績檔已匯入。
</div>`;
return;
}
list.innerHTML = rows.map((row) => {
const action = row.recommended_action || {};
const reason = (row.reason_lines || []).slice(0, 2).join(' ');
const price = row.external_price;
const priceText = price && price.gap_pct !== null && price.gap_pct !== undefined
? `外部價差 ${price.gap_pct > 0 ? '+' : ''}${price.gap_pct}%`
: '尚未可比價';
return `<article class="growth-item">
<div>
<h3 class="growth-item-title">${escapeHtml(row.product_name)}</h3>
<p class="growth-item-meta">
${formatMoney(row.sales_7d)} · 近 7 天業績 · ${escapeHtml(priceText)}
</p>
<p class="growth-item-reason">${escapeHtml(reason)}</p>
</div>
<span class="growth-action-pill">${escapeHtml(action.label || '待判斷')}</span>
</article>`;
}).join('');
}
function fillExternalOfferSample() {
const sample = [
'資料來源,外部商品ID,商品名稱,售價,資料時間,取得方式,PChome商品ID,同款狀態,資料可信度',
'momo_reference,MOMO-1001,範例保養商品,899,2026-06-15T10:00:00,manual_csv,PCH-1001,verified,88',
'shopee,SHP-2001,待確認商品,790,2026-06-15T10:05:00,manual_csv,,unmatched,60'
].join('\\n');
document.getElementById('externalOfferCsvText').value = sample;
}
async function previewExternalOfferCsv() {
const fileInput = document.getElementById('externalOfferCsvFile');
const textInput = document.getElementById('externalOfferCsvText');
const resultBox = document.getElementById('offerDryRunResult');
const button = document.getElementById('btnExternalOfferDryRun');
const formData = new FormData();
if (fileInput.files && fileInput.files[0]) {
formData.append('file', fileInput.files[0]);
}
if (textInput.value.trim()) {
formData.append('csv_text', textInput.value.trim());
}
if (!formData.has('file') && !formData.has('csv_text')) {
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-exclamation d-block mb-2"></i>
請先上傳 CSV 或貼上內容。
</div>`;
return;
}
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>預檢中';
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>檢查資料品質中...
</div>`;
try {
const response = await fetch('/api/ai/pchome-growth/external-offers/csv-dry-run', {
method: 'POST',
body: formData,
});
const data = await response.json();
renderExternalOfferDryRun(data);
} catch (error) {
console.error(error);
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-exclamation d-block mb-2"></i>
CSV 預檢暫時失敗,請稍後再試。
</div>`;
} finally {
button.disabled = false;
button.innerHTML = '<i class="fas fa-magnifying-glass me-1"></i>預檢 CSV';
}
}
function renderExternalOfferDryRun(data) {
const resultBox = document.getElementById('offerDryRunResult');
const summary = data.summary || {};
document.getElementById('offerDryRunReady').textContent = (summary.ready_count || 0).toLocaleString();
document.getElementById('offerDryRunReview').textContent = (summary.review_count || 0).toLocaleString();
document.getElementById('offerDryRunBlocked').textContent = (summary.blocked_count || 0).toLocaleString();
if (!data.success) {
const errors = (data.errors || [data.error || 'CSV 格式需要修正']).slice(0, 4);
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-exclamation d-block mb-2"></i>
${escapeHtml(errors.join(''))}
</div>`;
return;
}
const rows = (data.rows || []).slice(0, 8);
if (!rows.length) {
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
<i class="fas fa-circle-info d-block mb-2"></i>
CSV 裡沒有可檢查的資料列。
</div>`;
return;
}
resultBox.innerHTML = rows.map((row) => {
const statusClass = row.status_code === 'ready'
? ' is-ready'
: row.status_code === 'blocked'
? ' is-blocked'
: '';
const reasons = (row.reasons || []).join('、');
const price = row.price ? formatMoney(row.price) : '未填價格';
return `<article class="offer-dryrun-row">
<div>
<h3 class="offer-dryrun-title">${escapeHtml(row.title || '未命名商品')}</h3>
<p class="offer-dryrun-meta">
${escapeHtml(row.row_number || '-')} 列 · ${escapeHtml(row.source_code || '未填來源')} · ${escapeHtml(price)}
</p>
<p class="offer-dryrun-reason">${escapeHtml(reasons)}</p>
</div>
<span class="offer-status-pill${statusClass}">${escapeHtml(row.status_label || '待確認')}</span>
</article>`;
}).join('');
}
// ── 競品比價表格(熱力圖底色)──────────────────────
function renderCompetitorTable(rows) {
const tbody = document.getElementById('competitorTbody');
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-5 text-muted">
<i class="fas fa-info-circle me-2"></i>暫無競品比價資料
</td></tr>`;
document.getElementById('compCount').textContent = '0 筆';
return;
}
tbody.innerHTML = rows.map(r => {
// 行底色
let rowBg = '';
if (r.gap_pct > 20) rowBg = 'background:#fee2e2'; // 淺紅 — 嚴重貴
else if (r.gap_pct > 10) rowBg = 'background:#fef9c3'; // 淺黃 — 有風險
else if (r.gap_pct < 0) rowBg = 'background:#f0fdf4'; // 淺綠 — 我便宜
// 價差文字顏色
const gapClass = r.gap_pct > 15 ? 'text-danger fw-bold'
: r.gap_pct > 5 ? 'text-warning fw-bold'
: r.gap_pct < 0 ? 'text-success fw-bold'
: 'text-muted';
const gapSign = r.gap_pct > 0 ? '+' : '';
const tagHtml = (r.tags || []).map(t => {
const tagMap = {
'on_sale': ['bg-info text-dark', '促銷中'],
'discount_30pct': ['bg-danger text-white', '折30%+'],
'discount_20pct': ['bg-warning text-dark', '折20%+'],
'discount_10pct': ['bg-secondary text-white','折10%+'],
'low_stock': ['bg-dark text-white', '低庫存'],
'high_rating': ['bg-primary text-white', '高評分'],
'identity_v2': ['bg-success text-white', '同款確認'],
'match_type_exact':['bg-success text-white', '同款確認'],
'price_alert_exact':['bg-danger text-white', '價差告警'],
'external_offers': ['bg-success text-white', '自動同步'],
'legacy_competitor_cache':['bg-light text-dark','舊資料'],
'evidence_brand': ['bg-light text-dark', '品牌一致'],
'evidence_identity':['bg-light text-dark', '同款證據'],
'match_shared_model_token':['bg-light text-dark','型號一致'],
'match_product_line':['bg-light text-dark', '品線一致'],
'alert_tier_price':['bg-warning text-dark', '優先追蹤'],
};
const mapped = tagMap[t];
if (!mapped) return '';
const [cls, label] = mapped;
return `<span class="badge ${cls} me-1" style="font-size:0.68rem">${label}</span>`;
}).filter(Boolean).slice(0, 3).join('');
const scoreColor = r.match_score >= 0.7 ? 'text-success'
: r.match_score >= 0.55 ? 'text-warning'
: 'text-danger';
return `<tr data-risk="${r.risk}" data-source="${escapeHtml(r.data_source || '')}" data-name="${r.name.toLowerCase()}" style="${rowBg}">
<td class="ps-3">
<div style="font-size:0.82rem;font-weight:500" title="${r.name}">${r.name}</div>
<small class="text-muted">${r.category} · ${r.sku}</small>
</td>
<td class="text-end text-dark fw-bold">$${r.momo_price.toLocaleString()}</td>
<td class="text-end text-secondary">$${r.pchome_price.toLocaleString()}</td>
<td class="text-end ${gapClass}">${gapSign}${r.gap_pct}%</td>
<td>${tagHtml || '<span class="text-muted small">—</span>'}</td>
<td class="text-center ${scoreColor}" style="font-size:0.8rem">${r.match_score.toFixed(2)}</td>
<td class="text-muted" style="font-size:0.75rem">${r.crawled_at}</td>
</tr>`;
}).join('');
document.getElementById('compCount').textContent = `${rows.length}`;
}
// ── 篩選 ─────────────────────────────────────────────
function filterTable() {
const risk = document.getElementById('riskFilter').value;
const search = document.getElementById('searchInput').value.toLowerCase().trim();
const filtered = allCompetitors.filter(r => {
const riskOk = risk === 'all' || r.risk === risk;
const searchOk = !search || r.name.toLowerCase().includes(search) || r.sku.includes(search);
return riskOk && searchOk;
});
renderCompetitorTable(filtered);
}
// ── 作戰建議紀錄 ────────────────────────
function renderAiRecs(recs) {
const container = document.getElementById('aiRecsList');
document.getElementById('aiRecsCount').textContent =
recs.length ? `${recs.length} 筆建議` : '尚無建議';
if (!recs.length) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-brain fa-3x text-muted mb-3 d-block"></i>
<p class="text-muted mb-1">目前還沒有作戰建議</p>
<p class="small text-muted mb-3">
系統會定期整理,也可以手動更新。
</p>
<button class="btn btn-sm btn-outline-danger" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>整理建議
</button>
</div>`;
return;
}
const strategyMap = {
'price_cut': ['bg-danger', '降價'],
'promote': ['bg-primary', '主推'],
'product_pick':['bg-success', 'AI挑品'],
'monitor': ['bg-secondary', '觀察'],
'flag': ['bg-warning text-dark', '覆核'],
};
container.innerHTML = recs.map(r => {
const [sBg, sLabel] = strategyMap[r.strategy] || ['bg-secondary', r.strategy];
const confPct = Math.round(r.confidence * 100);
const confColor = confPct >= 80 ? 'bg-success' : confPct >= 60 ? 'bg-warning' : 'bg-danger';
const gapSign = r.gap_pct > 0 ? '+' : '';
return `<div class="border rounded mb-2 p-2" style="font-size:0.83rem">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="fw-bold text-truncate me-2" style="max-width:200px" title="${r.name}">${r.name}</span>
<span class="badge ${sBg} flex-shrink-0">${sLabel}</span>
</div>
<div class="d-flex gap-3 mb-1 text-muted small">
<span>MOMO <strong class="text-dark">$${r.momo_price.toLocaleString()}</strong></span>
<span>PChome <strong class="text-secondary">$${r.pchome_price.toLocaleString()}</strong></span>
<span class="${r.gap_pct > 10 ? 'text-danger fw-bold' : 'text-muted'}">${gapSign}${r.gap_pct}%</span>
</div>
<div class="mb-1 text-muted" style="font-size:0.78rem;line-height:1.4">
${r.reason ? r.reason.substring(0, 90) + (r.reason.length > 90 ? '…' : '') : ''}
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-1">
<div class="progress" style="width:60px;height:6px">
<div class="progress-bar ${confColor}" style="width:${confPct}%"></div>
</div>
<small class="text-muted">${confPct}%</small>
</div>
<small class="text-muted">
${r.created_at}
</small>
</div>
</div>`;
}).join('');
}
// ── 產生 AI 建議挑品清單 ───────────────────────────
async function generatePickList() {
const btn = document.getElementById('btnPickList');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/product-picks/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 50 })
});
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error || '產生失敗'}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) loadDashboard();
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>整理失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-wand-magic-sparkles me-1"></i>產生作戰商品';
}
}
// ── 補抓 PChome 尚未搜尋商品 ───────────────────────
async function backfillPchomeMatches() {
const btn = document.getElementById('btnBackfill');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/pchome-match/backfill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 60 })
});
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error || '商品對應啟動失敗'}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) setTimeout(loadDashboard, 90000);
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>商品對應失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-magnifying-glass-chart me-1"></i>補商品對應';
}
}
// ── 手動觸發分析 ────────────────────────────────────
async function triggerAnalysis() {
const btn = document.getElementById('btnTrigger');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/icaim/trigger', { method: 'POST' });
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) {
// 60 秒後自動重新整理儀表板
setTimeout(loadDashboard, 60000);
}
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>整理失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-bolt me-1"></i>整理建議';
}
}
</script>
{% endblock %}