ADR-017 Phase 3f-4:根目錄模板搬入 templates/,補 trends/login_history,移除 ChoiceLoader 根目錄 fallback,搬移 components,刪除 web/templates 下的空檔/死檔與 compose 舊模板 mount。
136 lines
5.3 KiB
HTML
136 lines
5.3 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}趨勢資料 - WOOO TECH{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<div class="page-header">
|
|
<h1><i class="fas fa-chart-line me-2"></i>趨勢資料</h1>
|
|
<p>Google News、PTT、Dcard、YouTube 趨勢訊號</p>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="sourceFilter">
|
|
<option value="">全部來源</option>
|
|
<option value="google_news">Google News</option>
|
|
<option value="ptt">PTT</option>
|
|
<option value="dcard">Dcard</option>
|
|
<option value="youtube">YouTube</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<input class="form-control" id="categoryFilter" placeholder="分類">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="daysFilter">
|
|
<option value="1">近 1 天</option>
|
|
<option value="7" selected>近 7 天</option>
|
|
<option value="30">近 30 天</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button class="btn btn-primary w-100" id="refreshBtn">
|
|
<i class="fas fa-rotate me-1"></i>更新
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-lg-4">
|
|
<div class="card h-100">
|
|
<div class="card-header"><i class="fas fa-fire me-2"></i>熱門關鍵字</div>
|
|
<div class="card-body" id="keywordsBox">
|
|
<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-8">
|
|
<div class="card h-100">
|
|
<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">
|
|
<tr>
|
|
<th>日期</th>
|
|
<th>來源</th>
|
|
<th>分類</th>
|
|
<th>標題</th>
|
|
<th>熱度</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="recordsBody">
|
|
<tr><td colspan="5" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function escapeHtml(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[char]));
|
|
}
|
|
|
|
function filters() {
|
|
const params = new URLSearchParams();
|
|
const source = document.getElementById('sourceFilter').value;
|
|
const category = document.getElementById('categoryFilter').value.trim();
|
|
const days = document.getElementById('daysFilter').value;
|
|
if (source) params.set('source', source);
|
|
if (category) params.set('category', category);
|
|
params.set('days', days);
|
|
return params;
|
|
}
|
|
|
|
async function loadTrends() {
|
|
const params = filters();
|
|
params.set('limit', '50');
|
|
const [recordsResponse, keywordsResponse] = await Promise.all([
|
|
fetch(`/api/trends/records?${params}`),
|
|
fetch(`/api/trends/keywords?${params}`)
|
|
]);
|
|
const records = await recordsResponse.json();
|
|
const keywords = await keywordsResponse.json();
|
|
|
|
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>';
|
|
} else {
|
|
recordsBody.innerHTML = records.data.map(item => `
|
|
<tr>
|
|
<td>${escapeHtml(item.trend_date)}</td>
|
|
<td>${escapeHtml(item.source)}</td>
|
|
<td>${escapeHtml(item.category)}</td>
|
|
<td class="text-truncate" style="max-width: 420px;">${escapeHtml(item.title || item.keyword)}</td>
|
|
<td>${escapeHtml(item.popularity_score || '')}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
const keywordsBox = document.getElementById('keywordsBox');
|
|
if (!keywords.success || !keywords.data.length) {
|
|
keywordsBox.innerHTML = '<div class="text-muted text-center py-4">尚無關鍵字</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>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
document.getElementById('refreshBtn').addEventListener('click', loadTrends);
|
|
document.getElementById('sourceFilter').addEventListener('change', loadTrends);
|
|
document.getElementById('daysFilter').addEventListener('change', loadTrends);
|
|
document.addEventListener('DOMContentLoaded', loadTrends);
|
|
</script>
|
|
{% endblock %}
|