Skip to content

Commit fd66ddf

Browse files
committed
feat: add recent plays list with pagination to overview tab
1 parent 8845de6 commit fd66ddf

3 files changed

Lines changed: 161 additions & 2 deletions

File tree

scripts/db.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,19 @@ def get_daily_genre_stats() -> list[dict]:
347347
return list(result.values())
348348

349349

350+
def get_all_plays() -> list[dict]:
351+
"""获取所有观影记录(按观看时间倒序),关联媒体表获取海报。"""
352+
conn = get_conn()
353+
rows = conn.execute("""
354+
SELECT p.*, m.poster_url
355+
FROM plays p
356+
LEFT JOIN media m ON m.trakt_id = p.media_trakt_id
357+
ORDER BY p.watched_at DESC
358+
""").fetchall()
359+
conn.close()
360+
return [dict(r) for r in rows]
361+
362+
350363
def ensure_dirs():
351364
"""确保 data 和 web 相关目录存在。"""
352365
os.makedirs(DB_PATH.replace("trakt.db", ""), exist_ok=True)

scripts/render.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_genre_stats,
1414
get_all_media,
1515
get_daily_genre_stats,
16+
get_all_plays,
1617
ensure_dirs,
1718
)
1819

@@ -48,6 +49,12 @@ def run():
4849
json.dump(all_media, f, ensure_ascii=False, indent=2)
4950
print(f"[Render] 已生成 media.json({len(all_media)} 个媒体)")
5051

52+
# ── 最近观影记录 ──
53+
all_plays = get_all_plays()
54+
with open(f"{WEB_DATA_DIR}/recent.json", "w", encoding="utf-8") as f:
55+
json.dump(all_plays, f, ensure_ascii=False, indent=2)
56+
print(f"[Render] 已生成 recent.json({len(all_plays)} 条记录)")
57+
5158

5259
if __name__ == "__main__":
5360
run()

