@@ -95,11 +95,18 @@ async function reloadSource(id) {
9595}
9696
9797async function viewChannels ( sourceId ) {
98+ const section = document . getElementById ( 'channelSection' ) ;
99+ const container = document . getElementById ( 'channelGroups' ) ;
100+ const countBadge = document . getElementById ( 'channelCount' ) ;
101+ section . style . display = 'block' ;
102+ countBadge . textContent = '' ;
103+ container . innerHTML = '<div class="loading-state"><span class="spinner"></span>正在加载频道列表…</div>' ;
98104 try {
99105 const res = await fetch ( `${ API } /api/sources/${ sourceId } /channels` ) ;
100106 const data = await res . json ( ) ;
101107 renderChannels ( data ) ;
102108 } catch ( err ) {
109+ container . innerHTML = '<p class="empty-hint">频道加载失败</p>' ;
103110 console . error ( 'Failed to load channels:' , err ) ;
104111 }
105112}
@@ -129,11 +136,11 @@ function renderChannels(data) {
129136 </div>
130137 <div class="group-channels">
131138 ${ channels . map ( ch => `
132- <div class="channel-item">
139+ <div class="channel-item" onclick="showEpg( ${ ch . id } , ' ${ escapeHtml ( ch . displayName ) . replace ( / ' / g , "\\'" ) } ')" >
133140 ${ ch . logoUrl ? `<img src="${ escapeHtml ( ch . logoUrl ) } " alt="" onerror="this.style.display='none'">` : '' }
134141 <span>${ escapeHtml ( ch . displayName ) } </span>
135142 <button class="btn-fav ${ favoriteIds . has ( ch . id ) ? 'is-fav' : '' } "
136- onclick="toggleFavorite(${ ch . id } , this)" title="${ favoriteIds . has ( ch . id ) ? '取消收藏' : '收藏' } ">
143+ onclick="event.stopPropagation(); toggleFavorite(${ ch . id } , this)" title="${ favoriteIds . has ( ch . id ) ? '取消收藏' : '收藏' } ">
137144 ${ favoriteIds . has ( ch . id ) ? '★' : '☆' }
138145 </button>
139146 </div>
@@ -254,6 +261,82 @@ function showToast(msg, type) {
254261 setTimeout ( ( ) => { toast . className = 'toast' ; } , 3000 ) ;
255262}
256263
264+ async function loadVersion ( ) {
265+ try {
266+ const res = await fetch ( `${ API } /api/version` ) ;
267+ const data = await res . json ( ) ;
268+ document . getElementById ( 'appVersion' ) . textContent = 'v' + ( data . version || '' ) ;
269+ } catch ( err ) {
270+ console . error ( 'Failed to load version:' , err ) ;
271+ }
272+ }
273+
274+ const EPG_LIST_LIMIT = 48 ;
275+
276+ async function showEpg ( channelId , channelName ) {
277+ const modal = document . getElementById ( 'epgModal' ) ;
278+ const title = document . getElementById ( 'epgModalTitle' ) ;
279+ const body = document . getElementById ( 'epgModalBody' ) ;
280+
281+ title . textContent = channelName ;
282+ body . innerHTML = '<div class="loading-state"><span class="spinner"></span>正在加载节目信息…</div>' ;
283+ modal . classList . add ( 'show' ) ;
284+
285+ try {
286+ const res = await fetch ( `${ API } /api/epg/channel/${ channelId } ?limit=${ EPG_LIST_LIMIT } ` ) ;
287+ const data = await res . json ( ) ;
288+ const programs = data . programs || [ ] ;
289+ if ( programs . length === 0 ) {
290+ body . innerHTML = '<p class="epg-empty">暂无节目信息</p>' ;
291+ return ;
292+ }
293+ const fmt = ts => {
294+ const d = new Date ( ts ) ;
295+ return d . getHours ( ) . toString ( ) . padStart ( 2 , '0' ) + ':' + d . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
296+ } ;
297+ const now = Date . now ( ) ;
298+ const curIdx = programs . findIndex ( p => p . startTime <= now && p . endTime > now ) ;
299+ const nextIdx = curIdx === - 1 ? 0 : curIdx + 1 ;
300+
301+ function epgLabel ( i ) {
302+ if ( i === curIdx ) return '当前播出' ;
303+ if ( i === nextIdx ) return '即将播出' ;
304+ return '后续' ;
305+ }
306+
307+ const rows = programs . map ( ( p , i ) => {
308+ const isCurrent = i === curIdx ;
309+ const desc = p . description && String ( p . description ) . trim ( ) ;
310+ return `
311+ <div class="epg-item ${ isCurrent ? 'epg-current' : '' } ">
312+ <span class="epg-label">${ epgLabel ( i ) } </span>
313+ <span class="epg-title">${ escapeHtml ( p . title ) } </span>
314+ <span class="epg-time">${ fmt ( p . startTime ) } - ${ fmt ( p . endTime ) } </span>
315+ ${ desc ? `<p class="epg-desc">${ escapeHtml ( desc ) } </p>` : '' }
316+ </div>` ;
317+ } ) . join ( '' ) ;
318+
319+ const cap = data . limit != null ? data . limit : EPG_LIST_LIMIT ;
320+ let footerText ;
321+ if ( programs . length >= cap ) {
322+ footerText = `已显示 ${ cap } 条未结束节目(已达本页上限,数据库中可能还有更多)` ;
323+ } else {
324+ footerText = `共 ${ programs . length } 条未结束节目` ;
325+ }
326+ const footer = `<p class="epg-footer">${ footerText } </p>` ;
327+
328+ body . innerHTML = rows + footer ;
329+ } catch ( err ) {
330+ body . innerHTML = '<p class="epg-empty">节目信息加载失败</p>' ;
331+ }
332+ }
333+
334+ function closeEpgModal ( event ) {
335+ if ( event && event . target !== event . currentTarget ) return ;
336+ document . getElementById ( 'epgModal' ) . classList . remove ( 'show' ) ;
337+ }
338+
257339// Initialize
340+ loadVersion ( ) ;
258341loadFavorites ( ) . then ( ( ) => loadSources ( ) ) ;
259342loadSettings ( ) ;
0 commit comments