掘金插件

掘金插件 - 赞文章分类功能

// ==UserScript==
// @name         掘金插件
// @namespace    https://github.com/zhukunpenglinyutong/Juejin-Plugin
// @version      1.0
// @description  掘金插件 - 赞文章分类功能
// @author       朱昆鹏
// @match        *://juejin.im/user/*
// @require https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js
// @grant        none
// ==/UserScript==

let globalData = null; // 全局数据

// 实现一个简单的路由监听(监听跳转是否是主页)
window.addEventListener('load', function (e) {
    var reg = /https:\/\/juejin.im\/user/;
    if (reg.test(e.target.URL)) init(); // 如果进入主页,触发
})


// ============================================================================================
// ===================== 初始化部分:创建 点赞文章分类,等待点击事件触发 ==============================
// ============================================================================================

function init() {
    //(异步是因为有时候掘金网站加载太慢)
    setTimeout(() => {
        createMoreItem(); // 创建 点赞文章分类 div
        eventBus(); // 点击事件监听(并执行拿取数据 和 创建展示DOM的方法)
    }, 500)
}

// 创建 点赞文章分类 div
function createMoreItem() {
    let selectItems = document.querySelectorAll('.nav-item.not-in-scroll-mode .more-panel .more-item')
    let newA = selectItems[1].cloneNode() // 赋值节点
    newA.attributes.removeNamedItem('href') // 删除掉href跳转,因为就当个触发器使用
    newA.id = 'jSort'
    newA.innerText = '文章分类'
    selectItems[1].parentNode.appendChild(newA)
}

// 管理事件的函数
function eventBus() {
    let jSort = document.getElementById('jSort')
    jSort.addEventListener('click', async event => {
        let dialogId = document.getElementById('dialogId')

        if (dialogId) { // 已经点击过分类查看了,只是隐藏到了,这里避免重复拿取,浪费性能
          dialogId.style.display = 'inline';
        } else {
          let dataReq = new DataReq()
          let userId = location.pathname.match(/\w{24}/)[0]
          let res = await dataReq.getData(userId) // 获取数据
          createDOM(res) // 创建展示数据的DOM层
        }

    })
}



// ============================================================================================
// =================== 数据处理部分:逻辑处理部分(请求数据 && 处理数据) =============================
// ============================================================================================

// axios配置
let instance = axios.create({
    baseURL: 'https://user-like-wrapper-ms.juejin.im/v1/user/',
    timeout: 30000,
    headers: { "X-Juejin-Src": "web" }
});

// 数据请求类
class DataReq {

    constructor () {
    }

    /**
     * 请求数据(对外开放)
     * @param {String} userId
     */
    getData(userId) {

        console.log('userID', userId)

        return new Promise (async (resolve, reject) => {
            // 因为现成的掘金接口,一次只能拿到30个数据,如果点赞很多文章的话,我们需要拼接很多次请求才行
            // 思考一:等一个请求返回后,再把page的值增加,直到返回数组为空,不再进行请求,这样是个串行的
            // ✅ 思考二:在发送请求之前,先发送一个 page=0&pageSize=0 这时会返回你有多少点赞的文章数量,根据数量这个就能获取到需要发送的次数,这个是并行的

            let that = this

            // 根据返回的数组,建立对应数量的axios请求
            let totalArr = await this.getTotal(userId)
            let aixosArr = totalArr.map(item => instance.get(item))


            axios.all(aixosArr)
                .then(axios.spread( function () {
                let arr = []
                for (let i = 0; i < arguments.length; i++) {
                    arr.push(arguments[i].data.d.entryList)
                }
                let resData = that.dataHandle(arr.flat())
                resolve(resData) // 经过处理类处理后的数据
            }));
        })

    }


    /**
     * 请求page=0&pageSize=0 结果用于生成多次axios请求(原因是掘金接口一次拿不到全部点赞数据,上限是30)
     * (内部使用)
     * @param { String } 用户ID
     * @return { Array } ['请求地址一', '', ...]
     */
    getTotal(userId) {
        return new Promise((resolve, reject) => {

            instance.get(`${userId}/like/entry?page=0&pageSize=0`)
                .then(res => {

                let len = Math.ceil(res.data.d.total/30),
                    arr = [];

                for (let i = 0; i < len; i++) {
                    arr.push(`${userId}/like/entry?page=${i}&pageSize=30`)
                }

                resolve(arr)
            })
                .catch(e => {
                reject(e)
            })

        })
    }


    /**
     * 数据处理函数(内部使用)(此处代码写的不好qwq)
     */
    dataHandle (data) {
        let countedNames = data
        .map( item => item.tags.map(jtem => jtem.title ) )
        .flat(Infinity)
        .reduce((allNames, name) => {
            if (name in allNames) {
                allNames[name]++;
            } else {
                allNames[name] = 1;
            }
            return allNames;
        }, {})
        // ['Vue.js', 'Vue.js', '...', ...] ===> [ { name: 'Vue.js', num: 6}, {'Jquery': 4}, {} ]

        let resArr = []
        for (let prop in countedNames) {
            resArr.push({
                name: prop,
                num: countedNames[prop],
                data: []
            })
        }

        resArr.sort((a, b) =>  b.num - a.num); // 按照数量排序

        // 第二部分:
        // 获取 所有值
        let aaa = data.map( item => {
            return {
                title: item.title,
                originalUrl: item.originalUrl,
                username: item.user.username,
                objectId: `https://juejin.im/user/${item.user.objectId}`,
                tags: item.tags.map( j => j.title )
            }
        })


        // 整成想要的格式
        resArr.forEach( item => {
            aaa.forEach( jtem => {
                jtem.tags.some( j => j === item.name) ? item.data.push(jtem) : ''
            })
        })

        return resArr
    }

}