web/index.html

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,58 @@
140140
.heatmap-legend .lv3 { background: #26a641; }
141141
.heatmap-legend .lv4 { background: #39d353; }
142142

143+
/* ── 最近观影列表 ── */
144+
.recent-list { display: flex; flex-direction: column; gap: 1px; }
145+
.recent-item {
146+
display: flex; align-items: center; gap: 16px;
147+
padding: 14px 16px; border-radius: var(--radius-sm);
148+
transition: background var(--transition); cursor: default;
149+
}
150+
.recent-item:hover { background: var(--surface-hover); }
151+
.recent-poster {
152+
width: 48px; height: 72px; border-radius: 6px;
153+
object-fit: cover; background: rgba(48,54,61,0.4); flex-shrink: 0;
154+
}
155+
.recent-poster-placeholder {
156+
width: 48px; height: 72px; border-radius: 6px;
157+
background: rgba(48,54,61,0.4); flex-shrink: 0;
158+
display: flex; align-items: center; justify-content: center;
159+
font-size: 1.2rem; color: var(--muted);
160+
}
161+
.recent-info { flex: 1; min-width: 0; }
162+
.recent-title {
163+
font-weight: 600; color: var(--text-bright); font-size: 0.95rem;
164+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px;
165+
}
166+
.recent-meta {
167+
display: flex; align-items: center; gap: 8px;
168+
font-size: 0.8rem; color: var(--muted); flex-wrap: wrap;
169+
}
170+
.recent-meta .sep { color: rgba(72,79,88,0.6); }
171+
.recent-type {
172+
padding: 1px 6px; border-radius: 4px; font-size: 0.72rem; font-weight: 600;
173+
}
174+
.recent-type.movie { background: rgba(88,166,255,0.15); color: var(--primary); }
175+
.recent-type.episode { background: rgba(139,92,246,0.15); color: #8b5cf6; }
176+
.recent-date { margin-left: auto; font-size: 0.78rem; color: var(--muted); white-space: nowrap; flex-shrink: 0; }
177+
178+
/* ── 分页 ── */
179+
.pagination {
180+
display: flex; align-items: center; justify-content: center; gap: 12px;
181+
margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border);
182+
}
183+
.pagination button {
184+
padding: 6px 14px; border: 1px solid var(--border); border-radius: var(--radius-sm);
185+
background: var(--surface); color: var(--text); font-size: 0.85rem;
186+
cursor: pointer; transition: all var(--transition);
187+
}
188+
.pagination button:hover:not(:disabled) {
189+
border-color: var(--primary); color: var(--text-bright);
190+
background: rgba(88,166,255,0.08);
191+
}
192+
.pagination button:disabled { opacity: 0.35; cursor: not-allowed; }
193+
.pagination .page-info { font-size: 0.85rem; color: var(--muted); }
194+
143195
/* ── 动画 ── */
144196
@keyframes fadeInUp {
145197
from { opacity: 0; transform: translateY(20px); }
@@ -201,6 +253,13 @@ <h1>trakt<span>Daily</span></h1>
201253
<h2><span class="icon">📈</span> 月度观影趋势</h2>
202254
<div id="trend-chart" class="chart-box"></div>
203255
</div>
256+
<div class="card animate-in" style="margin-top:24px;">
257+
<h2><span class="icon">🕐</span> 最近观影</h2>
258+
<div id="recent-list" class="recent-list">
259+
<div class="empty-state"><p>加载中...</p></div>
260+
</div>
261+
<div id="recent-pagination" class="pagination" style="display:none;"></div>
262+
</div>
204263
</div>
205264

206265
<!-- ── Tab: 热力图 ── -->
@@ -254,17 +313,22 @@ <h2><span class="icon">📅</span> 年度数据对比</h2>
254313
// ── 全局状态 ──
255314
let appData = null;
256315
let mediaList = [];
316+
let recentPlays = [];
317+
let recentPage = 1;
318+
const PAGE_SIZE = 10;
257319
let charts = {};
258320

259321
// ── 数据加载 ──
260322
async function loadData() {
261323
try {
262-
const [summaryResp, mediaResp] = await Promise.all([
324+
const [summaryResp, mediaResp, recentResp] = await Promise.all([
263325
fetch('data/summary.json'),
264-
fetch('data/media.json').catch(() => null)
326+
fetch('data/media.json').catch(() => null),
327+
fetch('data/recent.json').catch(() => null)
265328
]);
266329
appData = await summaryResp.json();
267330
mediaList = mediaResp ? await mediaResp.json() : [];
331+
recentPlays = recentResp ? await recentResp.json() : [];
268332
renderAll();
269333
} catch (err) {
270334
document.getElementById('stats-cards').innerHTML =
@@ -279,6 +343,7 @@ <h2><span class="icon">📅</span> 年度数据对比</h2>
279343
renderHeatmap();
280344
renderGenrePies();
281345
renderCharts();
346+
renderRecent();
282347
}
283348

284349
// ── 统计数字跳动动画 ──
@@ -876,6 +941,80 @@ <h2><span class="icon">📅</span> 年度数据对比</h2>
876941
});
877942
}
878943

944+
// ── 最近观影 ──
945+
function formatDate(dateStr) {
946+
if (!dateStr) return '';
947+
try {
948+
const d = new Date(dateStr);
949+
if (isNaN(d.getTime())) return dateStr.substring(0, 10);
950+
const pad = n => String(n).padStart(2, '0');
951+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
952+
} catch {
953+
return dateStr.substring(0, 10);
954+
}
955+
}
956+
957+
function renderRecent(page) {
958+
if (page === undefined) page = recentPage;
959+
recentPage = page;
960+
961+
const listEl = document.getElementById('recent-list');
962+
const paginationEl = document.getElementById('recent-pagination');
963+
if (!listEl || !paginationEl) return;
964+
965+
if (!recentPlays.length) {
966+
listEl.innerHTML = '<div class="empty-state"><div class="icon">🎬</div><p>暂无观影记录</p></div>';
967+
paginationEl.style.display = 'none';
968+
return;
969+
}
970+
971+
const totalPages = Math.ceil(recentPlays.length / PAGE_SIZE);
972+
const start = (page - 1) * PAGE_SIZE;
973+
const pageItems = recentPlays.slice(start, start + PAGE_SIZE);
974+
975+
listEl.innerHTML = pageItems.map(item => {
976+
const title = item.title || 'Unknown';
977+
const mediaType = item.media_type || '';
978+
const year = item.year || '';
979+
const runtime = item.runtime ? `${item.runtime} min` : '';
980+
const watchedAt = formatDate(item.watched_at_local);
981+
const posterUrl = item.poster_url;
982+
983+
const typeClass = mediaType === 'movie' ? 'movie' : 'episode';
984+
const typeLabel = mediaType === 'movie' ? '电影' : '剧集';
985+
const typeIcon = mediaType === 'movie' ? '🎥' : '📺';
986+
987+
return `
988+
<div class="recent-item">
989+
${posterUrl
990+
? `<img class="recent-poster" src="${posterUrl}" alt="" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">`
991+
: ''}
992+
<div class="recent-poster-placeholder" style="${posterUrl ? '' : 'display:flex'}">${typeIcon}</div>
993+
<div class="recent-info">
994+
<div class="recent-title" title="${title}">${title}</div>
995+
<div class="recent-meta">
996+
<span class="recent-type ${typeClass}">${typeLabel}</span>
997+
${year ? `<span class="sep">·</span><span>${year}</span>` : ''}
998+
${runtime ? `<span class="sep">·</span><span>${runtime}</span>` : ''}
999+
</div>
1000+
</div>
1001+
<div class="recent-date">${watchedAt}</div>
1002+
</div>
1003+
`;
1004+
}).join('');
1005+
1006+
if (totalPages <= 1) {
1007+
paginationEl.style.display = 'none';
1008+
} else {
1009+
paginationEl.style.display = 'flex';
1010+
paginationEl.innerHTML = `
1011+
<button ${page <= 1 ? 'disabled' : ''} onclick="renderRecent(${page - 1})">← 上一页</button>
1012+
<span class="page-info">第 ${page} / ${totalPages} 页</span>
1013+
<button ${page >= totalPages ? 'disabled' : ''} onclick="renderRecent(${page + 1})">下一页 →</button>
1014+
`;
1015+
}
1016+
}
1017+
8791018
// ── Tab 切换 ──
8801019
function initTabs() {
8811020
const buttons = document.querySelectorAll('.tab-btn');

0 commit comments

Comments
 (0)