fix: make pchome source exports operator friendly
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s

This commit is contained in:
ogt
2026-06-26 19:16:25 +08:00
parent f823439496
commit fdaa4bb2c9
4 changed files with 99 additions and 33 deletions

View File

@@ -78,6 +78,31 @@
color: var(--momo-tag-muted-text);
}
.pchome-product-title {
color: var(--momo-text-primary);
font-weight: 800;
line-height: 1.45;
}
.pchome-product-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
color: var(--momo-text-secondary);
font-size: var(--momo-text-small);
}
.pchome-product-meta span {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 2px 7px;
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-sm);
background: var(--momo-bg-paper);
}
.pchome-toast {
top: 20px;
right: 20px;
@@ -127,7 +152,7 @@
<div class="container-fluid py-4 pchome-tool-page">
<header class="pchome-tool-head mb-4">
<h2><i class="fas fa-magnifying-glass-chart me-2"></i>PChome 24h 商品監控</h2>
<p class="text-muted">補齊 PChome 商品資料,支援同款與價差判斷</p>
<p class="text-muted">補齊 PChome 商品、售價、庫存與賣場連結,支援同款確認、價差與促銷監控</p>
</header>
<!-- 資料取得方式選擇 -->
@@ -237,8 +262,8 @@
<button class="btn btn-sm btn-outline-primary me-2" id="exportExcelBtn">
<i class="fas fa-file-excel me-1"></i>下載表格
</button>
<button class="btn btn-sm btn-outline-secondary" id="exportJsonBtn">
<i class="fas fa-file-alt me-1"></i>下載完整清單
<button class="btn btn-sm btn-outline-secondary" id="exportStoreLinksBtn">
<i class="fas fa-link me-1"></i>下載賣場清單
</button>
</div>
</div>
@@ -343,7 +368,7 @@
// 匯出
document.getElementById('exportExcelBtn').addEventListener('click', exportExcel);
document.getElementById('exportJsonBtn').addEventListener('click', exportJson);
document.getElementById('exportStoreLinksBtn').addEventListener('click', exportStoreLinks);
}
async function crawlRegion() {
@@ -465,33 +490,47 @@
tbody.innerHTML = '';
for (const p of products) {
const discount = p.discount ? `<span class="pchome-badge is-danger">-${p.discount}%</span>` : '-';
const stockBadge = p.stock > 0
? `<span class="pchome-badge is-success">${p.stock}</span>`
const productName = String(p.name || 'PChome 商品');
const productId = String(p.product_id || '').trim();
const productUrl = String(p.product_url || '').trim();
const imageUrl = String(p.image_url || '').trim();
const price = toNumber(p.price);
const originalPrice = toNumber(p.original_price);
const discountValue = toNumber(p.discount);
const stock = toNumber(p.stock);
const discount = discountValue > 0 ? `<span class="pchome-badge is-danger">-${discountValue}%</span>` : '-';
const stockBadge = stock > 0
? `<span class="pchome-badge is-success">${stock}</span>`
: `<span class="pchome-badge is-muted">缺貨</span>`;
const imageSrc = imageUrl ? `${escapeHtml(imageUrl)}?width=80` : '/static/images/no-image.png';
const productTitle = `${escapeHtml(productName.substring(0, 50))}${productName.length > 50 ? '...' : ''}`;
const productLink = productUrl
? `<a href="${escapeHtml(productUrl)}" target="_blank" rel="noopener noreferrer" class="text-decoration-none pchome-product-title">${productTitle}</a>`
: `<span class="pchome-product-title">${productTitle}</span>`;
const storeAction = productUrl
? `<a href="${escapeHtml(productUrl)}" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-primary"><i class="fas fa-up-right-from-square me-1"></i>賣場</a>`
: `<button type="button" class="btn btn-sm btn-outline-secondary" disabled>待補</button>`;
const row = document.createElement('tr');
row.innerHTML = `
<td>
<img src="${p.image_url}?width=80" alt="" class="img-thumbnail"
<img src="${imageSrc}" alt="${escapeHtml(productName)}" class="img-thumbnail"
style="width: 60px; height: 60px; object-fit: cover;"
onerror="this.src='/static/images/no-image.png'">
</td>
<td>
<a href="${p.product_url}" target="_blank" class="text-decoration-none">
${escapeHtml(p.name.substring(0, 50))}${p.name.length > 50 ? '...' : ''}
</a>
<br>
<small class="text-muted">${p.product_id}</small>
${productLink}
<div class="pchome-product-meta">
<span>商品編號 ${escapeHtml(productId || '待補')}</span>
<span>PChome 24h 官方賣場</span>
</div>
</td>
<td class="text-danger fw-bold">$${p.price.toLocaleString()}</td>
<td class="text-muted"><s>$${p.original_price.toLocaleString()}</s></td>
<td class="text-danger fw-bold">${formatMoney(price)}</td>
<td class="text-muted">${originalPrice ? `<s>${formatMoney(originalPrice)}</s>` : '-'}</td>
<td>${discount}</td>
<td>${stockBadge}</td>
<td>
<a href="${p.product_url}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="fas fa-external-link-alt"></i>
</a>
${storeAction}
</td>
`;
tbody.appendChild(row);
@@ -504,31 +543,36 @@
return;
}
// 建立 CSV
const headers = ['商品ID', '名稱', '售價', '原價', '折扣%', '庫存', '圖片URL', '商品URL'];
const headers = ['商品編號', '商品名稱', 'PChome 售價', 'PChome 原價', '折扣', '庫存狀態', '商品圖片', 'PChome 賣場'];
const rows = currentProducts.map(p => [
p.product_id,
`"${p.name.replace(/"/g, '""')}"`,
p.price,
p.original_price,
csvCell(p.product_id || ''),
csvCell(p.name || ''),
toNumber(p.price),
toNumber(p.original_price),
p.discount || '',
p.stock,
p.image_url,
p.product_url
toNumber(p.stock),
csvCell(p.image_url || ''),
csvCell(p.product_url || '')
]);
const csv = '\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
downloadFile(csv, 'pchome_products.csv', 'text/csv;charset=utf-8');
}
function exportJson() {
function exportStoreLinks() {
if (!currentProducts.length) {
showToast('沒有資料可匯出', 'warning');
return;
}
const json = JSON.stringify(currentProducts, null, 2);
downloadFile(json, 'pchome_products.json', 'application/json');
const headers = ['商品編號', '商品名稱', 'PChome 賣場'];
const rows = currentProducts.map(p => [
csvCell(p.product_id || ''),
csvCell(p.name || ''),
csvCell(p.product_url || '')
]);
const csv = '\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
downloadFile(csv, 'pchome_store_links.csv', 'text/csv;charset=utf-8');
}
function downloadFile(content, filename, type) {
@@ -547,6 +591,19 @@
return div.innerHTML;
}
function toNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function formatMoney(value) {
return `$${toNumber(value).toLocaleString()}`;
}
function csvCell(value) {
return `"${String(value).replace(/"/g, '""')}"`;
}
function showToast(message, type = 'info') {
// 簡易 Toast
const toast = document.createElement('div');