<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 不携带 referer --> <meta name="referrer" content="never"> <title>B站排行</title> <link rel="stylesheet" href="./assets/css/main.css"> <style> #title { margin-bottom: 0; } #dynamic-note { color: grey; margin: 5px auto; font-size: 13px; } td { font-size: 12px; max-width: 200px; word-wrap: break-word; } </style> </head> <body> <div style="text-align: center;"> <h1 id="title">B站排行榜</h1> <p id="dynamic-note"></p> <hr> <div style="display: none;"> 显示字段: <label for="show_emoticon"> <input type="checkbox" class="filter_checkbox" name="show_emoticon" id="show_emoticon" detailed-checked="true">热搜表情 </label> <label for="show_num"> <input type="checkbox" class="filter_checkbox" name="show_num" id="show_num" checked="true" concise-checked="true" detailed-checked="true">热度 </label> <label for="show_category"> <input type="checkbox" class="filter_checkbox" name="show_category" id="show_category" checked="true" detailed-checked="true">分类 </label> <label for="show_onboard_time"> <input type="checkbox" class="filter_checkbox" name="show_onboard_time" id="show_onboard_time" checked="true" detailed-checked="true">上线时间 </label> <label for="show_is_new"> <input type="checkbox" class="filter_checkbox" name="show_is_new" id="show_is_new" detailed-checked="true">是否新热搜 </label> <label for="show_detail"> <input type="checkbox" class="filter_checkbox" name="show_detail" id="show_detail" detailed-checked="true">热搜详情 </label> <label for="show_mid"> <input type="checkbox" class="filter_checkbox" name="show_mid" id="show_mid">mid </label> <br> <button id="btn_show_all">全选</button> <button id="btn_show_none">全不选</button> | <button id="btn_show_concise">简洁</button> <button id="btn_show_default">普通</button> <button id="btn_show_detailed">详细</button> </div> </div> <p id="latestUpdateTime" style="font-size: 12px; display: inline-block; vertical-align: middle;"></p> <nobr> <button id="btn_refresh">重新拉取</button> </nobr> <nobr> <label for="auto_refresh"> <input type="checkbox" name="auto_refresh" id="auto_refresh">自动拉取<span id="auto_refresh_countdown"></span> </label> </nobr> <nobr> <span id="update-finish-info" style="color: green; font-weight: bold; display: none;">拉取成功,数据已更新</span> </nobr> <table id="list"></table> <div class="bottom-placeholder"> <p> — 到底啦 —<br> 数据来源: https://www.bilibili.com/v/popular/rank/all </p> </div> <script> /** * 全局变量 */ // 拉取下来的数据 let hotBandData; // 按钮 const btnShowAll = document.getElementById('btn_show_all'); const btnShowNone = document.getElementById('btn_show_none'); const btnShowConcise = document.getElementById('btn_show_concise'); const btnShowDefault = document.getElementById('btn_show_default'); const btnShowDetailed = document.getElementById('btn_show_detailed'); const btnRefresh = document.getElementById('btn_refresh'); // 复选框 const filterCheckbox = document.getElementsByClassName("filter_checkbox"); const showEmoticon = document.getElementById("show_emoticon"); const showNum = document.getElementById("show_num"); const showCategory = document.getElementById("show_category"); const showOnboardTime = document.getElementById("show_onboard_time"); const showIsNew = document.getElementById("show_is_new"); const showDetail = document.getElementById("show_detail"); const showMid = document.getElementById("show_mid"); const autoRefresh = document.getElementById("auto_refresh"); // 绑定按钮点击事件 btnShowAll.addEventListener('click', function () { for (let i = 0; i < filterCheckbox.length; i++) { const element = filterCheckbox[i]; element.checked = true; } render(); }); btnShowNone.addEventListener('click', function () { for (let i = 0; i < filterCheckbox.length; i++) { const element = filterCheckbox[i]; element.checked = false; } render(); }); btnShowConcise.addEventListener('click', function () { for (let i = 0; i < filterCheckbox.length; i++) { const element = filterCheckbox[i]; element.checked = element.getAttribute('concise-checked') === 'true'; } render(); }); btnShowDefault.addEventListener('click', function () { for (let i = 0; i < filterCheckbox.length; i++) { const element = filterCheckbox[i]; element.checked = element.getAttribute('checked') === 'true'; } render(); }); btnShowDetailed.addEventListener('click', function () { for (let i = 0; i < filterCheckbox.length; i++) { const element = filterCheckbox[i]; element.checked = element.getAttribute('detailed-checked') === 'true'; } render(); }); btnRefresh.onclick = function () { getData(); document.getElementById("update-finish-info").style.display = ""; // btnRefresh.style.display = "none"; btnRefresh.style.visibility = "hidden"; setTimeout(function () { document.getElementById("update-finish-info").style.display = "none"; // btnRefresh.style.display = ""; btnRefresh.style.visibility = ""; }, 1000); }; // 绑定复选框改变事件 for (let i = 0; i < filterCheckbox.length; i++) { // console.log(filterCheckbox[i]); filterCheckbox[i].onchange = function () { render(); }; } let autoRefreshIntreval = null; const autoRefreshCountDownElement = document.getElementById('auto_refresh_countdown'); const countDown = 20; // 自动拉取间隔时间,单位:秒 let autoRefreshCountDown = countDown; autoRefresh.onchange = function () { if (autoRefresh.checked) { btnRefresh.style.display = "none"; btnRefresh.click(); autoRefreshIntreval = setInterval(function () { if ((--autoRefreshCountDown) > 0) { autoRefreshCountDownElement.innerHTML = `(${autoRefreshCountDown}s)`; } else { autoRefreshCountDown = countDown; btnRefresh.click(); autoRefreshCountDownElement.innerHTML = ``; } }, 1000); } else { clearInterval(autoRefreshIntreval); btnRefresh.style.display = ""; autoRefreshCountDownElement.innerHTML = ``; } }; // 根据屏幕判断要显示哪些字段 // 此时还未拉取数据,所以进入 render 函数会直接返回,不会多次渲染 let initWidth = document.body.offsetWidth; // console.log(initWidth); /* if (initWidth < 400) { btnShowNone.click(); btnShowNone.innerHTML += "(默认)"; } else */ if (initWidth < 600) { btnShowConcise.click(); btnShowConcise.innerHTML += "(默认)"; } else if (initWidth < 1900) { btnShowDefault.click(); btnShowDefault.innerHTML += "(默认)"; } else { btnShowDetailed.click(); btnShowDetailed.innerHTML += "(默认)"; } // 网页加载后加载榜单 getData(); // 定时刷新 // setInterval(getData, 10 * 1000); function getData() { var xhr = new XMLHttpRequest(); xhr.open("GET", "../data/bilibili-rank/latest.json?t=" + Date.now(), true); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return; if (xhr.status == 200) { try { hotBandData = JSON.parse(xhr.responseText); if (!hotBandData.data || typeof hotBandData.data !== 'object') throw new Error("data is undefined or not an object"); } catch (e) { console.error("[error]", "\n", e, "\n", "\n", "[xhr.responseText]", "\n", xhr.responseText); alert("latest.json 文件解析失败,请检查文件"); return; } console.log(hotBandData); // 更新动态 note document.getElementById("dynamic-note").innerHTML = hotBandData.note; // 更新时间 document.getElementById("latestUpdateTime").innerHTML = "数据拉取时间:" + new Date().toLocaleString() + "<br/>" + "热榜更新时间:" + new Date(hotBandData.update_time).toLocaleString(); // 渲染榜单 render(); } else if (xhr.status == 404) { alert("data 目录下未找到 latest.json 文件,可能的原因:\n您还没有运行脚本拉取数据,请先运行脚本,然后刷新页面"); } } } function render() { if (!hotBandData) return; /** * 渲染热搜列表 */ let hotBandList = hotBandData.data; var str = []; // 渲染表格 str.push(`<thead> <tr class="thead" style="top: 0; background-color: white; position: sticky;"> <td>编号</td> <td>标题</td> <td>时长</td> <td>封面</td> <td>第一帧</td> <td>视频ID</td> <td>分类</td> <td>发布时间</td> <td>简介</td> <td>作者(mid)</td> <td>统计</td> <td>视频宽高</td> <td>定位</td> <td>videos</td> <td>tid</td> <td>copyright</td> <td>state</td> <td>rights</td> <td>dynamic</td> <td>score</td> <td>mission_id</td> <td>season_id</td> <td>up_from_v2</td> <td>others</td> </tr> </thead>`); str.push(`<tbody>`); for (var i = 0; i < hotBandList.length; i++) { const hotBand = hotBandList[i]; let link = hotBand.short_link == hotBand.short_link_v2 ? hotBand.short_link : `${hotBand.short_link}<br/>${hotBand.short_link_v2}`; /** * 视频总秒数转化为友好显示时间 */ // refer: https://blog.csdn.net/weixin_43838488/article/details/122337474 function formatZero(num, len) { if (String(num).length > len) { return num; } return (Array(len).join(0) + num).slice(-len) } let duration = hotBand.duration - 1; // 根据观测,基本上所有视频都是少1s(有些少了2s或者其他),所以这里减1 let durationArr = [0, 0, 0]; durationArr[0] = Math.floor(duration / (60 * 60)); durationArr[1] = Math.floor((duration - durationArr[0] * 60 * 60) / 60); durationArr[2] = Math.floor(duration - durationArr[0] * 60 * 60 - durationArr[1] * 60); let durationStr = ""; if (durationArr[0] === 0) { // 小时为0 durationStr = `${formatZero(durationArr[1], 2)}:${formatZero(durationArr[2], 2)}`; } else { // 小时不为0 durationStr = `${formatZero(durationArr[0], 2)}:${formatZero(durationArr[1], 2)}:${formatZero(durationArr[2], 2)}`; } //功能:求最大公约数 //参数: x 、y number //返回值: number function gcd(x, y) { if (isNaN(x) || isNaN(y)) return null; if (x % y === 0) { return y; } return gcd(y, x % y) //三目运算符写法: //return x % y === 0 ? y : gcd(y , x % y) ; } let dimension_gcd = gcd(hotBand.dimension.width, hotBand.dimension.height); str.push(`<tr> <!-- 编号 --> <td>${i + 1}</td> <!-- 标题 --> <td> <a href="${link}" target="_blank">${hotBand.title}</a> </td> <!-- 时长 --> <td>${durationStr}<br>(${hotBand.duration}s)</td> <!-- 封面 --> <td> <img src="${hotBand.pic}" style="width: 120px;"/> </td> <!-- 第一帧 --> <td> <img src="${hotBand.first_frame}" style="width: 120px;"/> </td> <!-- aid --> <td style="text-align: left;"> <nobr>aid: ${hotBand.aid}</nobr> <nobr>bvid: ${hotBand.bvid}</nobr> <nobr>cid: ${hotBand.cid}</nobr> </td> <!-- 分类 --> <td>${hotBand.tname}</td> <!-- 发布时间 --> <td style="font-size: 10px;"> <nobr>pubdate: ${new Date(hotBand.pubdate * 1000).toLocaleString()}</nobr> <nobr>ctime: ${new Date(hotBand.ctime * 1000).toLocaleString()}</nobr> </td> <!-- 简介 --> <td>${hotBand.desc}</td> <!-- 作者 --> <td> <img src="${hotBand.owner.face}" style="width: 30px;"/><br> ${hotBand.owner.name}<br> (${hotBand.owner.mid}) <!-- ${JSON.stringify(hotBand.owner)} --> </td> <!-- 统计 --> <td style="text-align: left;"> <nobr>播放: ${hotBand.stat.view}</nobr> <nobr>弹幕: ${hotBand.stat.danmaku}</nobr> <nobr>评论: ${hotBand.stat.reply}</nobr> <nobr>喜欢: ${hotBand.stat.favorite}</nobr> <nobr>投币: ${hotBand.stat.coin}</nobr> <nobr>分享: ${hotBand.stat.share}</nobr> <!--<nobr>当前排名: ${hotBand.stat.now_rank}</nobr>--> <nobr>历史<!--最高-->排名: ${hotBand.stat.his_rank}</nobr> <nobr>喜欢数: ${hotBand.stat.like}</nobr> <nobr>不喜欢数: ${hotBand.stat.dislike}</nobr> <!-- ${JSON.stringify(hotBand.stat)} --> </td> <!-- 视频宽高 --> <td> ${hotBand.dimension.width}:${hotBand.dimension.height}<br> (${hotBand.dimension.width / dimension_gcd}:${hotBand.dimension.height / dimension_gcd}) <!-- ${hotBand.dimension.rotate} --> <!-- ${JSON.stringify(hotBand.dimension)} --> </td> <!-- 定位 --> <td> <nobr>${hotBand.pub_location}</nobr> </td> <!-- videos --> <td>${hotBand.videos}</td> <!-- tid --> <td>${hotBand.tid}</td> <!-- copyright --> <td>${hotBand.copyright}</td> <!-- state --> <td>${hotBand.state}</td> <!-- rights --> <td>${JSON.stringify(hotBand.rights)}</td> <!-- dynamic --> <td>${hotBand.dynamic}</td> <!-- score --> <td>${hotBand.score}</td> <!-- mission_id --> <td>${hotBand.mission_id ? hotBand.mission_id : ''}</td> <!-- season_id --> <td>${hotBand.season_id ? hotBand.season_id : ''}</td> <!-- up_from_v2 --> <td>${hotBand.up_from_v2 ? hotBand.up_from_v2 : ''}</td> <!-- others --> <td> ${hotBand.others ? `<div style="max-height: 200px; overflow: scroll;"><span>${JSON.stringify(hotBand.others)}</span></div>` : ''} </td> </tr >`); } str.push(`</tbody>`); document.getElementById('list').innerHTML = str.join(''); } </script> </body> </html>