注意
使用油猴插件可能会有一些安全风险的,所以这里提供的是源码(未压缩),供大家审查。
介绍
玩转掘金,玩转掘友分。
点击今日掘友分,有更多功能啦。
TODO
- 稳定性持续优化(有时需要刷新页面才能显示的问题)
- 解决进入用户页面卡死,猜测为MutaionObserve持续触发导致
2023.9.27更新
- 修复:沸点弹窗无法发送自定义内容
- 新功能:好文推荐
2023.9.25更新
-
新功能:一键前往我的文章
-
新功能:本页面弹窗发布沸点
-
新功能:一键随机评论/点赞沸点
-
新功能:对本页面文章一键收藏、点赞、评论
2023.9.21 更新
-
新功能:点击
今日掘友分可弹出操作快捷栏 -
新功能:操作快捷栏增加一键签到、抽奖、沾喜气。
-
修复层级遮挡问题(header的z-index调整了)
2023.9.20 更新
- 发文章:支持选择参与热门活动主题
2023.9.19 更新
- 任务进度条排序调整
- 进度条样式调整
- 间隔3秒自动刷新掘友分进度
- 使用rollup+vue重构,数据驱动开发
2023.9.18 更新
- 调整的进度条色彩与样式
- 支持深色模式
- 支持更多页面展示插件并做了一定兼容
最新插件代码地址
// ==UserScript==
// @name 今日掘友分
// @namespace http://tampermonkey.net/
// @version 0.1.1
// @description 帮助玩转掘金社区,快速升级
// @author 奇幻心灵
// @match https://juejin.cn/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=juejin.cn
// @connect juejin.cn
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/vue@2.6.14
// ==/UserScript==
(function (Vue) {
'use strict';
function ___$insertStyle(css) {
if (!css || typeof window === 'undefined') {
return;
}
const style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = css;
document.head.appendChild(style);
return css;
}
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var Vue__default = /*#__PURE__*/_interopDefaultLegacy(Vue);
function request(method, path, data) {
return new Promise((resolve, reject) => {
const url = `https://api.juejin.cn/${path}`;
const headers = {
'Content-Type': 'application/json',
};
const options = {
method,
url,
headers,
responseType: 'json',
onload: xhr => {
// console.log(url, xhr.response);
const { err_no, data, err_msg } = xhr.response;
if (err_no === 0) return resolve(data);
reject({ message: err_msg });
},
onerror: err => reject(err),
};
if (method === 'POST') {
options.data = JSON.stringify(data);
}
GM_xmlhttpRequest(options);
});
}
const post = (...args) => request('POST', ...args);
const get = (...args) => request('GET', ...args);
// 获取成长任务列表
function getGrowthTaskList() {
return post('growth_api/v1/user_growth/task_list', {
growth_type: 1,
}).then(data => {
const { growth_tasks, today_jscore } = data;
// 有完成次数的任务
const limitTasks = [];
Object.values(growth_tasks).forEach(tasks => {
tasks.forEach(task => {
const { limit, score } = task;
// -1表示无限制,任务可以完成任意次数
if (limit !== -1) {
task.maxScore = limit * score;
limitTasks.push(task);
}
});
});
// 排序:优先展示单任务分值较大的任务,已完成的任务往后排
limitTasks.sort((a, b) => {
if (b.done === b.limit && a.done !== a.limit) return -1;
if (a.done === a.limit && b.done !== b.limit) return 1;
if (a.score !== b.score) return b.score - a.score;
return b.maxScore - a.maxScore;
});
return { today_jscore, limitTasks };
});
}
// 查询文章创作话题
function getArticleThemes() {
return post('tag_api/v1/theme/list_by_hot', {
limit: 1000,
cursor: '',
theme_type: 1,
}).then(list => {
// 按推荐取前两个
return list.slice(0, 2).map(item => item.theme);
});
}
// 今日状态-是否已签到
function getIsCheckIn() {
return get('growth_api/v2/get_today_status').then(data => data.check_in_done);
}
// 签到
function checkIn() {
return post('growth_api/v1/check_in').then(data => {
// 增长和总数
const { incr_point, sum_point } = data;
return `签到成功:获得${incr_point}矿石。\n`;
});
}
// 抽奖
function lottery() {
return post('growth_api/v1/lottery/draw').then(data => {
const { lottery_name } = data;
return `抽奖成功:获得${lottery_name}。\n`;
});
}
// 沾喜气
function dipLucky() {
// 查询获奖抽奖
return post('growth_api/v1/lottery_history/global_big', {
page_no: 1,
page_size: 1,
})
.then(({ lotteries }) => lotteries?.[0]?.history_id)
.then(lottery_history_id =>
post('growth_api/v1/lottery_lucky/dip_lucky', {
lottery_history_id,
})
)
.then(data => {
// 幸运值
const { dip_value } = data;
return `沾喜气:获得${dip_value}幸运值。\n`;
});
}
function getRandomItems(list, count = 1) {
const items = [];
const newList = [...list];
for (let i = 0; i < count; i++) {
const randomIndex = Math.floor(Math.random() * newList.length);
items.push(...newList.splice(randomIndex, 1));
}
return count === 1 ? items[0] : items;
}
const comments = ['666', '哈哈哈哈~~~', '打卡~', '坚持打卡每一天'];
// 评论沸点
function publishComments() {
// 查询5个最新沸点
return post('recommend_api/v1/short_msg/recommend', {
id_type: 4,
sort_type: 300,
cursor: '0',
limit: 20,
}).then(list => {
return Promise.all([
// 随机选5条评论
...getRandomItems(list, 5).map(msg =>
post('interact_api/v1/comment/publish', {
item_id: msg.msg_id,
item_type: 4,
comment_content: getRandomItems(comments),
comment_pics: [],
client_type: 2608,
})
),
// 随机选2条点赞
...getRandomItems(list, 2).map(msg =>
post('interact_api/v1/digg/save', {
item_id: msg.msg_id,
item_type: 4,
client_type: 2608,
})
),
]);
});
}
// 发布沸点
function publishShortMsg(content) {
return post('content_api/v1/short_msg/publish', {
content,
sync_to_org: false,
});
}
// 收藏文章至默认收藏集
function addArticleToDefaultCollectionset(articleId) {
// 获取收藏集
return post('interact_api/v2/collectionset/list', {
limit: 10,
cursor: '0',
article_id: articleId,
}).then(list => {
const collectionIds = list.map(collection => collection.collection_id);
const defaultIndex = list.findIndex(collection => collection.is_default);
const defaultCollectionIds = collectionIds.splice(defaultIndex, 1);
return post('interact_api/v2/collectionset/add_article', {
article_id: articleId,
select_collection_ids: defaultCollectionIds,
unselect_collection_ids: collectionIds,
is_collect_fast: false,
}).then(() => list[defaultIndex]);
});
}
// 文章点赞
function upvoteArticle(articleId) {
return post('interact_api/v1/digg/save', {
item_id: articleId,
item_type: 2,
client_type: 2608,
});
}
// 评论文章
function commentArticle(articleId) {
return post('interact_api/v1/comment/publish', {
client_type: 2608,
item_id: articleId,
item_type: 2,
comment_content: '666',
comment_pics: [],
});
}
// 好文推荐
function getRecommendArticles() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://juejin.cn/post/7283028899042017343',
headers: {
'Content-Type': 'text/html; charset=utf-8',
},
onload: xhr => {
// 解析内容
const content = /<h2 data\-id="heading\-0">.*?<ol>(.*?)<\/ol>/s.exec(
xhr.response
)?.[1];
let result = [];
if (content) {
result = content.match(/<li>(.*?)<\/li>/gs).map(li => {
const [, url, name] = /<a href="(.*?)"[^>]+>(.*?)<\/a>/.exec(li);
return { url, name };
});
}
resolve(result);
},
onerror: err => reject(err),
});
});
}
const messagesQueue = [];
let currentMessage = null;
const nextMessage = () => {
if (messagesQueue.length === 0) return;
currentMessage = messagesQueue.shift();
currentMessage.$mount();
};
const MessageConstructor = Vue__default['default'].extend({
props: {
message: String,
stay: {
type: Number,
default: 3000,
},
resolve: Function,
},
render(h) {
return h(
'div',
{
staticClass: 'xx-message-box',
},
this.message
);
},
mounted() {
document.body.appendChild(this.$el);
setTimeout(() => {
this.$destroy();
currentMessage = null;
nextMessage();
this.resolve();
}, this.stay);
},
destroyed() {
this.$el.remove();
},
});
const message = options =>
new Promise(resolve => {
if (typeof options === 'string') {
options = { message: options };
}
const instance = new MessageConstructor({
propsData: { resolve, ...options },
});
if (currentMessage) {
messagesQueue.push(instance);
} else {
currentMessage = instance;
instance.$mount();
}
});
const ConfirmBox = {
name: 'ConfirmBox',
props: {
title: {
type: String,
default: '提示',
},
content: String,
},
render(h) {
const content = this.$slots.default || this.content;
return h(
'div',
{
staticClass: 'xx-confirm-box',
},
[
h('div', { staticClass: 'xx-confirm-box-title' }, [
h('div', { staticClass: 'title' }, this.title),
h(
'a',
{ staticClass: 'close', on: { click: this.handleClose } },
'关闭'
),
]),
h('div', { staticClass: 'xx-confirm-box-content' }, [content]),
h('div', { staticClass: 'xx-confirm-box-footer' }, [
h('button', { on: { click: this.handleClose } }, '取消'),
h('button', { on: { click: this.handleConfirm } }, '确认'),
]),
]
);
},
methods: {
handleClose(e) {
this.$emit('close', e);
},
handleConfirm(e) {
this.$emit('confirm', e);
},
},
};
const ConfirmBoxConstructor = Vue__default['default'].extend(ConfirmBox);
const confirm = (options = {}) => {
if (typeof options === 'string') options = { content: options };
const { content, title, onConfirm, onClose } = options;
let instance;
const props = { content, title };
const on = {};
const close = () => instance.$destroy();
on.close = onClose ? () => onClose(close) : close;
on.confirm = onConfirm ? () => onConfirm(close) : close;
if (typeof content === 'string') {
instance = new ConfirmBoxConstructor({ propsData: props, on });
} else {
const CompConstructor = Vue__default['default'].extend({
render: h => h(ConfirmBox, { props, on }, [content]),
});
instance = new CompConstructor();
}
instance.$on('hook:mounted', () => {
document.body.appendChild(instance.$el);
});
instance.$on('hook:destroyed', () => {
instance.$el.remove();
});
instance.$mount();
};
const ActionItem = {
props: {
disabled: {
type: Boolean,
default: false,
},
name: String,
},
render(h) {
return h(
'div',
{
staticClass: 'action-item',
class: {
disabled: this.disabled,
},
on: {
click: e => this.$emit('click', e),
},
},
this.name
);
},
};
var QuickOperates = {
props: {
isCheckIn: Boolean,
},
data() {
return {
recommendArticles: [],
};
},
render(h) {
// 签到/抽奖
const CheckIn = h(ActionItem, {
props: {
name: this.isCheckIn ? '已签到/抽奖' : '签到/抽奖',
disabled: this.isCheckIn,
},
on: {
click: e => {
if (this.isCheckIn) return;
const p1 = checkIn().then(msg1 => {
this.$emit('checkIn');
return lottery().then(msg2 => msg1 + msg2);
});
Promise.all([p1, dipLucky()]).then(([msg1, msg2]) => {
message(msg1 + msg2);
});
},
},
});
// 前往我的文章
const GoMyArticle = h(ActionItem, {
props: {
name: '前往我的文章',
},
on: {
click: e => {
const userId = localStorage.getItem(
'user_first_visit_dispatch_coupon'
);
window.open(`/user/${userId}/posts`);
},
},
});
// 发布一条沸点
const PublishShortMsg = h(ActionItem, {
props: {
name: '发布一条沸点',
},
on: {
click: e => {
const content = h(
'textarea',
{
attrs: { rows: '3' },
style: { width: '300px' },
},
'每日打卡,从使用今日掘友分油猴插件开始。\nhttps://juejin.cn/post/7280006996572340283'
);
confirm({
title: '输入沸点内容',
content,
onConfirm: close => {
const msg = content.elm.innerHTML;
close();
publishShortMsg(msg).then(msg => {
message('发布一条沸点成功').then(() => {
window.open(`/pin/${msg.msg_id}`);
});
});
},
});
},
},
});
// 评论/点赞沸点
const PublishComments = h(ActionItem, {
props: {
name: '评论/点赞沸点',
},
on: {
click: e => {
publishComments().then(() => {
message('评论/点赞成功');
});
},
},
});
// 收藏/点赞/评论文章
const CollectArticle = h(ActionItem, {
props: {
name: '收藏/点赞/评论文章',
},
on: {
click: e => {
const articleId = location.pathname.split('/').pop();
addArticleToDefaultCollectionset(articleId).then(
defaultCollection => {
message(
`文件成功加入到默认收藏集(${defaultCollection.collection_name})中`
);
}
);
upvoteArticle(articleId).then(() => {
message('文章点赞成功!');
});
commentArticle(articleId).then(() => {
message('文章评论成功!');
});
},
},
});
return h('div', { staticClass: 'quick-operates' }, [
h('div', { staticClass: 'action-list' }, [
CheckIn,
GoMyArticle,
PublishShortMsg,
PublishComments,
CollectArticle,
]),
h('div', { staticClass: 'recommend-articles' }, [
h('div', { staticClass: 'recommend-articles-title' }, '好文推荐'),
h(
'ul',
{},
this.recommendArticles.map((article, i) =>
h('li', {}, [
h(
'a',
{ attrs: { href: article.url, target: '_blank' } },
`${i + 1}.${article.name}`
),
])
)
),
]),
]);
},
mounted() {
getRecommendArticles().then(list => {
this.recommendArticles = list;
});
},
};
const TaskDetailPopup = {
props: {
task: {
type: Array,
default() {
return [];
},
},
themes: {
type: Array,
default() {
return [];
},
},
},
render(h) {
const { icon, title, score, done, limit, task_id, web_jump_url } =
this.task;
const content = h('div', { staticClass: 'task-detail-popup-content' }, [
h('div', { staticClass: 'task-icon' }, [
h('img', { attrs: { src: icon, alt: title } }),
]),
h('div', { staticClass: 'task-info' }, [
h('span', {}, title),
h('span', {}, `掘友分+${score},已完成${done}/${limit}`),
]),
]);
let extra;
// 发文
if (task_id === 5 && this.themes.length) {
extra = h(
'div',
{ staticClass: 'task-detail-popup-extra' },
this.themes.map(theme => {
const { name, theme_id } = theme;
return h(
'a',
{
attrs: {
href: `${web_jump_url}&theme_id=${theme_id}`,
target: '_blank',
},
on: {
click(e) {
e.stopPropagation();
},
},
},
`# ${name}`
);
})
);
}
return h('div', { staticClass: 'task-detail-popup' }, [content, extra]);
},
};
var ProgressBar = {
name: 'ProgressBar',
data() {
return {
// 今日已获掘友分
today_jscore: 0,
// 有完成次数的任务
limitTasks: [],
// 头部区域是否展示
headerVisible: true,
themes: [],
showQuickOperates: false,
isCheckIn: false,
};
},
render(h) {
const { themes } = this;
return h(
'div',
{ staticClass: 'pl-progressbar', class: { top: !this.headerVisible } },
[
// 进度条
h(
'div',
{ staticClass: 'pl-progressbar-container' },
this.limitTasks.map(task => {
const { done, limit, maxScore } = task;
// 任务进度
const taskProgressItems = [];
for (let i = 0; i < limit; i++) {
taskProgressItems.push(
h('div', {
staticClass: 'task-progress-item',
class: { 'item-done': i < done },
})
);
}
return h(
'div',
{
staticClass: 'task-item',
class: {
'all-finished': done === limit,
'not-start': done === 0,
},
// 按比例占据宽度
style: {
'flex-grow': maxScore,
},
on: {
// 点击任务跳转去完成
click() {
window.open(task.web_jump_url);
},
},
},
[
h(
'div',
{ staticClass: 'progress-item-wrapper' },
taskProgressItems
),
h(TaskDetailPopup, { props: { task, themes } }),
]
);
})
),
// 总分
h('div', { staticClass: 'pl-progressbar-totalscore' }, [
h(
'span',
{
style: 'line-height: 1; cursor: pointer',
on: {
click: () => {
// 关闭
if (this.showQuickOperates) {
this.showQuickOperates = false;
} else {
Promise.all([this.getIsCheckIn()]).finally(() => {
this.showQuickOperates = true;
});
}
},
},
},
`今日掘友分 ${this.today_jscore}`
),
h(QuickOperates, {
style: { display: this.showQuickOperates ? '' : 'none' },
props: {
isCheckIn: this.isCheckIn,
},
on: {
checkIn: () => (this.isCheckIn = true),
},
}),
]),
]
);
},
mounted() {
this.observeHeader();
const loop = () => {
this.getGrowthTaskList().finally(() => {
setTimeout(loop, 3000);
});
};
loop();
this.getArticleThemes();
},
methods: {
// 需要监听头部的展示与隐藏
observeHeader() {
const header = document.querySelector('header.main-header');
const observer = new MutationObserver(mutationsList => {
const [record] = mutationsList;
if (record.attributeName !== 'class') return;
this.headerVisible = header.classList.contains('visible');
});
observer.observe(header, {
attributes: true,
childList: false,
subtree: false,
});
},
getGrowthTaskList() {
return getGrowthTaskList().then(data => {
Object.assign(this, data);
});
},
getArticleThemes() {
return getArticleThemes().then(themes => {
this.themes = themes;
});
},
getIsCheckIn() {
return getIsCheckIn().then(isCheckIn => {
this.isCheckIn = isCheckIn;
});
},
},
};
___$insertStyle(".main-header {\n box-shadow: none !important;\n}\n\n.list-block .list-header.sticky {\n top: calc(5rem + 12px) !important;\n}\n.list-block .list-header.sticky.top {\n top: 12px !important;\n}\n\n.view-nav {\n top: calc(5rem + 12px) !important;\n}\n\n.pl-progressbar {\n display: flex;\n justify-content: space-between;\n padding: 1px;\n font-size: 12px;\n height: 10px;\n color: var(--juejin-brand-1-normal);\n background: var(--juejin-navigation);\n position: fixed;\n top: 5rem;\n left: 0;\n right: 0;\n z-index: 249;\n transition: transform 0.2s;\n}\n.pl-progressbar.top {\n transform: translate3d(0, -5rem, 0);\n}\n.pl-progressbar-container {\n flex-grow: 1;\n display: flex;\n}\n.pl-progressbar-totalscore {\n align-self: center;\n padding: 0 20px;\n position: relative;\n user-select: none;\n}\n\nbody[data-theme=dark] .pl-progressbar {\n background: var(--juejin-navigation);\n}\n\n.task-item {\n overflow: hidden;\n display: flex;\n cursor: pointer;\n}\n.task-item .progress-item-wrapper {\n position: relative;\n flex: 1;\n display: flex;\n}\n.task-item .progress-item-wrapper::before {\n content: \"\";\n border: 5px solid var(--juejin-brand-1-normal);\n border-left: 3px solid var(--juejin-navigation);\n position: absolute;\n z-index: 1;\n}\n.task-item .progress-item-wrapper::after {\n content: \"\";\n border: solid 5px var(--juejin-navigation);\n border-left: 3px solid transparent;\n position: absolute;\n right: -5px;\n}\n.task-item .task-detail-popup {\n position: absolute;\n top: 12px;\n display: none;\n background: var(--juejin-navigation);\n padding: 5px 10px;\n border-radius: 5px;\n border: solid 1px;\n z-index: 999;\n}\n.task-item .task-detail-popup-content {\n display: flex;\n align-items: center;\n}\n.task-item .task-detail-popup-content .task-icon {\n width: 40px;\n height: 40px;\n flex-shrink: 0;\n}\n.task-item .task-detail-popup-content .task-icon img {\n width: 100%;\n height: 100%;\n}\n.task-item .task-detail-popup-content .task-info {\n margin-left: 10px;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n.task-item .task-detail-popup-extra {\n display: flex;\n flex-direction: column;\n margin-top: 5px;\n}\n.task-item.all-finished .progress-item-wrapper::after {\n border-left-color: var(--juejin-brand-1-normal);\n}\n.task-item.not-start .progress-item-wrapper::before {\n border-top-color: transparent;\n border-bottom-color: transparent;\n border-right-color: transparent;\n}\n.task-item:hover .progress-item-wrapper {\n opacity: 0.5;\n}\n.task-item:hover .task-detail-popup {\n display: block;\n}\n.task-item .task-progress-item {\n flex: 1;\n background: #f2f3f5;\n position: relative;\n}\n.task-item .task-progress-item.item-done {\n background: var(--juejin-brand-1-normal);\n}\n.task-item .task-progress-item + .task-progress-item {\n margin-left: 1px;\n}\n\n.quick-operates {\n position: absolute;\n background: #fff;\n right: 0;\n top: 20px;\n padding: 20px;\n border-radius: 4px;\n box-shadow: 0 0 24px rgba(81, 87, 103, 0.16);\n}\n.quick-operates .action-list {\n width: 160px;\n}\n.quick-operates .action-list .action-item {\n line-height: 1.8;\n cursor: pointer;\n}\n.quick-operates .action-list .action-item:hover {\n background-color: #e8f3ff;\n}\n.quick-operates .action-list .action-item.disabled {\n color: #777;\n cursor: not-allowed;\n}\n\n.recommend-articles {\n margin-top: 5px;\n}\n.recommend-articles-title {\n font-weight: 700;\n color: #777;\n line-height: 2;\n}\n\n.xx-message-box, .xx-confirm-box {\n position: fixed;\n top: 80px;\n left: 0;\n right: 0;\n margin: auto;\n padding: 5px 10px;\n width: fit-content;\n background: #e8f3ff;\n z-index: 2999;\n color: var(--juejin-brand-1-normal);\n border-radius: 5px;\n white-space: pre-wrap;\n line-height: 1.8;\n border: solid 1px;\n}\n\n.xx-confirm-box-title {\n display: flex;\n justify-content: space-between;\n padding-bottom: 4px;\n}\n.xx-confirm-box-title .title {\n flex: 1;\n font-weight: 600;\n}\n.xx-confirm-box-footer {\n margin-top: 10px;\n display: flex;\n}\n.xx-confirm-box-footer button {\n flex: 1;\n}\n.xx-confirm-box-footer button:first-child {\n background-color: #7bbdff;\n}");
const app = new Vue__default['default']({ render: h => h(ProgressBar) }).$mount();
function insertApp() {
document.querySelector('.main-header-box').appendChild(app.$el);
}
const progressBar = app.$children[0];
const observeCb = () => {
insertApp();
progressBar.observeHeader();
};
const observeConfig = { attributes: false, childList: false, subtree: false };
// 请求完接口再展示dom
progressBar.getGrowthTaskList().then(() => {
insertApp();
// 由于是ssr会导致el被删除
const observer = new MutationObserver(observeCb);
observer.observe(document.querySelector('#__nuxt'), {
...observeConfig,
attributes: true,
});
// 单页导航
const observer2 = new MutationObserver(observeCb);
observer2.observe(document.querySelector('#juejin'), {
...observeConfig,
childList: true,
});
});
}(Vue));