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=201905
2.部署教程
3.代码如下
(() => {
'use strict';
const getEl = (name) => document.querySelector(name);
// comment area
const elComment = getEl('#commentapp') || getEl('#comment-module');
if (!elComment) return;
if (location.pathname.startsWith('/video/')) {
// observe once
const 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 need
observeOnce(elComment, 'bili-comments', (el) => {
// info to show
const 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 hook
if (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 watcher
observeOnce(el.shadowRoot, '#feed', (el) => {
// handle list
const 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 handle
el.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 exist
new MutationObserver((mutations, ob) => {
const elReplyList = elComment.querySelector('.reply-list');
if (elReplyList) {
ob.disconnect();
watchReply(elReplyList);
}
}).observe(elComment, {
childList: true,
subtree: true
});
const watchReply = (elReplyList) => {
// 防重复执行mutation
let 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);
}
};
// 添加css
elReplyList.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 监听 两条路径来可靠插入与更新这些标记。
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论