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