fix: 收斂 PChome 工具頁新版樣式
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-17 22:13:46 +08:00
parent 5690d79a40
commit a9b5385615
4 changed files with 356 additions and 52 deletions

View File

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

View File

@@ -2,20 +2,139 @@
{% block title %}PChome 爬蟲 - EwoooC{% endblock %}
{% block extra_css %}
<style>
.pchome-tool-page {
color: var(--momo-text-primary);
}
.pchome-tool-head {
padding: var(--momo-space-4) var(--momo-space-5);
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
}
.pchome-tool-head h2 {
margin: 0;
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-headline);
font-weight: 800;
letter-spacing: 0;
}
.pchome-tool-head p,
.pchome-tool-page .text-muted {
color: var(--momo-text-secondary) !important;
}
.pchome-card-head {
background: var(--momo-bg-paper);
border-bottom: 1px solid var(--momo-border-light);
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-weight: 800;
}
.pchome-table-head th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.pchome-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 3px 8px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
line-height: 1;
}
.pchome-badge.is-danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.pchome-badge.is-success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.pchome-badge.is-muted {
background: var(--momo-tag-muted-bg);
border-color: var(--momo-tag-muted-border);
color: var(--momo-tag-muted-text);
}
.pchome-toast {
top: 20px;
right: 20px;
z-index: 9999;
min-width: min(300px, calc(100vw - 40px));
padding: 12px 14px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
background: var(--momo-bg-elevated);
color: var(--momo-text-primary);
font-weight: 700;
}
.pchome-toast--success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.pchome-toast--danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.pchome-toast--warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
@media (max-width: 760px) {
.pchome-tool-head {
padding: var(--momo-space-4);
}
.pchome-tool-page .card-header {
align-items: flex-start !important;
flex-direction: column;
gap: var(--momo-space-2);
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col">
<div class="container-fluid py-4 pchome-tool-page">
<header class="pchome-tool-head mb-4">
<h2><i class="fas fa-spider me-2"></i>PChome 24h 爬蟲</h2>
<p class="text-muted">爬取 PChome 24h 商品資料</p>
</div>
</div>
</header>
<!-- 爬取方式選擇 -->
<div class="row mb-4">
<div class="col-lg-6">
<div class="card">
<div class="card-header bg-primary text-white">
<div class="card-header pchome-card-head">
<i class="fas fa-folder-open me-2"></i>館別爬取
</div>
<div class="card-body">
@@ -39,7 +158,7 @@
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header bg-success text-white">
<div class="card-header pchome-card-head">
<i class="fas fa-search me-2"></i>關鍵字搜尋
</div>
<div class="card-body">
@@ -55,7 +174,7 @@
<option value="100">100 筆</option>
</select>
</div>
<button class="btn btn-success" id="searchBtn">
<button class="btn btn-primary" id="searchBtn">
<i class="fas fa-search me-1"></i>搜尋
</button>
</div>
@@ -67,7 +186,7 @@
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header bg-info text-white">
<div class="card-header pchome-card-head">
<i class="fas fa-link me-2"></i>自訂 URL 爬取
</div>
<div class="card-body">
@@ -78,7 +197,7 @@
placeholder="https://24h.pchome.com.tw/region/DDAB">
</div>
<div class="col-lg-3">
<button class="btn btn-info w-100" id="crawlCustomBtn">
<button class="btn btn-primary w-100" id="crawlCustomBtn">
<i class="fas fa-download me-1"></i>爬取
</button>
</div>
@@ -125,8 +244,8 @@
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-striped" id="resultTable">
<thead class="table-dark">
<table class="table table-hover mb-0" id="resultTable">
<thead class="pchome-table-head">
<tr>
<th style="width: 80px;">圖片</th>
<th>商品名稱</th>
@@ -346,10 +465,10 @@
tbody.innerHTML = '';
for (const p of products) {
const discount = p.discount ? `<span class="badge bg-danger">-${p.discount}%</span>` : '-';
const discount = p.discount ? `<span class="pchome-badge is-danger">-${p.discount}%</span>` : '-';
const stockBadge = p.stock > 0
? `<span class="badge bg-success">${p.stock}</span>`
: `<span class="badge bg-secondary">缺貨</span>`;
? `<span class="pchome-badge is-success">${p.stock}</span>`
: `<span class="pchome-badge is-muted">缺貨</span>`;
const row = document.createElement('tr');
row.innerHTML = `
@@ -431,8 +550,7 @@
function showToast(message, type = 'info') {
// 簡易 Toast
const toast = document.createElement('div');
toast.className = `alert alert-${type} position-fixed`;
toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
toast.className = `pchome-toast pchome-toast--${type} position-fixed`;
toast.innerHTML = `
<button type="button" class="btn-close float-end" onclick="this.parentElement.remove()"></button>
${message}

View File

@@ -2,21 +2,169 @@
{% block title %}比價系統 - EwoooC{% endblock %}
{% block extra_css %}
<style>
.price-tool-page {
color: var(--momo-text-primary);
}
.price-tool-head {
padding: var(--momo-space-4) var(--momo-space-5);
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
}
.price-tool-head h2 {
margin: 0;
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-size: var(--momo-text-headline);
font-weight: 800;
letter-spacing: 0;
}
.price-tool-head p,
.price-tool-page .text-muted {
color: var(--momo-text-secondary) !important;
}
.price-step-head {
background: var(--momo-bg-paper);
border-bottom: 1px solid var(--momo-border-light);
color: var(--momo-text-primary);
font-family: var(--momo-font-display);
font-weight: 800;
}
.price-count-badge,
.price-result-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 3px 8px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
line-height: 1;
}
.price-count-badge.is-muted,
.price-result-badge.is-muted {
background: var(--momo-tag-muted-bg);
border-color: var(--momo-tag-muted-border);
color: var(--momo-tag-muted-text);
}
.price-count-badge.is-pchome,
.price-result-badge.is-pchome {
background: var(--momo-info-bg);
border-color: var(--momo-info-border);
color: var(--momo-info-text);
}
.price-count-badge.is-momo,
.price-result-badge.is-momo {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
.price-stat-card {
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
}
.price-stat-card h4 {
color: var(--momo-text-primary);
font-family: var(--momo-font-mono, monospace);
font-weight: 800;
}
.price-stat-card.is-pchome h4 {
color: var(--momo-info-text);
}
.price-stat-card.is-momo h4 {
color: var(--momo-warning-text);
}
.price-stat-card.is-diff h4 {
color: var(--momo-text-primary);
}
.price-note {
padding: 10px 12px;
background: var(--momo-info-bg);
border: 1px solid var(--momo-info-border);
border-radius: var(--momo-radius-md);
color: var(--momo-info-text);
}
.price-table-head th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.price-toast {
top: 20px;
right: 20px;
z-index: 9999;
min-width: min(300px, calc(100vw - 40px));
padding: 12px 14px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-md);
background: var(--momo-bg-elevated);
color: var(--momo-text-primary);
font-weight: 700;
}
.price-toast--success {
background: var(--momo-success-bg);
border-color: var(--momo-success-border);
color: var(--momo-success-text);
}
.price-toast--danger {
background: var(--momo-danger-bg);
border-color: var(--momo-danger-border);
color: var(--momo-danger-text);
}
.price-toast--warning {
background: var(--momo-warning-bg);
border-color: var(--momo-warning-border);
color: var(--momo-warning-text);
}
@media (max-width: 760px) {
.price-tool-head {
padding: var(--momo-space-4);
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col">
<div class="container-fluid py-4 price-tool-page">
<header class="price-tool-head mb-4">
<h2><i class="fas fa-balance-scale me-2"></i>PChome vs MOMO 比價</h2>
<p class="text-muted">比較 PChome 24h 和 MOMO 美妝商品價格</p>
</div>
</div>
</header>
<!-- 操作區 -->
<div class="row mb-4">
<!-- Step 1: 選擇品牌 -->
<div class="col-lg-4 mb-3">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<div class="card-header price-step-head">
<i class="fas fa-tag me-2"></i>Step 1: 選擇品牌
</div>
<div class="card-body">
@@ -36,11 +184,11 @@
<!-- Step 2: 取得 PChome 商品 -->
<div class="col-lg-4 mb-3">
<div class="card h-100">
<div class="card-header bg-info text-white">
<div class="card-header price-step-head">
<i class="fas fa-shopping-cart me-2"></i>Step 2: PChome 商品
</div>
<div class="card-body">
<button class="btn btn-info w-100 mb-2" id="fetchPchomeBtn">
<button class="btn btn-primary w-100 mb-2" id="fetchPchomeBtn">
<i class="fas fa-download me-1"></i>自動爬取 PChome
</button>
<div class="text-center my-2"><small class="text-muted"></small></div>
@@ -48,7 +196,7 @@
<i class="fas fa-keyboard me-1"></i>手動輸入
</button>
<div class="mt-3">
<span class="badge bg-secondary" id="pchomeCount">0 筆商品</span>
<span class="price-count-badge is-muted" id="pchomeCount">0 筆商品</span>
</div>
</div>
</div>
@@ -57,11 +205,11 @@
<!-- Step 3: 取得 MOMO 商品 -->
<div class="col-lg-4 mb-3">
<div class="card h-100">
<div class="card-header bg-warning text-dark">
<div class="card-header price-step-head">
<i class="fas fa-store me-2"></i>Step 3: MOMO 商品
</div>
<div class="card-body">
<button class="btn btn-warning w-100 mb-2" id="uploadMomoBtn" data-bs-toggle="modal" data-bs-target="#uploadModal">
<button class="btn btn-primary w-100 mb-2" id="uploadMomoBtn" data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="fas fa-file-excel me-1"></i>上傳 Excel
</button>
<div class="text-center my-2"><small class="text-muted"></small></div>
@@ -69,7 +217,7 @@
<i class="fas fa-keyboard me-1"></i>手動輸入
</button>
<div class="mt-3">
<span class="badge bg-secondary" id="momoCount">0 筆商品</span>
<span class="price-count-badge is-muted" id="momoCount">0 筆商品</span>
</div>
</div>
</div>
@@ -79,7 +227,7 @@
<!-- 比價按鈕 -->
<div class="row mb-4">
<div class="col text-center">
<button class="btn btn-lg btn-success px-5" id="compareBtn" disabled>
<button class="btn btn-lg btn-primary px-5" id="compareBtn" disabled>
<i class="fas fa-balance-scale me-2"></i>開始比價
</button>
</div>
@@ -108,7 +256,7 @@
<!-- 統計卡片 -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card bg-light">
<div class="card price-stat-card">
<div class="card-body text-center">
<h4 class="mb-0" id="matchedCount">0</h4>
<small class="text-muted">成功匹配</small>
@@ -116,7 +264,7 @@
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-info text-white">
<div class="card price-stat-card is-pchome">
<div class="card-body text-center">
<h4 class="mb-0" id="pchomeCheaperCount">0</h4>
<small>PChome 較便宜</small>
@@ -124,7 +272,7 @@
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-warning text-dark">
<div class="card price-stat-card is-momo">
<div class="card-body text-center">
<h4 class="mb-0" id="momoCheaperCount">0</h4>
<small>MOMO 較便宜</small>
@@ -132,7 +280,7 @@
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-secondary text-white">
<div class="card price-stat-card is-diff">
<div class="card-body text-center">
<h4 class="mb-0" id="avgPriceDiff">$0</h4>
<small>平均價差</small>
@@ -154,7 +302,7 @@
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-striped mb-0">
<thead class="table-dark">
<thead class="price-table-head">
<tr>
<th style="width: 35%;">PChome 商品</th>
<th style="width: 35%;">MOMO 商品</th>
@@ -188,14 +336,14 @@
<label class="form-label">選擇 Excel 檔案</label>
<input type="file" class="form-control" id="momoExcelFile" accept=".xlsx,.xls,.csv">
</div>
<div class="alert alert-info">
<div class="price-note">
<i class="fas fa-info-circle me-1"></i>
Excel 需包含「商品名稱」和「售價」欄位。可選包含「商品編號」和「連結」。
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="parseExcelBtn">
<button type="button" class="btn btn-primary" id="parseExcelBtn">
<i class="fas fa-upload me-1"></i>上傳並解析
</button>
</div>
@@ -212,7 +360,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info py-2">
<div class="price-note py-2">
<i class="fas fa-info-circle me-1"></i>
<strong>格式說明:</strong> 每行一筆商品,用<strong>逗號</strong><strong>Tab</strong> 分隔<br>
<code>商品名稱,價格</code><code>商品名稱,價格,商品連結(選填)</code>
@@ -330,7 +478,7 @@ La Roche-Posay 安得利防曬液 50ml,920
if (data.success) {
pchomeProducts = data.data.products;
document.getElementById('pchomeCount').textContent = `${pchomeProducts.length} 筆商品`;
document.getElementById('pchomeCount').classList.replace('bg-secondary', 'bg-info');
document.getElementById('pchomeCount').className = 'price-count-badge is-pchome';
updateCompareButton();
showToast(`成功取得 ${pchomeProducts.length} 筆 PChome 商品`, 'success');
} else {
@@ -365,7 +513,7 @@ La Roche-Posay 安得利防曬液 50ml,920
if (data.success) {
momoProducts = data.data.products;
document.getElementById('momoCount').textContent = `${momoProducts.length} 筆商品`;
document.getElementById('momoCount').classList.replace('bg-secondary', 'bg-warning');
document.getElementById('momoCount').className = 'price-count-badge is-momo';
updateCompareButton();
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
showToast(`成功解析 ${momoProducts.length} 筆 MOMO 商品`, 'success');
@@ -424,11 +572,11 @@ La Roche-Posay 安得利防曬液 50ml,920
if (currentManualSource === 'pchome') {
pchomeProducts = products;
document.getElementById('pchomeCount').textContent = `${products.length} 筆商品`;
document.getElementById('pchomeCount').classList.replace('bg-secondary', 'bg-info');
document.getElementById('pchomeCount').className = 'price-count-badge is-pchome';
} else {
momoProducts = products;
document.getElementById('momoCount').textContent = `${products.length} 筆商品`;
document.getElementById('momoCount').classList.replace('bg-secondary', 'bg-warning');
document.getElementById('momoCount').className = 'price-count-badge is-momo';
}
updateCompareButton();
@@ -487,8 +635,8 @@ La Roche-Posay 安得利防曬液 50ml,920
const similarityClass = m.similarity >= 0.8 ? 'text-success' : (m.similarity >= 0.6 ? 'text-warning' : 'text-danger');
const cheaperBadge = m.cheaper_at === 'pchome'
? '<span class="badge bg-info">PChome</span>'
: (m.cheaper_at === 'momo' ? '<span class="badge bg-warning text-dark">MOMO</span>' : '<span class="badge bg-secondary">相同</span>');
? '<span class="price-result-badge is-pchome">PChome</span>'
: (m.cheaper_at === 'momo' ? '<span class="price-result-badge is-momo">MOMO</span>' : '<span class="price-result-badge is-muted">相同</span>');
// 處理 URL (可能是 url 或 product_url)
const pchomeUrl = m.pchome.url || m.pchome.product_url || '';
@@ -619,8 +767,7 @@ La Roche-Posay 安得利防曬液 50ml,920
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} position-fixed`;
toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
toast.className = `price-toast price-toast--${type} position-fixed`;
toast.innerHTML = `
<button type="button" class="btn-close float-end" onclick="this.parentElement.remove()"></button>
${message}

View File

@@ -2,8 +2,47 @@
{% block title %}趨勢資料 - EwoooC{% endblock %}
{% block extra_css %}
<style>
.trends-page {
color: var(--momo-text-primary);
}
.trends-table-head th {
background: var(--momo-bg-paper) !important;
color: var(--momo-text-secondary) !important;
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
letter-spacing: 0.04em;
}
.trends-empty {
padding: var(--momo-space-5) !important;
color: var(--momo-text-secondary) !important;
text-align: center;
}
.trends-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 3px 8px;
background: var(--momo-tag-caramel-bg);
border: 1px solid var(--momo-tag-caramel-border);
border-radius: var(--momo-radius-sm);
color: var(--momo-tag-caramel-text);
font-family: var(--momo-font-mono, monospace);
font-size: var(--momo-text-label);
font-weight: 800;
line-height: 1;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="container-fluid py-4 trends-page">
<div class="page-header">
<h1><i class="fas fa-chart-line me-2"></i>趨勢資料</h1>
<p>Google News、PTT、Dcard、YouTube 趨勢訊號</p>
@@ -50,7 +89,7 @@
<div class="card-header"><i class="fas fa-newspaper me-2"></i>趨勢記錄</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead class="trends-table-head">
<tr>
<th>日期</th>
<th>來源</th>
@@ -101,7 +140,7 @@ async function loadTrends() {
const recordsBody = document.getElementById('recordsBody');
if (!records.success || !records.data.length) {
recordsBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-4">尚無趨勢資料</td></tr>';
recordsBody.innerHTML = '<tr><td colspan="5" class="trends-empty">尚無趨勢資料</td></tr>';
} else {
recordsBody.innerHTML = records.data.map(item => `
<tr>
@@ -116,12 +155,12 @@ async function loadTrends() {
const keywordsBox = document.getElementById('keywordsBox');
if (!keywords.success || !keywords.data.length) {
keywordsBox.innerHTML = '<div class="text-muted text-center py-4">尚無關鍵字</div>';
keywordsBox.innerHTML = '<div class="trends-empty">尚無關鍵字</div>';
} else {
keywordsBox.innerHTML = keywords.data.map(item => `
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
<span>${escapeHtml(item.keyword)}</span>
<span class="badge text-bg-primary">${escapeHtml(item.total_mentions || 0)}</span>
<span class="trends-count-badge">${escapeHtml(item.total_mentions || 0)}</span>
</div>
`).join('');
}