'use client'; import { AlertTriangle, Loader2, Play, Radio, Search } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { Favorite, getAllFavorites, getAllPlayRecords, PlayRecord } from '@/lib/db.client'; import TVLayout from '@/components/tv/TVLayout'; type LiveSource = { key: string; name: string }; type LiveChannel = { id: string; name: string; group?: string; logo?: string }; type LastLiveChannel = { source: string; sourceName?: string; id: string; title: string; group?: string; logo?: string; updatedAt?: number }; const TV_LIVE_LAST_CHANNEL_KEY = 'tv_live_last_channel'; function getLogoUrl(logo?: string, source?: string) { if (!logo) return ''; if (logo.startsWith('/api/proxy/logo')) return logo; const sourceParam = source ? `&source=${encodeURIComponent(source)}` : ''; return `/api/proxy/logo?url=${encodeURIComponent(logo)}${sourceParam}`; } export default function TVLivePage() { const router = useRouter(); const [sources, setSources] = useState([]); const [source, setSource] = useState(''); const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [selectedGroup, setSelectedGroup] = useState('全部'); const [query, setQuery] = useState(''); const [visibleCount, setVisibleCount] = useState(120); const [quickChannels, setQuickChannels] = useState>([]); const [lastChannel, setLastChannel] = useState(null); useEffect(() => { try { const saved = localStorage.getItem(TV_LIVE_LAST_CHANNEL_KEY); if (saved) { const parsed = JSON.parse(saved) as Partial; if (parsed.source && parsed.id && parsed.title) { setLastChannel({ source: parsed.source, sourceName: parsed.sourceName || '', id: parsed.id, title: parsed.title, group: parsed.group || '', logo: parsed.logo || '', updatedAt: parsed.updatedAt, }); } } } catch { setLastChannel(null); } fetch('/api/live/sources') .then((r) => { if (!r.ok) throw new Error('获取直播源失败'); return r.json(); }) .then((data) => { const list = data.data || []; setSources(list); if (list[0]?.key) setSource(list[0].key); }) .catch((err) => setError(err instanceof Error ? err.message : '获取直播源失败')) .finally(() => setLoading(false)); }, []); useEffect(() => { Promise.all([ getAllPlayRecords().catch(() => ({} as Record)), getAllFavorites().catch(() => ({} as Record)), ]).then(([records, favorites]) => { const recents = Object.entries(records) .filter(([, record]) => record.origin === 'live') .sort((a, b) => (b[1].save_time || 0) - (a[1].save_time || 0)) .slice(0, 8) .map(([key, record]) => { const plus = key.indexOf('+'); return { source: key.slice(0, plus).replace(/^live_/, ''), id: key.slice(plus + 1).replace(/^live_/, ''), title: record.title, cover: record.cover, type: '最近' as const, }; }); const favs = Object.entries(favorites) .filter(([, favorite]) => favorite.origin === 'live') .sort((a, b) => (b[1].save_time || 0) - (a[1].save_time || 0)) .slice(0, 8) .map(([key, favorite]) => { const plus = key.indexOf('+'); return { source: key.slice(0, plus).replace(/^live_/, ''), id: key.slice(plus + 1).replace(/^live_/, ''), title: favorite.title, cover: favorite.cover, type: '收藏' as const, }; }); setQuickChannels([...favs, ...recents].slice(0, 12)); }); }, []); useEffect(() => { if (!source) return; setLoading(true); setError(''); setSelectedGroup('全部'); setVisibleCount(120); fetch(`/api/live/channels?source=${encodeURIComponent(source)}`) .then((r) => { if (r.status === 401 || r.status === 403) throw new Error('无权限访问电视直播,请先登录或检查权限'); if (!r.ok) throw new Error('获取频道列表失败'); return r.json(); }) .then((data) => setChannels(data.data || [])) .catch((err) => { setChannels([]); setError(err instanceof Error ? err.message : '获取频道列表失败'); }) .finally(() => setLoading(false)); }, [source]); const groups = useMemo(() => ['全部', ...Array.from(new Set(channels.map((c) => c.group || '其他')))], [channels]); const filteredChannels = useMemo(() => { const keyword = query.trim().toLowerCase(); return channels.filter((channel) => { const groupMatched = selectedGroup === '全部' || (channel.group || '其他') === selectedGroup; const queryMatched = !keyword || channel.name.toLowerCase().includes(keyword) || (channel.group || '').toLowerCase().includes(keyword); return groupMatched && queryMatched; }); }, [channels, query, selectedGroup]); const visibleChannels = useMemo(() => filteredChannels.slice(0, visibleCount), [filteredChannels, visibleCount]); return ( {lastChannel && (
)}

直播

选择频道后进入全屏直播播放页,频道列表作为播放层弹出。

{sources.map((item) => ( ))}
{quickChannels.length > 0 && (

常用频道

{quickChannels.map((item) => ( ))}
)} {error ? (
{error}
) : loading ?
正在加载频道...
: (
{selectedGroup} · {filteredChannels.length} 个频道
{visibleChannels.map((channel, index) => ( ))}
{visibleCount < filteredChannels.length && ( )}
)}
); }