document.addEventListener('DOMContentLoaded', () => { // ================================================================= // 1. DOM 元素获取 (结构清晰,无需改动) // ================================================================= const seriesGrid = document.getElementById('series-grid'); const searchInput = document.getElementById('search-input'); const modalBackdrop = document.getElementById('modal-backdrop'); const allModals = document.querySelectorAll('.modal, .video-modal'); const detailsModal = document.getElementById('details-modal'); const detailTitle = document.getElementById('detail-title'); const episodeList = document.getElementById('episode-list'); const videoModal = document.getElementById('video-modal'); const videoPlayer = document.getElementById('video-player'); const playerTitle = document.getElementById('player-title'); const prevBtn = document.getElementById('prev-episode-btn'); const nextBtn = document.getElementById('next-episode-btn'); const playerEpisodesContainer = document.getElementById('player-episodes'); const requestModal = document.getElementById('request-modal'); const requestForm = document.getElementById('request-form'); const requestInput = document.getElementById('request-input'); const requestStatus = document.getElementById('request-status'); const noResultsContainer = document.getElementById('no-results-container'); const showRequestModalBtn = document.getElementById('show-request-modal-btn'); const historyBtn = document.getElementById('history-btn'); const historyModal = document.getElementById('history-modal'); const historyList = document.getElementById('history-list'); // ================================================================= // 2. 应用状态变量 // ================================================================= let allSeriesData = []; // 存储所有剧集的基础信息 { name, posterUrl },剧集列表会按需加载 let activeSeriesData = null; // 当前正在操作的剧集数据 { name, episodes } let currentEpisodeIndex = -1; // 当前播放的集数索引 let watchHistory = {}; // 从 localStorage 加载的播放历史 const HISTORY_KEY = 'WatchHistory'; // ================================================================= // 3. 核心功能函数 // ================================================================= /** * 异步获取指定剧集的剧集列表。 * 内置缓存:如果已获取过,则直接从 allSeriesData 返回,避免重复API请求。 * @param {string} seriesName - 剧集名称 * @returns {Promise>} - 包含剧集文件名的数组 */ const fetchEpisodesForSeries = async (seriesName) => { const seriesInfo = allSeriesData.find(s => s.name === seriesName); if (!seriesInfo) throw new Error(`在本地数据中未找到剧集: "${seriesName}"`); // 如果已经获取过剧集列表,直接返回缓存的数据 if (seriesInfo.episodes) { return seriesInfo.episodes; } try { const response = await fetch(`./api/videos/${encodeURIComponent(seriesName)}`); if (!response.ok) throw new Error(`API 请求失败,状态码: ${response.status}`); const episodes = await response.json(); // 缓存结果到主数据对象中 seriesInfo.episodes = episodes; return episodes; } catch (error) { console.error(`获取剧集 "${seriesName}" 失败:`, error); // 将错误向上抛出,由调用者处理用户提示 throw error; } }; /** * 渲染主页的剧集网格。 * @param {Array} seriesArray - 需要渲染的剧集数组 */ const renderSeriesGrid = (seriesArray) => { seriesGrid.innerHTML = ''; if (seriesArray.length === 0) return; seriesArray.forEach(series => { const card = document.createElement('div'); card.className = 'series-card'; card.dataset.seriesName = series.name; card.innerHTML = `
${series.name} Poster

${series.name}

`; seriesGrid.appendChild(card); }); setupLazyLoader(); }; /** * 播放指定索引的剧集,并处理续播逻辑。 * @param {number} episodeIndex - 剧集索引 */ const playVideo = (episodeIndex) => { if (!activeSeriesData || episodeIndex < 0 || episodeIndex >= activeSeriesData.episodes.length) { console.error("无法播放:剧集数据无效或索引越界"); return; } currentEpisodeIndex = episodeIndex; const { name: seriesName, episodes } = activeSeriesData; const episodeFilename = episodes[episodeIndex]; const readableTitle = episodeFilename.replace(/\.[^/.]+$/, ""); playerTitle.textContent = readableTitle; videoPlayer.src = `./videos/${encodeURIComponent(seriesName)}/${encodeURIComponent(episodeFilename)}`; // play() 返回一个 Promise,处理自动播放被浏览器阻止的情况 videoPlayer.play().then(() => { // 检查是否有播放历史且不是已看完状态,用于续播 const history = watchHistory[seriesName]; if (history && history.lastIndex === episodeIndex && !history.finished) { // 在视频总时长的合理范围内才续播 if (history.lastTime > 0 && history.lastTime < videoPlayer.duration - 5) { videoPlayer.currentTime = history.lastTime; } } }).catch(error => console.error("视频播放失败:", error)); updateNavButtons(); populatePlayerEpisodeList(); openModal(videoModal); }; // ================================================================= // 4. 辅助函数 (UI, History, etc.) // ================================================================= /** * 创建一个剧集列表项的 DOM 元素。 * @param {string} episode - 剧集文件名 * @param {number} index - 剧集索引 * @param {string} seriesName - 所属剧集名称 * @param {string} itemClass - 元素的 CSS class * @returns {HTMLElement} - 创建的 div 元素 */ const createEpisodeListItem = (episode, index, seriesName, itemClass) => { const isWatched = isEpisodeWatched(seriesName, index); const item = document.createElement('div'); item.className = itemClass; item.dataset.index = index; if (isWatched) item.classList.add('watched'); item.innerHTML = ` ${episode.replace(/\.[^/.]+$/, "")} ${isWatched ? '' : ''} `; return item; }; const openModal = (modal) => { modalBackdrop.classList.add('active'); modal.classList.add('active'); document.body.classList.add('modal-open'); }; const closeModal = () => { modalBackdrop.classList.remove('active'); allModals.forEach(modal => modal.classList.remove('active')); document.body.classList.remove('modal-open'); // 关闭弹窗时暂停视频并清空src,防止后台播放和资源占用 if (!videoPlayer.paused) { videoPlayer.pause(); } videoPlayer.src = ""; }; const loadHistory = () => watchHistory = JSON.parse(localStorage.getItem(HISTORY_KEY)) || {}; const saveHistory = () => localStorage.setItem(HISTORY_KEY, JSON.stringify(watchHistory)); const recordPlaybackTime = (time) => { if (!activeSeriesData || currentEpisodeIndex < 0) return; const seriesName = activeSeriesData.name; watchHistory[seriesName] = { lastIndex: currentEpisodeIndex, lastTime: time, finished: false }; saveHistory(); }; const markEpisodeAsFinished = (seriesName, episodeIndex) => { watchHistory[seriesName] = { lastIndex: episodeIndex, lastTime: 0, finished: true }; saveHistory(); }; /** * 检查一集是否被视为“已观看”。 * 条件:1. 历史记录中的索引大于当前集;2. 历史记录索引等于当前集且标记为finished。 * @returns {boolean} */ const isEpisodeWatched = (seriesName, episodeIndex) => { const history = watchHistory[seriesName]; if (!history) return false; return history.lastIndex > episodeIndex || (history.lastIndex === episodeIndex && history.finished); }; const formatTime = (seconds) => { const min = Math.floor(seconds / 60); const sec = Math.floor(seconds % 60); return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; }; const populateHistoryModal = async () => { historyList.innerHTML = ''; const historyEntries = Object.entries(watchHistory); if (historyEntries.length === 0) { historyList.innerHTML = '

暂无播放记录

'; return; } // 按剧集名称排序 historyEntries.sort((a, b) => a[0].localeCompare(b[0], 'zh-CN')); // 使用 for...of 循环以正确处理 await for (const [seriesName, history] of historyEntries) { try { // 此处无需关心剧集是否已获取,fetchEpisodesForSeries 会处理缓存 const episodes = await fetchEpisodesForSeries(seriesName); if (history.lastIndex >= episodes.length) { console.warn(`剧集 "${seriesName}" 历史记录越界,跳过。`); continue; } const episodeFilename = episodes[history.lastIndex]; const readableEpisodeName = episodeFilename.replace(/\.[^/.]+$/, ""); const progressText = history.finished ? `已看完: ${readableEpisodeName}` : `看到: ${readableEpisodeName} (${formatTime(history.lastTime)})`; const item = document.createElement('div'); item.className = 'history-item'; item.dataset.seriesName = seriesName; item.dataset.episodeIndex = history.lastIndex; item.innerHTML = `${seriesName}${progressText}`; historyList.appendChild(item); } catch (error) { // 如果 fetchEpisodesForSeries 失败 (例如剧集已被删除),则跳过此条目 console.error(`处理历史记录中的 "${seriesName}" 时出错:`, error.message); continue; } } if (historyList.childElementCount === 0) { historyList.innerHTML = '

有播放记录,但未能匹配到现有剧集。

'; } }; const updateNavButtons = () => { prevBtn.disabled = currentEpisodeIndex <= 0; nextBtn.disabled = currentEpisodeIndex >= activeSeriesData.episodes.length - 1; }; const populatePlayerEpisodeList = () => { playerEpisodesContainer.innerHTML = ''; const { name: seriesName, episodes } = activeSeriesData; episodes.forEach((episode, index) => { const item = createEpisodeListItem(episode, index, seriesName, 'player-episode-item'); if (index === currentEpisodeIndex) item.classList.add('active'); playerEpisodesContainer.appendChild(item); }); // 自动滚动到当前播放的剧集 const activeItem = playerEpisodesContainer.querySelector('.active'); if (activeItem) { activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }; function setupLazyLoader() { const lazyImages = document.querySelectorAll('img.lazy'); const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // 图片加载失败时,显示剧集名称首字母作为占位符 img.onerror = () => { img.parentElement.innerHTML = `
${img.dataset.seriesName.charAt(0)}
`; }; img.classList.remove('lazy'); observer.unobserve(img); } }); }, { rootMargin: '0px 0px 150px 0px' }); // 预加载视口下方150px的图片 lazyImages.forEach(img => imageObserver.observe(img)); } // 通用节流函数,防止事件过于频繁触发 const throttle = (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } }; // ================================================================= // 5. 事件处理 (使用事件委托提高性能) // ================================================================= searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase().trim(); const filteredSeries = allSeriesData.filter(series => series.name.toLowerCase().includes(searchTerm)); renderSeriesGrid(filteredSeries); noResultsContainer.style.display = (filteredSeries.length === 0 && searchTerm) ? 'block' : 'none'; }); showRequestModalBtn.addEventListener('click', () => { requestInput.value = searchInput.value.trim(); // 自动填充搜索词 openModal(requestModal); requestInput.focus(); }); requestForm.addEventListener('submit', async (e) => { e.preventDefault(); const dramaName = requestInput.value.trim(); if (!dramaName) return; try { const response = await fetch('./api/request', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ dramaName: dramaName }), }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '服务器返回了一个未知错误'); } requestStatus.textContent = result.message; requestStatus.className = 'request-status success'; requestInput.value = ''; setTimeout(() => { closeModal(); setTimeout(() => { requestStatus.textContent = ''; requestStatus.className = 'request-status'; }, 500); }, 2000); } catch (error) { console.error('提交心愿失败:', error); requestStatus.textContent = `出错了: ${error.message}`; requestStatus.className = 'request-status error'; } }); historyBtn.addEventListener('click', () => { populateHistoryModal(); openModal(historyModal); }); // -- 使用事件委托统一处理所有动态生成的列表项的点击事件 // 处理剧集卡片点击 seriesGrid.addEventListener('click', async (e) => { const card = e.target.closest('.series-card'); if (!card) return; const seriesName = card.dataset.seriesName; try { const episodes = await fetchEpisodesForSeries(seriesName); activeSeriesData = { name: seriesName, episodes }; detailTitle.textContent = seriesName; episodeList.innerHTML = ''; episodes.forEach((ep, index) => { const item = createEpisodeListItem(ep, index, seriesName, 'episode-item'); episodeList.appendChild(item); }); openModal(detailsModal); } catch (error) { alert(`加载 "${seriesName}" 失败,请稍后重试。`); } }); // 处理剧集详情列表点击 episodeList.addEventListener('click', (e) => { const item = e.target.closest('.episode-item'); if (item) { closeModal(); playVideo(parseInt(item.dataset.index, 10)); } }); // 处理播放器内剧集列表点击 playerEpisodesContainer.addEventListener('click', (e) => { const item = e.target.closest('.player-episode-item'); if (item) { playVideo(parseInt(item.dataset.index, 10)); } }); // 处理历史记录列表点击 historyList.addEventListener('click', async (e) => { const item = e.target.closest('.history-item'); if (!item) return; const seriesName = item.dataset.seriesName; const episodeIndex = parseInt(item.dataset.episodeIndex, 10); try { const episodes = await fetchEpisodesForSeries(seriesName); activeSeriesData = { name: seriesName, episodes }; closeModal(); playVideo(episodeIndex); } catch (error) { alert('找不到该剧集,可能已被移除。'); } }); // -- 视频播放器事件 videoPlayer.addEventListener('timeupdate', throttle(() => recordPlaybackTime(videoPlayer.currentTime), 5000)); videoPlayer.addEventListener('ended', () => { markEpisodeAsFinished(activeSeriesData.name, currentEpisodeIndex); // 自动播放下一集 if (currentEpisodeIndex < activeSeriesData.episodes.length - 1) { playVideo(currentEpisodeIndex + 1); } else { alert('全剧终!'); closeModal(); } }); // -- 通用关闭事件 modalBackdrop.addEventListener('click', closeModal); document.querySelectorAll('[data-close-modal]').forEach(btn => btn.addEventListener('click', closeModal)); // -- 播放器导航按钮 prevBtn.addEventListener('click', () => playVideo(currentEpisodeIndex - 1)); nextBtn.addEventListener('click', () => playVideo(currentEpisodeIndex + 1)); // ================================================================= // 6. 初始化 // ================================================================= async function initializeApp() { loadHistory(); try { const response = await fetch('./api/series'); if (!response.ok) throw new Error(`网络错误: ${response.statusText}`); allSeriesData = await response.json(); renderSeriesGrid(allSeriesData); historyBtn.disabled = false; // 数据加载成功后才启用历史记录按钮 } catch (error) { console.error('初始化失败:', error); seriesGrid.innerHTML = '

加载剧集列表失败,请检查网络或刷新页面。

'; } } initializeApp(); });