1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/E8o0aM若失效,可用地址
阿里云:
服务器购买地址
https://t.aliyun.com/U/E8o0aM若失效,可用地址
https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d腾讯云:
https://curl.qcloud.com/wJpWmSfU若失效,可用地址
https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console华为云
https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=2019052.部署教程
3.代码如下
(() => {'use strict';const getEl = (name) => document.querySelector(name);// comment areaconst elComment = getEl('#commentapp') || getEl('#comment-module');if (!elComment) return;if (location.pathname.startsWith('/video/')) {// observe onceconst observeOnce = (parent, sel, callback, options) => {new MutationObserver((mutations, ob) => {const el = parent.querySelector(sel);if (el) {ob.disconnect();callback(el);}}).observe(parent, {childList: true,// subtree: true,...options});};// watch on needobserveOnce(elComment, 'bili-comments', (el) => {// info to showconst id = '--' + Math.floor(Math.random() * 10000),labelClass = 'com-ip' + id,colorClass = labelClass + '-input';const getExtraEle = (text) => {if (!text) return '';const ele = document.createElement('label');ele.part = labelClass;ele.innerHTML = `${text}<input type="color" part=${colorClass} />`;ele.title = '点击调色';for (const k in colorEvent) {ele.firstElementChild[k] = colorEvent[k];}return ele;};const colorEvent = {oninput(e) {elComment.style.setProperty(id, e.target.value);},onchange(e) {localStorage._ipsv = e.target.value;},onclick(e) {e.target.value = elComment.style.getPropertyValue(id);}};elComment.style.setProperty(id, localStorage._ipsv || '#9499a0');GM_addStyle(`bili-comments::part(${labelClass}){color:var(${id})}bili-comments::part(${colorClass}){overflow:hidden;width:0;height:0;border:none;padding:0;visibility:hidden;}`);// by hookif (customElements.get('bili-comment-renderer')) {const update = (proto, isFooter, key = 'updated') => {const refUpdated = proto[key];const baseFunc = function (e) {refUpdated.call(this, e);this.setAttribute('exportparts', labelClass + ',' + colorClass);};proto[key] = !isFooter? baseFunc: function (e) {baseFunc.call(this, e);if (isFooter) {this.renderRoot.querySelector(`:host>label[part=${labelClass}]`)? this.renderRoot.firstElementChild.replaceWith(getExtraEle(this.data.reply_control.location)): this.renderRoot.prepend(getExtraEle(this.data.reply_control.location));}};};const map = {'bili-comment-action-buttons-renderer': true,'bili-comment-thread-renderer': null,'bili-comment-renderer': null,'bili-comment-replies-renderer': null,'bili-comment-reply-renderer': null};for (const k in map) {update(customElements.get(k).prototype, map[k]);}return;}// by watcherobserveOnce(el.shadowRoot, '#feed', (el) => {// handle listconst handleList = (arr) => {arr.forEach((e) => {e.setAttribute('exportparts', labelClass + ',' + colorClass);observeOnce(e.shadowRoot, '#commentapp', (el) => {el.setAttribute('exportparts', labelClass + ',' + colorClass);observeOnce(el.shadowRoot, '#footer', (fel) => {fel.firstElementChild.setAttribute('exportparts', labelClass + ',' + colorClass);fel.firstElementChild.shadowRoot.prepend(getExtraEle(fel.firstElementChild.data.reply_control.location));// more to handleel.nextElementSibling.firstElementChild.setAttribute('exportparts',labelClass + ',' + colorClass);const handleList = (arr) => {arr.forEach((e) => {e.setAttribute('exportparts', labelClass + ',' + colorClass);observeOnce(e.shadowRoot, '#footer', (el) => {el.firstElementChild.setAttribute('exportparts',labelClass + ',' + colorClass);new MutationObserver(() => {el.firstElementChild.shadowRoot.querySelector(`>label[part=${labelClass}]`)? el.firstElementChild.shadowRoot.firstElementChild.replaceWith(getExtraEle(el.firstElementChild.data.reply_control.location)): el.firstElementChild.shadowRoot.prepend(getExtraEle(el.firstElementChild.data.reply_control.location));}).observe(el.previousElementSibling.children[1].shadowRoot, {childList: true,subtree: true});});});};new MutationObserver((mutations) => {handleList(mutations.filter((e) => e.addedNodes[0]?.nodeName === 'BILI-COMMENT-REPLY-RENDERER').flatMap((e) => e.addedNodes[0]));}).observe(el.nextElementSibling.firstElementChild.shadowRoot.querySelector('#expander-contents'),{childList: true});handleList(Array.from(el.nextElementSibling.firstElementChild.shadowRoot.querySelectorAll('#expander-contents>bili-comment-reply-renderer')));});});});};new MutationObserver((mutations) => {handleList(mutations.filter((e) => e.addedNodes[0]?.nodeName === 'BILI-COMMENT-THREAD-RENDERER').flatMap((e) => e.addedNodes[0]));}).observe(el, {childList: true});handleList(Array.from(el.children));});});return;}// if comments existnew MutationObserver((mutations, ob) => {const elReplyList = elComment.querySelector('.reply-list');if (elReplyList) {ob.disconnect();watchReply(elReplyList);}}).observe(elComment, {childList: true,subtree: true});const watchReply = (elReplyList) => {// 防重复执行mutationlet flag;const { apiData } =elComment.firstElementChild.__vue_app__.config.globalProperties.$store.state,id = '--' + Math.floor(Math.random() * 10000),labelClass = 'com-ip' + id;// 要展示的信息const getExtraEle = (text) => {if (!text) return '';const ele = document.createElement('label');ele.className = labelClass;ele.innerHTML = `${text}<input type="color"/>`;ele.title = '点击调色';return ele;};// 处理子级评论// 子评论下有新的子评论,也可能是原评论位置变动const handleSubReply = (el) => {// console.log("%c子评论", "font-size:16px;color:cyan");// const { reply_control } = el.__vueParentComponent.ctx.subReply;const { rootReplyId, userId } = el.querySelector('.sub-reply-avatar').dataset;const { reply_control } = findReply(rootReplyId,false,userId,Array.from(el.parentNode.children).indexOf(el));el.querySelector('.sub-reply-info').prepend(getExtraEle(reply_control.location));};// get by rrid...const findReply = (rrid, isRoot, subUid, subIndex) => {const rootReply = apiData.replyList.res.data.replies.find((e) => e.rpid_str === rrid);return ((isRoot? rootReply: rootReply?.replies.filter((e) => !e.invisible).find((e, i) => e.mid_str === subUid && i === subIndex)) ?? {reply_control: {}});};// 观察评论区节点并给新评论增加ip等额外信息展示new MutationObserver((mutations) => {if (flag) {flag = null;return;}mutations.filter((e) => e.addedNodes.length > 0).forEach((e) => {if (e.type !== 'childList' || e.addedNodes[0].nodeType !== 1) return;// 根评论下有新的子评论if (e.target === elReplyList && e.addedNodes[0].classList.contains('reply-item')) {// const { reply_control } =// e.addedNodes[0].__vueParentComponent.ctx.reply;const { reply_control } = findReply(e.addedNodes[0].querySelector('.root-reply-avatar').dataset.rootReplyId,true);e.addedNodes[0].querySelector('.reply-info').prepend(getExtraEle(reply_control.location));// 处理根评论下的子评论e.addedNodes[0].querySelectorAll('.sub-reply-item').forEach((se) => {handleSubReply(se);});flag = true;return;}if (e.target.classList.contains('sub-reply-list') &&e.addedNodes[0].classList.contains('sub-reply-item') &&!e.addedNodes[0].querySelector('.sub-reply-info > .' + labelClass)) {handleSubReply(e.addedNodes[0]);flag = true;}});}).observe(elReplyList, {attributes: false,childList: true,subtree: true});// 通过列表代理color input相关事件elReplyList.oninput = (e) => {if (e.target.parentNode.className === labelClass) {elReplyList.style.setProperty(id, e.target.value);}};elReplyList.onchange = (e) => {if (e.target.parentNode.className === labelClass) {// elReplyList.style.setProperty(id, e.target.value);localStorage._ipsv = e.target.value;}};elReplyList.onclick = (e) => {if (e.target.nodeName === 'INPUT' && e.target.parentNode.className === labelClass) {e.target.value = elReplyList.style.getPropertyValue(id);}};// 添加csselReplyList.style.setProperty(id, localStorage._ipsv || '#9499A0');GM_addStyle(`.${labelClass}{margin-right:10px;color:var(${id})}.${labelClass}>input{overflow:hidden;width:0;height:0;border:none;visibility:hidden;}`);};})();
这是一段用于 B 站 PC 端(视频页与番剧页)的脚本,用来在评论区把评论归属(IP 属地/归属地)显示出来,并且在每条(根/子)评论的昵称信息前面插入一个可点击的颜色选择器,你可以自定义这段"归属文字"的颜色;颜色会被保存到 localStorage,刷新后仍生效。脚本同时兼容:
新版评论组件(
bili-*Web Components,Shadow DOM);旧版 Vue 评论区域(
#commentapp/#comment-module)。
主要方法 / 关键模块作用
入口与环境
通过
@match匹配bilibili.com/video/*与bilibili.com/bangumi/play/*,document-end时运行。先定位评论根容器:
#commentapp或#comment-module,找不到即退出。observeOnce(parent, sel, callback, options)
工具函数:对parent做一次性 Mutation 监听,一旦出现sel节点就断开并回调。用来等待异步渲染出来的评论组件。颜色标记与样式注入
新版:利用
::part将 Shadow DOM 内样式暴露并设色;旧版:直接对插入的
.labelClass设color: var(--xxxx)。oninput:实时更新自定义属性颜色;onchange:把选择的颜色持久化到localStorage._ipsv;onclick:点颜色输入时,预填当前已应用的颜色。随机生成 CSS 自定义属性名
--xxxx,并把当前颜色写入elComment.style.setProperty(id, …)。getExtraEle(text):生成显示"归属 text + 隐藏 color input"的<label>,并挂上三种事件:GM_addStyle(...):新版 Web Components 适配
bili-comment-action-buttons-renderer(评论底部按钮行)bili-comment-thread-renderer(根评论)bili-comment-renderer(单条评论)bili-comment-replies-renderer(子评容器)bili-comment-reply-renderer(子评论)通过
customElements.get(...)拿到如下组件的原型并"猴补丁":核心做法:重写它们的
updated()(或等效生命周期)以追加exportparts,并在 footer 区域插入/更新我们生成的"归属+颜色"<label>。这样即使在 Shadow DOM 内部,也能把样式透出并渲染颜色。旧版 Vue 评论适配
从
elComment.firstElementChild.__vue_app__.config.globalProperties.$store.state.apiData里拿到评论数据;findReply(rrid, isRoot, subUid, subIndex):用根评论rpid和子评论mid/索引去原始数据里找到对应reply_control.location(归属文案)。handleSubReply(el):为新增子评论在.sub-reply-info前插入归属<label>。进入
watchReply(elReplyList)分支,走 Vue 的数据结构:MutationObserver监听根/子评论的增量渲染,给每个新增的根评论和子评论预置/更新归属<label>。给列表容器代理
oninput/onchange/onclick,统一处理颜色变更与持久化。颜色持久化与初始化
初始时把
localStorage._ipsv写到自定义属性上,后续颜色选择器改变会同步更新并保存。Shadow DOM 样式桥接
新版组件使用
exportparts+::part(...)技术,让外部 CSS 能作用到 Web Components 内部指定元素,避免 Shadow DOM 隔离带来的样式无法覆盖问题。
脚本的核心就是给每条评论(含子评)前注入"IP 归属"标记,并提供一个颜色选择器来动态改这段标记的颜色;为了兼容 B 站新旧两套评论实现,它分别用了 Web Components 的 ::part 技术与 Vue 数据解析 + DOM 监听 两条路径来可靠插入与更新这些标记。
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论