diff --git a/src/app/live/page.tsx b/src/app/live/page.tsx index 4670d89..b103075 100644 --- a/src/app/live/page.tsx +++ b/src/app/live/page.tsx @@ -141,34 +141,43 @@ function LivePageClient() { }, }); - // EPG数据清洗函数 - 去除重叠的节目,保留时间较短的,只显示今日节目 + // EPG数据清洗函数 - 去除重叠的节目,保留时间较短的,显示今日节目(18点后包含明天10点前的节目) const cleanEpgData = (programs: Array<{ start: string; end: string; title: string }>) => { if (!programs || programs.length === 0) return programs; + // 获取当前时间 + const now = new Date(); + const currentHour = now.getHours(); + // 获取今日日期(只考虑年月日,忽略时间) const today = new Date(); const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1); - // 首先过滤出今日的节目(包括跨天节目) - const todayPrograms = programs.filter(program => { + // 如果当前时间超过18点,扩展到明天10点 + let endTime = todayEnd; + if (currentHour >= 18) { + // 明天10点 + endTime = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 10, 0, 0); + } + + // 首先过滤出符合时间范围的节目(包括跨天节目) + const filteredPrograms = programs.filter(program => { const programStart = parseCustomTimeFormat(program.start); const programEnd = parseCustomTimeFormat(program.end); - // 获取节目的日期范围 - const programStartDate = new Date(programStart.getFullYear(), programStart.getMonth(), programStart.getDate()); - const programEndDate = new Date(programEnd.getFullYear(), programEnd.getMonth(), programEnd.getDate()); + // 使用时间戳进行比较 + const programStartTime = programStart.getTime(); + const programEndTime = programEnd.getTime(); + const todayStartTime = todayStart.getTime(); + const endTimeValue = endTime.getTime(); - // 如果节目的开始时间或结束时间在今天,或者节目跨越今天,都算作今天的节目 - return ( - (programStartDate >= todayStart && programStartDate < todayEnd) || // 开始时间在今天 - (programEndDate >= todayStart && programEndDate < todayEnd) || // 结束时间在今天 - (programStartDate < todayStart && programEndDate >= todayEnd) // 节目跨越今天(跨天节目) - ); + // 节目的开始时间在范围内,或者节目在范围内播放(开始时间早于范围开始,但结束时间在范围内) + return programStartTime < endTimeValue && programEndTime > todayStartTime; }); // 按开始时间排序 - const sortedPrograms = [...todayPrograms].sort((a, b) => { + const sortedPrograms = [...filteredPrograms].sort((a, b) => { const startA = parseCustomTimeFormat(a.start).getTime(); const startB = parseCustomTimeFormat(b.start).getTime(); return startA - startB; diff --git a/src/components/EpgScrollableRow.tsx b/src/components/EpgScrollableRow.tsx index 348ed5a..bc96dd6 100644 --- a/src/components/EpgScrollableRow.tsx +++ b/src/components/EpgScrollableRow.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { Clock, Target, Tv } from 'lucide-react'; +import { Clock, Target, Tv, List, BarChart3 } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { formatTimeToHHMM, parseCustomTimeFormat } from '@/lib/time'; @@ -17,14 +17,19 @@ interface EpgScrollableRowProps { isLoading?: boolean; } +type ViewMode = 'list' | 'timeline'; + export default function EpgScrollableRow({ programs, currentTime = new Date(), isLoading = false, }: EpgScrollableRowProps) { const containerRef = useRef(null); + const timelineHorizontalRef = useRef(null); + const timelineVerticalRef = useRef(null); const [isHovered, setIsHovered] = useState(false); const [currentPlayingIndex, setCurrentPlayingIndex] = useState(-1); + const [viewMode, setViewMode] = useState('list'); // 处理滚轮事件,实现横向滚动 const handleWheel = (e: WheelEvent) => { @@ -49,7 +54,7 @@ export default function EpgScrollableRow({ } }; - // 自动滚动到正在播放的节目 + // 自动滚动到正在播放的节目(列表视图) const scrollToCurrentProgram = () => { if (containerRef.current) { const currentProgramIndex = programs.findIndex(program => isCurrentlyPlaying(program)); @@ -73,6 +78,50 @@ export default function EpgScrollableRow({ } }; + // 自动滚动到正在播放的节目(时间线视图) + const scrollToCurrentProgramTimeline = () => { + const currentProgramIndex = programs.findIndex(program => isCurrentlyPlaying(program)); + if (currentProgramIndex === -1) return; + + // 横向时间线 + if (timelineHorizontalRef.current && window.innerWidth >= 768) { + const programElement = timelineHorizontalRef.current.children[currentProgramIndex] as HTMLElement; + if (programElement) { + const container = timelineHorizontalRef.current; + const programLeft = programElement.offsetLeft; + const containerWidth = container.clientWidth; + const programWidth = programElement.offsetWidth; + + const scrollLeft = programLeft - (containerWidth / 2) + (programWidth / 2); + + container.scrollTo({ + left: Math.max(0, scrollLeft), + behavior: 'smooth' + }); + } + } + // 竖向时间线 + else if (timelineVerticalRef.current) { + const programElement = timelineVerticalRef.current.children[currentProgramIndex] as HTMLElement; + if (programElement) { + // 找到包含滚动的父容器 + const scrollContainer = timelineVerticalRef.current.parentElement; + if (scrollContainer) { + const programTop = programElement.offsetTop; + const containerHeight = scrollContainer.clientHeight; + const programHeight = programElement.offsetHeight; + + const scrollTop = programTop - (containerHeight / 2) + (programHeight / 2); + + scrollContainer.scrollTo({ + top: Math.max(0, scrollTop), + behavior: 'smooth' + }); + } + } + } + }; + useEffect(() => { if (isHovered) { // 鼠标悬停时阻止页面滚动 @@ -128,6 +177,20 @@ export default function EpgScrollableRow({ return () => clearInterval(interval); }, [programs, currentTime, currentPlayingIndex]); + // 切换视图时自动跳转到当前播放位置 + useEffect(() => { + // 延迟执行,确保DOM完全渲染 + const timer = setTimeout(() => { + if (viewMode === 'list') { + scrollToCurrentProgram(); + } else if (viewMode === 'timeline') { + scrollToCurrentProgramTimeline(); + } + }, 100); + + return () => clearTimeout(timer); + }, [viewMode]); + // 格式化时间显示 const formatTime = (timeString: string) => { return formatTimeToHHMM(timeString); @@ -144,6 +207,37 @@ export default function EpgScrollableRow({ } }; + // 计算节目时长(分钟) + const getProgramDuration = (program: EpgProgram) => { + try { + const start = parseCustomTimeFormat(program.start); + const end = parseCustomTimeFormat(program.end); + return (end.getTime() - start.getTime()) / (1000 * 60); // 转换为分钟 + } catch { + return 30; // 默认30分钟 + } + }; + + // 计算当前时间在时间线上的位置百分比 + const getCurrentTimePosition = () => { + if (programs.length === 0) return 0; + + try { + const firstProgram = programs[0]; + const lastProgram = programs[programs.length - 1]; + const startTime = parseCustomTimeFormat(firstProgram.start).getTime(); + const endTime = parseCustomTimeFormat(lastProgram.end).getTime(); + const currentTimeMs = currentTime.getTime(); + + if (currentTimeMs < startTime) return 0; + if (currentTimeMs > endTime) return 100; + + return ((currentTimeMs - startTime) / (endTime - startTime)) * 100; + } catch { + return 0; + } + }; + // 加载中状态 if (isLoading) { return ( @@ -193,29 +287,62 @@ export default function EpgScrollableRow({ 今日节目单 - {currentPlayingIndex !== -1 && ( - - )} +
+ {/* 视图切换按钮 */} +
+ + +
+ + {/* 当前播放按钮 */} + {currentPlayingIndex !== -1 && ( + + )} +
-
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > + {/* 列表视图 */} + {viewMode === 'list' && (
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} > - {programs.map((program, index) => { +
+ {programs.map((program, index) => { // 使用 currentPlayingIndex 来判断播放状态,确保样式能正确更新 const isPlaying = index === currentPlayingIndex; const isFinishedProgram = index < currentPlayingIndex; @@ -288,6 +415,221 @@ export default function EpgScrollableRow({ })}
+ )} + + {/* 时间线视图 */} + {viewMode === 'timeline' && ( +
+ {/* 电脑端:横向时间线 */} +
+
+ {/* 时间线容器 - 可横向滚动 */} +
{ + const container = timelineHorizontalRef.current; + if (container) { + const handleWheel = (e: WheelEvent) => { + if (container.scrollWidth > container.clientWidth) { + e.preventDefault(); + container.scrollLeft += e.deltaY * 4; + } + }; + container.addEventListener('wheel', handleWheel, { passive: false }); + (container as any)._wheelHandler = handleWheel; + } + }} + onMouseLeave={(e) => { + const container = timelineHorizontalRef.current; + if (container && (container as any)._wheelHandler) { + container.removeEventListener('wheel', (container as any)._wheelHandler); + delete (container as any)._wheelHandler; + } + }} + > +
+ {programs.map((program, index) => { + const isPlaying = index === currentPlayingIndex; + const isFinished = index < currentPlayingIndex; + const duration = getProgramDuration(program); + + return ( +
+ {/* 节目信息卡片 */} +
+
+ + {formatTime(program.start)} + + + {Math.round(duration)}分钟 + +
+
+ {program.title} +
+ {isPlaying && ( +
+
+ + 正在播放 + +
+ )} +
+ + {/* 时间线轴 */} +
+ {/* 时间点 */} +
+ + {/* 右侧连接线 */} + {index < programs.length - 1 && ( +
+ )} +
+
+ ); + })} +
+
+
+
+ + {/* 手机端:竖向时间线 */} +
+
+ {/* 时间线容器 */} +
+ {programs.map((program, index) => { + const isPlaying = index === currentPlayingIndex; + const isFinished = index < currentPlayingIndex; + const duration = getProgramDuration(program); + + return ( +
+ {/* 时间线轴 */} +
+ {/* 时间点 */} +
+ + {/* 连接线 - 根据状态显示不同颜色 */} + {index < programs.length - 1 && ( +
+ )} +
+ + {/* 节目信息 */} +
+
+ + {formatTime(program.start)} + + + {Math.round(duration)}分钟 + +
+
+ {program.title} +
+ {isPlaying && ( +
+
+ + 正在播放 + +
+ )} +
+
+ ); + })} +
+
+
+
+ )}
); }