// ============================================================================================
// ========================= DOM生成层,根据数据生成对应的弹出层和交互  =============================
// ============================================================================================

const createDOM = (res) => {
    globalData = res

    // 响应式监听属性,响应点击标签时候的视图切换
    let observeData = {
        select: '' // 当前选中的分类名称
    }

    renderBasic() // 渲染弹出框基本结构
    defineReactive(observeData, 'select', observeData.select) // 监听响应式
    observeData.select = res[0].name // 初始化,渲染第一个分类中的数据
    createDOMEventBus(observeData) // 启动事件监听
}

// 渲染弹出框基本结构
function renderBasic() {

    let dialogMianItem = '' // 根据数据,生成底部导航栏结构
    globalData.forEach( item => {
      dialogMianItem += `<div class="dialog-mian-item" style="position: relative;margin-right: 25px;">
                <button class="dialogBtn" style="padding: 9px 15px;font-size: 12px;border-radius: 3px;min-width: 100px;">${item.name}</button>
                <sup style="position: absolute;top: 0;right: 10px;transform: translateY(-50%) translateX(100%);background-color: #f56c6c;border-radius: 10px;color: #fff;display: inline-block;font-size: 12px;height: 18px;line-height: 18px;padding: 0 6px;text-align: center;white-space: nowrap;">
                  ${item.num}
                </sup>
              </div>`
    })


    // 创建弹出层
    let div = document.createElement('div')
    div.id = 'dialogId'
    // 模板中展示的内容
    div.innerHTML = `
      <div class="dialog"
        style="margin: 15vh auto 50px;width: 60vw;height: 600px;background: #fff;opacity: 1;border-radius: 2px;box-shadow: 0 1px 3px rgba(0,0,0,.3);box-sizing: border-box;">
        <div class="dialog-header" style="padding: 20px 20px 10px;line-height: 24px;font-size: 18px;color: #303133;margin-bottom: 15px;display: flex;justify-content: space-between;">
          <div>文章分类 <input style="margin-left: 10px;" disabled value="搜索功能8.1日开放" /> </div>
          <div id="dialog-close" style="cursor: pointer;">X</div>
        </div>
        <div class="dialog-mian">
          <div class="dialog-mian" style="padding: 0 22px;">
            <div class="dialog-mian-lang" style="display: flex;justify-content: space-between;padding-top: 12px;overflow-y: auto;">${dialogMianItem}</div>
            <div class="dialog-content" style="margin-top: 10px;height: 430px;overflow-y: auto;">

            </div>
          </div>
        </div>
      </div>
    `
    let body = document.querySelector('body')
    div.style = `position: fixed;height: 100vh;width: 100vw;top: 0;left: 0;z-index: 1000;`
    body.appendChild(div)
}

// 生成详细文章列表
function createDialogContent(selectName) {

  let randerData = globalData.filter( item => item.name === selectName)[0].data
  let dialogContent = document.querySelector('.dialog-content')

  let dialogContentText = ''; // 具体一行行内容
  // 当前点击展示的内容
  randerData.forEach(item => {
      let aStr = ''
      item.tags.forEach( tag => {
        aStr += `<a href="javascript:;">${tag} · </a>`
      })
      dialogContentText += `
                <div class="dialog-content_item" style="margin: 18px 0px;border-bottom: 1px solid rgba(178,186,194,.15);">
                   <div class="meta-row">
                      <a href="${item.objectId}">${item.username}</a> · ${aStr}
                   </div>
                   <div class="title" style="margin-top: 6px;"><a href="${item.originalUrl}" style="font-size: 16px;color: #2E3135;">${item.title}</a></div>
                </div>`
  })

  dialogContent.innerHTML = dialogContentText
}

// 事件总线
function createDOMEventBus (observeData) {
    // 点击切换事件(采用事件冒泡,进行捕获)
    let btns = document.querySelector('.dialog-mian-lang')
    btns.addEventListener('click', function (e) {
      console.log(e)
      if (e.target.nodeName === 'BUTTON') observeData.select = e.target.textContent;
    })

    // 点击关闭按钮
    let dialogClose = document.getElementById('dialog-close')
    let dialogId = document.getElementById('dialogId')
    dialogClose.addEventListener('click', function () {
        console.log('触发')
       dialogId.style.display = 'none'
    })
}


// ============================================================================================
// ========================== 实现简单数据监听,用来简化DOM操作(朱昆鹏)  ===========================
// ============================================================================================

// 数据劫持
function defineReactive (obj, key, value) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,

        get () {
           return value
        },

        set (newValue) {
           value = newValue
           createDialogContent(value) // 更新视图 (生成详细文章列表)
        }
    })
}

































QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址