feat: show product identity in ai recommendations
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
ogt
2026-06-26 18:33:11 +08:00
parent 1dfeee0506
commit c268b5cc02
8 changed files with 226 additions and 25 deletions

View File

@@ -123,6 +123,113 @@
.ai-recommend-page .ar-card--insights { border-color: var(--momo-warm-caramel, #c96442) !important; }
.ai-recommend-page .ar-card--trends { border-color: var(--momo-warm-olive, #6f7a4a) !important; }
/* ── Product identity cards ────────────────────────── */
.ai-recommend-page .ar-product-card {
display: grid;
grid-template-columns: 1.6rem 4.5rem minmax(0, 1fr) auto;
gap: 0.7rem;
align-items: center;
min-height: 5.25rem;
padding: 0.7rem 0.8rem;
border-bottom: 1px solid var(--momo-border-subtle);
cursor: pointer;
}
.ai-recommend-page .ar-product-card:hover {
background: color-mix(in srgb, var(--momo-page-accent) 5%, var(--momo-surface));
}
.ai-recommend-page .ar-product-card__rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.55rem;
height: 1.55rem;
border-radius: 999px;
background: var(--momo-text-strong);
color: var(--momo-on-accent, #fff8ef);
font-size: 0.75rem;
font-weight: 800;
}
.ai-recommend-page .ar-product-card__media {
width: 4.5rem;
height: 4.5rem;
}
.ai-recommend-page .ar-product-card__img,
.ai-recommend-page .ar-product-card__missing-img {
width: 100%;
height: 100%;
border: 1px solid var(--momo-border-subtle);
border-radius: var(--momo-radius-sm, 6px);
background: var(--momo-surface-2);
}
.ai-recommend-page .ar-product-card__img {
display: block;
object-fit: contain;
}
.ai-recommend-page .ar-product-card__missing-img {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
color: var(--momo-text-tertiary);
font-size: 0.68rem;
font-weight: 800;
}
.ai-recommend-page .ar-product-card__body {
min-width: 0;
}
.ai-recommend-page .ar-product-card__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.25rem;
}
.ai-recommend-page .ar-product-card__id {
color: var(--momo-text-tertiary);
font-size: 0.72rem;
font-weight: 800;
}
.ai-recommend-page .ar-product-card__name {
overflow: hidden;
color: var(--momo-text-strong);
font-size: 0.86rem;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-recommend-page .ar-product-card__price {
margin-top: 0.2rem;
color: var(--momo-warm-rose, #a84428);
font-size: 0.86rem;
font-weight: 900;
}
.ai-recommend-page .ar-product-card__actions {
display: flex;
justify-content: flex-end;
min-width: 5.25rem;
}
.ai-recommend-page .ar-product-card__link {
white-space: nowrap;
}
.ai-recommend-page .ar-product-card__pending {
white-space: nowrap;
}
@media (max-width: 640px) {
.ai-recommend-page .ar-product-card {
grid-template-columns: 1.6rem 3.8rem minmax(0, 1fr);
}
.ai-recommend-page .ar-product-card__media {
width: 3.8rem;
height: 3.8rem;
}
.ai-recommend-page .ar-product-card__actions {
grid-column: 2 / -1;
justify-content: flex-start;
}
}
/* ── Dropdown / quick tags / keywords ──────────────── */
.ai-recommend-page .ar-dropdown { max-height: 300px; overflow-y: auto; }
.ai-recommend-page .ar-quick-tags { display: flex; flex-wrap: wrap; gap: 6px; }

View File

@@ -49,7 +49,7 @@
// 載入熱銷商品
function loadBestsellers() {
const category = document.getElementById('bestsellersCategory').value;
const platform = document.querySelector('input[name="platform"]:checked')?.value || 'momo';
const platform = document.querySelector('input[name="platform"]:checked')?.value || 'pchome';
const container = document.getElementById('bestsellersCard');
container.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>';
@@ -61,16 +61,8 @@
})
.then(data => {
if (data.success && data.data.products?.length) {
container.innerHTML = data.data.products.map((p, i) => `
<div class="d-flex align-items-center px-3 py-2 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(p.name)}')">
<span class="badge bg-secondary me-2">${i + 1}</span>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block">${p.name}</small>
<small class="text-muted">$${p.price?.toLocaleString() || 'N/A'}</small>
</div>
<a href="${p.url}" target="_blank" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()" title="前往電商網站查看商品"><i class="fas fa-external-link-alt"></i></a>
</div>
`).join('') + `<div class="text-center text-muted small py-1">${data.data.source}</div>`;
container.innerHTML = data.data.products.map((p, i) => renderBestsellerCard(p, i, data.data.platform)).join('')
+ `<div class="text-center text-muted small py-1">${escapeHtml(data.data.source || '')}</div>`;
} else {
container.innerHTML = '<p class="text-muted text-center py-3 mb-0">無法載入熱銷商品</p>';
}
@@ -78,6 +70,54 @@
.catch(e => container.innerHTML = '<p class="text-danger text-center py-3 mb-0">載入失敗</p>');
}
function normalizeBestsellerProduct(product, platform) {
const p = product || {};
return {
name: String(p.name || '').trim(),
productId: String(p.product_id || p.id || p.i_code || p.goodsCode || '').trim(),
platform: String(p.platform || platform || '').toLowerCase(),
price: Number(p.price || 0),
url: String(p.product_url || p.url || '').trim(),
imageUrl: String(p.image_url || p.image || '').trim()
};
}
function platformLabel(platform) {
return String(platform || '').toLowerCase() === 'momo' ? 'MOMO' : 'PChome';
}
function renderProductThumb(product) {
const image = product.imageUrl;
if (image && /^https?:\/\//i.test(image)) {
return `<img class="ar-product-card__img" src="${escapeHtml(image)}" alt="${escapeHtml(product.name)}" loading="lazy" referrerpolicy="no-referrer">`;
}
return `<div class="ar-product-card__missing-img"><i class="fas fa-image"></i><span>待補圖片</span></div>`;
}
function renderBestsellerCard(product, index, platform) {
const p = normalizeBestsellerProduct(product, platform);
const label = platformLabel(p.platform);
const price = p.price > 0 ? `NT$ ${p.price.toLocaleString()}` : '待補價格';
const productId = p.productId || '待補';
const storeAction = p.url
? `<a href="${escapeHtml(p.url)}" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-primary ar-product-card__link" onclick="event.stopPropagation()" title="開啟賣場"><i class="fas fa-up-right-from-square me-1"></i>開賣場</a>`
: '<span class="badge bg-light text-muted border ar-product-card__pending">待補連結</span>';
return `
<div class="ar-product-card" data-product-name="${escapeHtml(p.name)}" data-product-id="${escapeHtml(p.productId)}" data-product-url="${escapeHtml(p.url)}" data-platform="${escapeHtml(p.platform)}" data-price="${p.price}" onclick="setProductFromCard(this)">
<div class="ar-product-card__rank">${index + 1}</div>
<div class="ar-product-card__media">${renderProductThumb(p)}</div>
<div class="ar-product-card__body">
<div class="ar-product-card__meta">
<span class="badge bg-light text-dark border">${label}</span>
<span class="ar-product-card__id">商品 ID ${escapeHtml(productId)}</span>
</div>
<div class="ar-product-card__name">${escapeHtml(p.name || '待補商品名稱')}</div>
<div class="ar-product-card__price">${price}</div>
</div>
<div class="ar-product-card__actions">${storeAction}</div>
</div>`;
}
// 載入 COSME 排行榜
function loadCosmeRankings() {
const category = document.getElementById('cosmeCategory').value;
@@ -93,7 +133,7 @@
.then(data => {
if (data.success && data.data.products?.length) {
container.innerHTML = data.data.products.map((p, i) => `
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(p.name)}')">
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" data-product-name="${escapeHtml(p.name)}" onclick="setProductFromCard(this)">
<span class="badge ${i < 3 ? 'bg-warning text-dark' : 'bg-secondary'} me-2" style="font-size: 0.7rem;">${p.rank}</span>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block" style="font-size: 0.8rem;">${p.brand} ${p.name}</small>
@@ -123,7 +163,7 @@
.then(data => {
if (data.success && data.data.articles?.length) {
container.innerHTML = data.data.articles.map(a => `
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(a.title)}')">
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" data-product-name="${escapeHtml(a.title)}" onclick="setProductFromCard(this)">
<i class="fas fa-star text-success me-2" style="font-size: 0.7rem;"></i>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block" style="font-size: 0.8rem;">${a.title}</small>
@@ -212,7 +252,7 @@
}
container.innerHTML = items.slice(0, 20).map(n => `
<div class="d-flex align-items-center px-3 py-2 border-bottom news-item" style="cursor: pointer;" onclick="setProduct('${escapeHtml(n.title || n.query || '')}')">
<div class="d-flex align-items-center px-3 py-2 border-bottom news-item" style="cursor: pointer;" data-product-name="${escapeHtml(n.title || n.query || '')}" onclick="setProductFromCard(this)">
<i class="fas ${icon} ${iconClass} me-2"></i>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block fw-medium">${n.title || n.query || ''}</small>
@@ -319,6 +359,11 @@
document.getElementById('productName').value = name.substring(0, 100);
}
function setProductFromCard(card) {
const name = card?.dataset?.productName || '';
if (name) setProduct(name);
}
// 切換關鍵字
function toggleKeyword(el) {
el.classList.toggle('is-selected');
@@ -347,12 +392,16 @@
// 取得熱銷商品資訊
function getBestsellersForAPI() {
const items = document.querySelectorAll('#bestsellersCard > div.d-flex');
const items = document.querySelectorAll('#bestsellersCard .ar-product-card');
return Array.from(items).slice(0, 3).map(el => {
const name = el.querySelector('small.text-truncate')?.textContent || '';
const priceText = el.querySelector('small.text-muted')?.textContent || '';
const price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
return { name, price };
const price = parseInt(el.dataset.price || '0', 10) || 0;
return {
name: el.dataset.productName || '',
price,
product_id: el.dataset.productId || '',
platform: el.dataset.platform || '',
url: el.dataset.productUrl || ''
};
});
}
@@ -1001,6 +1050,27 @@
document.getElementById('productName').value = title;
}
Object.assign(window, {
addKeywordFromInsight,
copyCopyText,
doProductInsights,
doWebSearch,
generateCopy,
loadBestsellers,
loadCosmeRankings,
loadMybestArticles,
loadTrends,
onProviderChange,
quickWebSearch,
refreshTrends,
setProduct,
setProductFromCard,
switchTrendTab,
toggleKeyword,
useTrendForProduct,
useTrendKeyword
});
// 初始化
document.addEventListener('DOMContentLoaded', function() {
initAIProvider();