// ==UserScript== // @name B站壳层 · Naive UI 重设计 // @namespace bili-shell-naive // @version 0.2.0 // @description 隐藏官方首页 UI,挂载基于 Naive UI 的重设计壳层;尝试拉取真实推荐/热榜,并过滤广告条目。可与「B站去广告」共存。 // @author b站脚本 // @match *://www.bilibili.com/* // @match *://bilibili.com/* // @run-at document-start // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @require https://unpkg.com/vue@3.5.13/dist/vue.global.prod.js // @require https://unpkg.com/naive-ui@2.40.1/dist/index.prod.js // @inject-into page // @compatible edge 脚本猫/篡改猴 // @compatible chrome 脚本猫/篡改猴 // @license MIT // ==/UserScript== (function () { 'use strict'; const ENABLED_KEY = 'bili_shell_enabled_v1'; const enabled = () => { try { const v = GM_getValue(ENABLED_KEY, true); return v !== false && v !== 'false'; } catch (_) { return true; } }; // --------------------------------------------------------------------------- // 立刻隐藏官方 UI,避免闪屏 // --------------------------------------------------------------------------- function injectHideCss() { GM_addStyle(` html.bili-shell-active > body { overflow: hidden !important; } html.bili-shell-active #app, html.bili-shell-active #app * { visibility: hidden !important; } html.bili-shell-active #app { display: none !important; pointer-events: none !important; } #bili-shell-root { position: fixed; inset: 0; z-index: 2147483000; overflow: auto; background: #0f1012; color: #f1f2f3; } #bili-shell-root * { box-sizing: border-box; } #bili-shell-root a { color: inherit; text-decoration: none; } #bili-shell-root button { font-family: inherit; } `); } function activateShellClass(on) { document.documentElement.classList.toggle('bili-shell-active', !!on); } // --------------------------------------------------------------------------- // API(同域 GM 请求 + 广告过滤) // --------------------------------------------------------------------------- function gmFetch(url) { return new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: 'GET', url, headers: { Referer: 'https://www.bilibili.com/', Origin: 'https://www.bilibili.com', }, timeout: 12000, onload(res) { if (res.status >= 200 && res.status < 300) { try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); } } else { reject(new Error('HTTP ' + res.status)); } }, onerror: () => reject(new Error('network error')), ontimeout: () => reject(new Error('timeout')), }); } catch (e) { reject(e); } }); } function isAdItem(item) { if (!item || typeof item !== 'object') return false; if (item.is_ad === 1 || item.is_ad === true) return true; if (item.isAd === 1 || item.isAd === true) return true; if (item.is_ad_loc === 1 || item.is_ad_loc === true) return true; if (typeof item.ad_cb === 'string' && item.ad_cb.length > 0) return true; if (item.creative_id && item.source_id && String(item.source_id) === '5614') return true; if (item.type === 'bili_ad' || item.item === 'bili_ad') return true; if (item.card && (item.card.is_ad || item.card.isAd)) return true; return false; } function filterAdList(list) { if (!Array.isArray(list)) return []; return list.filter((x) => !isAdItem(x)); } function pickListFromPayload(json) { if (!json || typeof json !== 'object') return []; const data = json.data; if (!data) return []; if (Array.isArray(data.item)) return data.item; if (Array.isArray(data.list)) return data.list; if (data.archives && Array.isArray(data.archives)) return data.archives; return []; } function mapRcmdItem(raw, index) { const title = raw.title || raw.desc || '未命名视频'; const owner = raw.owner || {}; const up = owner.name || raw.author || raw.name || 'UP主'; const mid = owner.mid || raw.mid || 0; const cover = raw.pic || raw.cover || (raw.cover_url_text ? String(raw.cover_url_text).replace(/^\/\//, 'https://') : '') || ''; const bvid = raw.bvid || ''; const aid = raw.aid || raw.id || ''; const goto = raw.goto || ''; // 广告/带货在映射层再挡一道 if (isAdItem(raw)) return null; let href = 'https://www.bilibili.com/'; if (bvid) href = 'https://www.bilibili.com/video/' + bvid; else if (goto === 'av' && aid) href = 'https://www.bilibili.com/video/av' + aid; else if (raw.uri) href = String(raw.uri).startsWith('http') ? raw.uri : 'https:' + raw.uri; else if (goto === 'live' && raw.room_id) href = 'https://live.bilibili.com/' + raw.room_id; else if (goto === 'bangumi' && (raw.season_id || raw.epid)) { href = raw.epid ? 'https://www.bilibili.com/bangumi/play/ep' + raw.epid : 'https://www.bilibili.com/bangumi/ss' + raw.season_id; } const stat = raw.stat || {}; const view = stat.view || raw.play || 0; const like = stat.like || raw.like || 0; const danmaku = stat.danmaku || raw.video_review || 0; const pubdate = raw.pubdate || raw.ctime || 0; return { id: bvid || aid || 'item-' + index + '-' + Math.random().toString(36).slice(2, 7), title: String(title), up: String(up), mid, cover: String(cover || '').replace(/^\/\//, 'https://'), href, view, like, danmaku, pubdate, goto, area: raw.rcmd_reason && raw.rcmd_reason.content ? String(raw.rcmd_reason.content) : '', duration: formatDuration(raw.duration), live: goto === 'live', score: 0, }; } function formatDuration(d) { if (d == null || d === '') return ''; if (typeof d === 'number') { const s = Math.max(0, Math.floor(d)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; const mm = String(m).padStart(2, '0'); const ss = String(sec).padStart(2, '0'); return h > 0 ? h + ':' + mm + ':' + ss : m + ':' + ss; } return String(d); } function formatCount(n) { n = Number(n) || 0; if (n >= 100000000) return (n / 100000000).toFixed(1).replace(/\.0$/, '') + '亿'; if (n >= 10000) return (n / 10000).toFixed(1).replace(/\.0$/, '') + '万'; return String(n); } async function loadRecommendations() { const endpoints = [ // 热门(相对稳定,无 WBI) 'https://api.bilibili.com/x/web-interface/popular?ps=24&pn=1', // 推荐流(旧路径,有时仍可用) 'https://api.bilibili.com/x/web-interface/index/top/rcmd?ps=24&fresh_idx=1&feed_version=V3', ]; for (const url of endpoints) { try { const json = await gmFetch(url); if (json && json.code === 0) { const list = filterAdList(pickListFromPayload(json)) .map((raw, i) => mapRcmdItem(raw, i)) .filter(Boolean); if (list.length) return { list, source: url.includes('popular') ? '热门' : '推荐' }; } } catch (_) { /* try next */ } } return { list: null, source: '离线示例' }; } async function loadRanking() { const endpoints = [ 'https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all', 'https://api.bilibili.com/x/web-interface/popular/series/one?number=10', ]; for (const url of endpoints) { try { const json = await gmFetch(url); if (json && json.code === 0) { let list = pickListFromPayload(json); if (!list.length && json.data && json.data.list) list = json.data.list; const mapped = filterAdList(list) .map((raw, i) => mapRcmdItem(raw, i)) .filter(Boolean); if (mapped.length) return mapped; } } catch (_) {} } return null; } // --------------------------------------------------------------------------- // Mock(接口全挂时的兜底) // --------------------------------------------------------------------------- function mockVideos() { const grads = [ 'linear-gradient(135deg, #3b1d2a, #1a2740 55%, #123038)', 'linear-gradient(135deg, #243047, #1b1f2a 50%, #2a1830)', 'linear-gradient(135deg, #1d3a34, #15202b 55%, #3a2030)', 'linear-gradient(135deg, #40203a, #1a1d2e 50%, #102a38)', 'linear-gradient(135deg, #2a1f14, #221828 50%, #142430)', 'linear-gradient(135deg, #14283a, #1f1a2e 50%, #2e1a24)', ]; const titles = [ '【壳层演示】接口不可用时的离线推荐流', '如何判断推荐流里哪些是广告', '从零替换 B 站首页(脚本猫方案)', 'Naive UI 做内容站的边界在哪里', 'WBI 签名:前端抓包后要补的一步', '深色 UI 对比度怎么调才不瞎', '虚拟列表:长信息流性能关键', '把去广告脚本和壳层合在一起', '播放页要不要也重写?', '脚本猫 vs 浏览器扩展怎么选', ]; const ups = ['设计瘫痪中', '阿码同学', '像素厨房', '夜行电台']; return titles.map((title, i) => ({ id: 'mock-' + i, title, up: ups[i % ups.length], cover: '', href: '#', view: 120000 + i * 33000, like: 8000 + i * 400, danmaku: 1000 + i * 90, duration: (5 + i) + ':' + String(10 + i).padStart(2, '0'), live: i === 2, area: '示例', score: 0, _grad: grads[i % grads.length], mock: true, })); } // --------------------------------------------------------------------------- // 挂载 Vue 壳层 // --------------------------------------------------------------------------- function getLibs() { const w = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const Vue = w.Vue || window.Vue; const naive = w.naive || window.naive; return { Vue, naive, w }; } function createShellApp(Vue, naive) { const { createApp, ref, computed, h, onMounted } = Vue; const { NConfigProvider, NInput, NIcon, NButton, NBadge, NTag, NRate, NEmpty, NDrawer, NDrawerContent, NAlert, NSpace, NCard, NSpin, NImage, darkTheme, } = naive; const SearchIcon = { render() { return h( 'svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 2, }, [h('circle', { cx: 11, cy: 11, r: 7 }), h('path', { d: 'M20 20l-3.5-3.5' })] ); }, }; const MailIcon = { render() { return h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'currentColor' }, [ h('path', { d: 'M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4-8 5L4 8V6l8 5 8-5z', }), ]); }, }; const template = `
刷新 原站
当前只替换首页信息流。播放页、番剧等仍走原站;点「原站」可临时隐藏壳层(再刷会恢复)。
`; return createApp({ name: 'BiliShell', components: { NConfigProvider, NInput, NIcon, NButton, NBadge, NTag, NRate, NEmpty, NDrawer, NDrawerContent, NAlert, NSpace, NCard, NSpin, NImage, }, setup() { const tabs = ['首页', '番剧', '直播', '国创', '综艺', '音乐', '舞蹈', '游戏', '知识']; const activeTab = ref('首页'); const rails = ['推荐', '热门', '动态', '追番', '收藏', '历史']; const activeRail = ref('推荐'); const keyword = ref(''); const videos = ref([]); const ranking = ref([]); const loading = ref(true); const sourceLabel = ref('加载中'); const drawerActive = ref(false); const feedTitle = computed(() => activeRail.value || '推荐'); async function reload() { loading.value = true; const [feed, rank] = await Promise.all([loadRecommendations(), loadRanking()]); videos.value = feed.list && feed.list.length ? feed.list : mockVideos(); sourceLabel.value = feed.source; if (rank && rank.length) ranking.value = rank.slice(0, 10); else if (feed.list && feed.list.length) ranking.value = feed.list.slice(0, 10); else ranking.value = mockVideos().slice(0, 8); loading.value = false; } function onMountedLoad() { reload(); } function goHome() { activeTab.value = '首页'; activeRail.value = '推荐'; keyword.value = ''; } function onTab(tab) { activeTab.value = tab; if (tab !== '首页') { // 非首页:跳回原站对应频道,壳层保持 const map = { 番剧: 'https://www.bilibili.com/anime/', 直播: 'https://live.bilibili.com/', 国创: 'https://www.bilibili.com/guochuang/', 综艺: 'https://www.bilibili.com/variety/', 音乐: 'https://www.bilibili.com/music/', 舞蹈: 'https://www.bilibili.com/dance/', 游戏: 'https://www.bilibili.com/game/', 知识: 'https://www.bilibili.com/knowledge/', }; if (map[tab]) window.open(map[tab], '_blank'); activeTab.value = '首页'; } } function doSearch() { const kw = (keyword.value || '').trim(); if (!kw) return; window.open('https://search.bilibili.com/all?keyword=' + encodeURIComponent(kw), '_blank'); } function openOfficial() { activateShellClass(false); try { const el = document.getElementById('bili-shell-root'); if (el) el.style.display = 'none'; } catch (_) {} } function fmt(n) { return formatCount(n); } onMounted(onMountedLoad); return { tabs, activeTab, rails, activeRail, keyword, videos, ranking, loading, sourceLabel, drawerActive, feedTitle, reload, goHome, onTab, doSearch, openOfficial, fmt, SearchIcon, MailIcon, naiveTheme: darkTheme, themeOverrides: { common: { primaryColor: '#FB7299', primaryColorHover: '#FC8BAB', primaryColorPressed: '#E5618A', primaryColorSuppl: '#FB7299', borderRadius: '8px', fontFamily: '"HarmonyOS Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif', }, }, }; }, template, }); } function injectShellCss() { GM_addStyle(` #bili-shell-root .bs-root { min-height: 100%; font-family: "HarmonyOS Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; color: #f1f2f3; background: radial-gradient(1000px 360px at 15% -10%, rgba(251,114,153,0.08), transparent 55%), #0f1012; } #bili-shell-root .bs-top { position: sticky; top: 0; z-index: 10; height: 64px; display: flex; align-items: center; gap: 14px; padding: 0 18px; background: rgba(15,16,18,0.9); backdrop-filter: blur(12px); border-bottom: 1px solid #2e3035; } #bili-shell-root .bs-logo { display: flex; align-items: center; gap: 10px; cursor: pointer; flex-shrink: 0; user-select: none; } #bili-shell-root .bs-logo-mark { width: 36px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, #fb7299, #fc9db7 55%, #00aeec); display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 13px; box-shadow: 0 4px 14px rgba(251,114,153,0.35); } #bili-shell-root .bs-logo-text { font-size: 17px; font-weight: 700; } #bili-shell-root .bs-logo-text em { font-style: normal; color: #fb7299; } #bili-shell-root .bs-tabs { display: flex; align-items: stretch; height: 100%; gap: 2px; overflow-x: auto; scrollbar-width: none; } #bili-shell-root .bs-tabs::-webkit-scrollbar { display: none; } #bili-shell-root .bs-tab { position: relative; border: 0; background: transparent; color: #9499a0; padding: 0 12px; font-size: 14px; cursor: pointer; white-space: nowrap; } #bili-shell-root .bs-tab:hover { color: #f1f2f3; } #bili-shell-root .bs-tab.on { color: #f1f2f3; font-weight: 600; } #bili-shell-root .bs-tab.on::after { content: ""; position: absolute; left: 12px; right: 12px; bottom: 0; height: 2px; background: #fb7299; border-radius: 2px 2px 0 0; } #bili-shell-root .bs-center { flex: 1; display: flex; justify-content: center; min-width: 0; } #bili-shell-root .bs-center .n-input { width: min(480px, 100%); } #bili-shell-root .bs-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } #bili-shell-root .bs-pink-btn { --n-color: #fb7299 !important; --n-color-hover: #fc8bab !important; --n-color-pressed: #e5618a !important; } #bili-shell-root .bs-body { display: grid; grid-template-columns: 72px minmax(0, 1fr) 280px; min-height: calc(100vh - 64px); } #bili-shell-root .bs-rail { position: sticky; top: 64px; height: calc(100vh - 64px); padding: 14px 8px; border-right: 1px solid #2e3035; background: rgba(24,25,28,0.5); display: flex; flex-direction: column; gap: 4px; } #bili-shell-root .bs-rail-item { border: 0; background: transparent; color: #9499a0; border-radius: 12px; padding: 12px 6px; font-size: 12px; cursor: pointer; } #bili-shell-root .bs-rail-item:hover, #bili-shell-root .bs-rail-item.on { color: #fb7299; background: rgba(251,114,153,0.12); } #bili-shell-root .bs-main { padding: 18px 20px 40px; min-width: 0; } #bili-shell-root .bs-section-title { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; } #bili-shell-root .bs-section-title h2 { margin: 0; font-size: 18px; } #bili-shell-root .bs-section-title span { color: #9499a0; font-size: 12px; } #bili-shell-root .bs-feed { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 16px 12px; } #bili-shell-root .bs-card { display: block; border-radius: 12px; outline: none; } #bili-shell-root .bs-card:focus-visible .bs-cover { box-shadow: 0 0 0 2px #0f1012, 0 0 0 4px #fb7299; } #bili-shell-root .bs-cover { position: relative; aspect-ratio: 16/9; border-radius: 12px; overflow: hidden; border: 1px solid #2e3035; background: #111; } #bili-shell-root .bs-cover img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.35s ease; } #bili-shell-root .bs-card:hover .bs-cover img { transform: scale(1.05); } #bili-shell-root .bs-cover-grad { position: absolute; inset: 0; } #bili-shell-root .bs-shade { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent 48%); pointer-events: none; } #bili-shell-root .bs-meta { position: absolute; left: 8px; right: 8px; bottom: 8px; display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #fff; font-family: ui-monospace, Consolas, monospace; text-shadow: 0 1px 2px rgba(0,0,0,0.8); } #bili-shell-root .bs-meta .pill { padding: 2px 6px; border-radius: 4px; background: rgba(0,0,0,0.55); font-family: inherit; } #bili-shell-root .bs-meta .pill.live { background: #ff6b6b; font-weight: 600; } #bili-shell-root .bs-info { padding: 10px 2px 0; } #bili-shell-root .bs-info h3 { margin: 0; font-size: 14px; line-height: 1.45; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.9em; } #bili-shell-root .bs-up { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; color: #9499a0; font-size: 12px; } #bili-shell-root .bs-up .dot { opacity: 0.6; } #bili-shell-root .bs-empty { padding: 48px 12px; } #bili-shell-root .bs-foot { margin-top: 24px; } #bili-shell-root .bs-rank { position: sticky; top: 64px; height: calc(100vh - 64px); overflow: auto; border-left: 1px solid #2e3035; padding: 18px 14px 30px; background: rgba(24,25,28,0.35); } #bili-shell-root .bs-rank h3 { margin: 0 0 12px; font-size: 15px; } #bili-shell-root .bs-rank-list { display: flex; flex-direction: column; gap: 2px; } #bili-shell-root .bs-rank-item { display: grid; grid-template-columns: 28px 1fr; gap: 8px; padding: 10px 8px; border-radius: 10px; transition: background 0.15s ease; } #bili-shell-root .bs-rank-item:hover { background: #212224; } #bili-shell-root .bs-rank-item .num { font-family: ui-monospace, Consolas, monospace; font-weight: 700; color: #9499a0; } #bili-shell-root .bs-rank-item:nth-child(1) .num { color: #fb7299; } #bili-shell-root .bs-rank-item:nth-child(2) .num { color: #ffb400; } #bili-shell-root .bs-rank-item:nth-child(3) .num { color: #00aeec; } #bili-shell-root .bs-rank-item .t { font-size: 13px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } #bili-shell-root .bs-rank-item .m { margin-top: 4px; color: #9499a0; font-size: 11px; } @media (max-width: 1100px) { #bili-shell-root .bs-body { grid-template-columns: 72px minmax(0, 1fr); } #bili-shell-root .bs-rank { display: none; } } @media (max-width: 720px) { #bili-shell-root .bs-body { grid-template-columns: 1fr; } #bili-shell-root .bs-rail { display: none; } #bili-shell-root .bs-tabs { display: none; } #bili-shell-root .bs-logo-text { display: none; } } @media (prefers-reduced-motion: reduce) { #bili-shell-root .bs-cover img { transition: none; } } `); } function mountShell() { const { Vue, naive } = getLibs(); if (!Vue || !naive) { console.error('[B站壳层] Vue 或 Naive UI 未加载'); return false; } let host = document.getElementById('bili-shell-root'); if (!host) { host = document.createElement('div'); host.id = 'bili-shell-root'; (document.body || document.documentElement).appendChild(host); } host.style.display = ''; if (!host.dataset.mounted) { host.innerHTML = '
' + 'B站壳层启动中… 若长时间空白,请在脚本猫菜单选择「关闭壳层(用原站)」。' + '
'; } activateShellClass(true); injectShellCss(); const app = createShellApp(Vue, naive); try { app.mount(host); host.dataset.mounted = '1'; } catch (e2) { console.error('[B站壳层] mount failed', e2); host.innerHTML = '
' + '壳层挂载失败:' + (e2 && e2.message ? e2.message : '未知错误') + '
' + '
'; return false; } return true; } function registerMenu() { try { GM_registerMenuCommand(enabled() ? '关闭壳层(用原站)' : '开启壳层', () => { const next = !enabled(); GM_setValue(ENABLED_KEY, next); location.reload(); }); GM_registerMenuCommand('立即刷新壳层数据', () => { location.reload(); }); } catch (_) {} } // --------------------------------------------------------------------------- // 启动 // --------------------------------------------------------------------------- function waitAndMount(retry = 0) { if (!enabled()) return; const { Vue, naive } = getLibs(); if (!Vue || !naive) { if (retry < 40) setTimeout(() => waitAndMount(retry + 1), 50); else console.error('[B站壳层] 依赖超时'); return; } // 等 body if (!document.body) { if (retry < 40) setTimeout(() => waitAndMount(retry + 1), 50); return; } injectHideCss(); const ok = mountShell(); if (!ok && retry < 20) setTimeout(() => waitAndMount(retry + 1), 100); } injectHideCss(); registerMenu(); activateShellClass(enabled()); if (enabled()) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => waitAndMount(), { once: true }); // document-start 下也尽早尝试 waitAndMount(); } else { waitAndMount(); } } })();