// 外部脚本文件:轻量的菜单高亮逻辑与占位 document.addEventListener('DOMContentLoaded', () => { // 菜单点击激活态 const links = Array.from(document.querySelectorAll('.menu__link')); links.forEach(link => { link.addEventListener('click', () => { links.forEach(l => l.classList.remove('is-active')); link.classList.add('is-active'); }); }); // ===== 视频逻辑 ===== const player = document.getElementById('player'); const videoWrap = document.getElementById('videoWrap'); const videoPanel = document.getElementById('video'); const includeLocalToggle = document.getElementById('includeLocalToggle'); const statFlowEl = document.getElementById('statFlow'); const statVisitEl = document.getElementById('statVisit'); const statVideoEl = document.getElementById('statVideo'); const statPicEl = document.getElementById('statPic'); const formatBytes = (n) => { let val = Number(n) || 0; const units = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']; let i = 0; while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } const digits = val >= 100 ? 0 : (val >= 10 ? 1 : 2); return val.toFixed(digits) + ' ' + units[i]; }; const formatCount = (n) => { if (n >= 100000000) return (n / 100000000).toFixed(n < 1000000000 ? 2 : 1) + ' 亿'; if (n >= 10000) return (n / 10000).toFixed(n < 100000 ? 2 : 1) + ' 万'; return new Intl.NumberFormat('zh-CN').format(n); }; const updateStatsUI = (t) => { if (!t) return; const bw = (t.bandwidth_in ?? 0) + (t.bandwidth_out ?? 0); if (statFlowEl) statFlowEl.textContent = formatBytes(bw); if (statVisitEl) statVisitEl.textContent = formatCount(t.visits ?? 0); if (statVideoEl) statVideoEl.textContent = formatCount(t.video_plays ?? 0); if (statPicEl) statPicEl.textContent = formatCount(t.picture_shows ?? 0); }; const track = async (payload) => { try { const r = await fetch('api/stats.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const j = await r.json(); if (j && j.success && j.today) updateStatsUI(j.today); } catch {} }; (async () => { try { const r = await fetch('api/stats.php'); const j = await r.json(); if (j && j.success && j.today) updateStatsUI(j.today); } catch {} })(); track({ event: 'visit', page: location.pathname }); // 底部工具栏与标题已移除,无需获取相关元素 if (!player || !videoWrap) return; // 仅在视频区存在时执行 let playlist = []; let index = 0; let includeLocal = includeLocalToggle ? includeLocalToggle.checked : false; const BUFFER_SIZE = 3; // 预缓存条数 const loadVideo = (i, autoPlay = false) => { if (!playlist.length) return; index = (i + playlist.length) % playlist.length; const item = playlist[index]; player.src = item.url; // 已移除标题展示,无需设置文本 player.load(); if (videoPanel) videoPanel.style.setProperty('--progress', '0'); if (autoPlay) { player.play().catch(() => {}); } }; // 按需拉取一条或多条新视频 const fetchOne = async (n = 1) => { try { const url = `api/videos.php?count=${n}&include_local=${includeLocal ? 1 : 0}&local_count=${n}`; const res = await fetch(url); const data = await res.json(); const items = Array.isArray(data.list) ? data.list : []; if (items.length) { const existing = new Set(playlist.map(i => i.url)); const append = items.filter(it => it && it.url && !existing.has(it.url)); playlist = playlist.concat(append); } } catch (e) { // 忽略错误 } }; const ensureBuffer = async () => { const ahead = playlist.length - index - 1; if (ahead < BUFFER_SIZE) { await fetchOne(BUFFER_SIZE - ahead); } }; const next = async () => { await ensureBuffer(); if (index + 1 < playlist.length) { loadVideo(index + 1, true); } else if (!playlist.length) { // 仍无数据,尝试拉取 await fetchOne(BUFFER_SIZE + 1); if (playlist.length) loadVideo(0, true); } }; const prev = async () => { if (index - 1 >= 0) { loadVideo(index - 1, true); } }; // 鼠标移入显示并播放,移出隐藏并暂停 const showVideo = () => { player.classList.add('visible'); player.play().catch(() => {}); // 用户首次交互后取消静音(可选) if (player.muted) player.muted = false; }; const hideVideo = () => { player.pause(); player.classList.remove('visible'); // 可选:player.currentTime = 0; // 回到开头 }; videoWrap.addEventListener('mouseenter', showVideo); videoWrap.addEventListener('mouseleave', hideVideo); // 页面加载时若鼠标已在视频区域,立即显示 if (videoWrap.matches(':hover')) showVideo(); // 单击下一条,双击上一条(去抖处理) // 按最新需求:不再点击视频进入投稿,而是点击菜单栏“视频”进入投稿 let clickTimer = null; videoWrap.addEventListener('click', () => { if (clickTimer) return; clickTimer = setTimeout(() => { clickTimer = null; next(); }, 250); }); videoWrap.addEventListener('dblclick', () => { if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } prev(); }); // 自动保存:播放到一半时保存(每个视频仅保存一次) const savedURLs = new Set(); const trySaveOnHalf = async () => { const item = playlist[index]; if (!item) return; // 本地视频不入库(URL非http/https时跳过保存) if (!/^https?:\/\//i.test(item.url || '')) return; const d = player.duration; const t = player.currentTime; if (!isFinite(d) || d <= 0) return; // 元数据未就绪 const reachedHalf = t / d >= 0.5; if (!reachedHalf || savedURLs.has(item.url)) return; try { const res = await fetch('api/save_video.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: item.url, title: item.title || '' }), }); const data = await res.json(); if (data && data.success) { savedURLs.add(item.url); } } catch (e) { // 忽略保存错误,不影响播放 } }; player.addEventListener('timeupdate', trySaveOnHalf); player.addEventListener('timeupdate', () => { const d = player.duration; const t = player.currentTime; if (!isFinite(d) || d <= 0) return; const p = Math.max(0, Math.min(1, t / d)); if (videoPanel) videoPanel.style.setProperty('--progress', String(p * 360)); }); // 播放结束自动下一条 player.addEventListener('ended', () => { next(); }); let lastLoggedVideo = ''; player.addEventListener('play', () => { const cur = player.currentSrc || player.src || ''; if (!cur || cur === lastLoggedVideo) return; lastLoggedVideo = cur; track({ event: 'video_play', page: location.pathname, url: cur, title: '' }); }); // 从后端接口获取视频列表 // 初始化:先拉取首条并填充缓存 (async () => { await fetchOne(BUFFER_SIZE + 1); if (!playlist.length) { // 无视频可用 return; } loadVideo(0, false); // 启动缓冲确保后续播放连续 await ensureBuffer(); })(); // 切换本地视频开关:重置播放列表并重新拉取 if (includeLocalToggle) { includeLocalToggle.addEventListener('change', async () => { includeLocal = includeLocalToggle.checked; // 重置播放列表 playlist = []; index = 0; await fetchOne(BUFFER_SIZE + 1); if (playlist.length) { loadVideo(0, false); await ensureBuffer(); } }); } // ===== 讨论区气泡逻辑 ===== const discussionWrap = document.getElementById('discussionWrap'); if (discussionWrap) { const fetchAvatar = async () => { try { const r = await fetch('api/head.php'); const j = await r.json(); return j.url || ''; } catch { return ''; } }; const fetchText = async () => { try { const r = await fetch('api/text.php'); // 随机来源 const j = await r.json(); return { text: j.text || '', source: j.source || '' }; } catch { return { text: '', source: '' }; } }; const createBubble = (avatarUrl, text, side = Math.random() < 0.5 ? 'left' : 'right') => { const item = document.createElement('div'); item.className = 'bubble' + (side === 'right' ? ' right' : ''); const img = document.createElement('img'); img.className = 'avatar'; img.src = avatarUrl || 'https://images.unsplash.com/photo-1547425260-76bcadfb4f9b?w=200&h=200&fit=crop'; img.alt = '头像'; const textEl = document.createElement('div'); textEl.className = 'bubble__text'; textEl.textContent = text || '...'; item.appendChild(img); item.appendChild(textEl); discussionWrap.appendChild(item); // 滚动到底部 discussionWrap.scrollTop = discussionWrap.scrollHeight; // 控制条数最多50条 const nodes = discussionWrap.querySelectorAll('.bubble'); if (nodes.length > 50) { for (let i = 0; i < nodes.length - 50; i++) { discussionWrap.removeChild(nodes[i]); } } }; const savedAvatars = new Set(); const savedTexts = new Set(); const spawn = async () => { const [avatar, textResp] = await Promise.all([fetchAvatar(), fetchText()]); const text = textResp.text; const source = textResp.source; createBubble(avatar, text); // 保存到本地,避免重复 try { // 仅保存远程头像(http/https),本地相对路径跳过 if (avatar && /^https?:\/\//i.test(avatar) && !savedAvatars.has(avatar)) { savedAvatars.add(avatar); await fetch('api/save_avatar.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: avatar }) }); } } catch {} try { const key = text.trim(); if (key && !savedTexts.has(key)) { savedTexts.add(key); await fetch('api/save_text.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: key, source }) }); } } catch {} }; // 立即生成一个,然后每5秒生成一次(可在此处调整间隔) spawn(); setInterval(spawn, 5000); } // ===== 图片区逻辑(淡进淡出,10s刷新,自动保存不清理) ===== const pictureWrap = document.getElementById('pictureWrap'); const pictureImg = document.getElementById('pictureImg'); const galleryPanel = document.getElementById('gallery'); if (pictureWrap && pictureImg) { const sources = ['heisi', 'wapmeinvpic', 'baisi', 'meinvpic']; const savedPics = new Set(); const fetchPicture = async () => { const type = sources[Math.floor(Math.random() * sources.length)]; try { const r = await fetch(`api/picture.php?type=${encodeURIComponent(type)}`); const j = await r.json(); return { url: j.url || '', source: j.source || type }; } catch { return { url: '', source: type }; } }; const showPicture = async () => { // 先淡出 pictureImg.classList.remove('visible'); const { url, source } = await fetchPicture(); if (!url) return; // 拉取失败时跳过本次 // 等图片加载完成后再淡入 await new Promise(resolve => { pictureImg.onload = () => { resolve(); }; pictureImg.onerror = () => { resolve(); }; pictureImg.src = url; }); // 加载完成但不再自动显示,仅悬停时显示 // 自动保存(避免重复),后端不做清理 try { // 仅保存远程图片(http/https),本地相对路径跳过 if (/^https?:\/\//i.test(url) && !savedPics.has(url)) { savedPics.add(url); await fetch('api/save_picture.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); } } catch {} }; // 悬停显隐 const showPic = () => pictureImg.classList.add('visible'); const hidePic = () => pictureImg.classList.remove('visible'); pictureWrap.addEventListener('mouseenter', showPic); pictureWrap.addEventListener('mouseleave', hidePic); // 页面加载时若鼠标已在图片区域,立即显示 if (pictureWrap.matches(':hover')) showPic(); let picIntervalMs = 10000; let picLastSwap = Date.now(); const updatePicProgress = () => { const elapsed = Date.now() - picLastSwap; const p = Math.max(0, Math.min(1, elapsed / picIntervalMs)); if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', String(p * 360)); requestAnimationFrame(updatePicProgress); }; requestAnimationFrame(updatePicProgress); (async () => { const { url } = await fetchPicture(); if (url) { pictureImg.src = url; picLastSwap = Date.now(); if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); track({ event: 'picture_show', page: location.pathname, url }); if (pictureWrap.matches(':hover')) showPic(); } })(); setInterval(async () => { const { url } = await fetchPicture(); if (url) { pictureImg.src = url; picLastSwap = Date.now(); if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); track({ event: 'picture_show', page: location.pathname, url }); if (pictureWrap.matches(':hover')) showPic(); } }, picIntervalMs); } });