GVP-Select-Box

### [GVP - Gitee最有价值开源项目](https://gitee.com/gvp/all) 的项目列表按照 star、fork 和编程语言进行筛选排序,增强使用体验

当前为 2024-04-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GVP-Select-Box
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  ### [GVP - Gitee最有价值开源项目](https://gitee.com/gvp/all) 的项目列表按照 star、fork 和编程语言进行筛选排序,增强使用体验
// @author       qdz
// @match        https://gitee.com/gvp/all
// @icon         https://gitee.com/favicon.ico
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    "use strict"

    // DOM树构建完成之后触发
    document.addEventListener('DOMContentLoaded', () => {
        main()
    })

    // 排序类型 star | fork
    let sortType = "star"

    // card所包含的编程语言
    let languages = []

    // 选中的编程语言
    let checkedLanguages = []

    // 给没有编程语言标签的项目取一个标签名
    let undefinedLanguage = '无标签'

    // 定位元素
    let positionElement = null

    // card父容器元素
    let cardContainerElement = null

    // card数量元素
    let cardNumberElement = null

    // card原始数据
    let originalCardArray = null

    // 入口函数
    function main() {
        let result = initPageElement()
        if (result) {
            initLanguage()
            renderBox()
            addSelectListener()
        }
    }

    function initPageElement() {
        positionElement = document.querySelector("#open-source-vip-page .gvp-category-container")
        cardContainerElement = document.querySelector("#open-source-vip-page .categorical-project-cards")
        cardNumberElement = document.querySelector('#open-source-vip-page .breadcrumb .text-muted')
        originalCardArray = Array.from(document.querySelectorAll("#open-source-vip-page .categorical-project-cards > div"))

        return positionElement && cardContainerElement && cardNumberElement && originalCardArray.length > 0
    }

    function initLanguage() {
        const languageMap = new Map()

        originalCardArray.forEach((item) => {
            let language = item.querySelector('.project-labels > div')?.innerText
            // 没有编程语言标签的项目给一个默认值
            let key = language === undefined ? undefinedLanguage : language
            // 不存在则设置值为1,存在则+1
            increment(languageMap, key)
        })

        // 将Map按照值的大小进行降序排序后,再获取排序后的键名数组
        languages = Array.from(languageMap).sort((a, b) => b[1] - a[1]).map(item => item[0])
        checkedLanguages = [...languages]
    }

    function renderBox() {
        // 定位元素设置相对定位,方便子元素进行绝对定位
        positionElement.style.position = "relative"

        // 将selectBox追加到定位元素的最后一个子元素之后
        positionElement.insertAdjacentHTML("beforeend", htmlCode)

        // 渲染编程语言checkbox
        let languageElements = ""
        languages.forEach((item) => {
            languageElements += `<label><input type="checkbox" value="${item}" checked /><span>${item}</span></label>`
        })
        document.getElementById("_languageBox").insertAdjacentHTML("beforeend", languageElements)

        // 往head元素追加css
        const styleElement = document.createElement("style")
        styleElement.appendChild(document.createTextNode(cssCode))
        document.head.appendChild(styleElement)
    }

    function renderCard() {
        // 清空card容器中的数据
        cardContainerElement.innerHTML = ""

        // 根据选中的编程语言进行过滤
        const filteredCards = originalCardArray.filter((item) => {
            const language = item.querySelector(".label")?.innerText
            // 没有编程语言标签的项目给一个默认值
            return checkedLanguages.includes(language === undefined ? undefinedLanguage : language)
        })

        // 根据排序类型,降序排序
        filteredCards.sort((a, b) => {
            const countA = convertToNumber(a.querySelector(`.icon-${sortType} + span`)?.innerText)
            const countB = convertToNumber(b.querySelector(`.icon-${sortType} + span`)?.innerText)
            return countB - countA
        })

        // 重新往card容器中添加数据
        filteredCards.forEach((item) => {
            cardContainerElement.appendChild(item)
        })

        // 重新渲染数量
        cardNumberElement.innerText = '(' + cardContainerElement.children.length + ')'
    }

    function addSelectListener() {
        // 排序类型添加监听事件
        const sortTypeElement = document.getElementById("_sortTypeBox")
        sortTypeElement.addEventListener("change", (event) => {
            if (event.target.type === "radio" && event.target.name === "sortType") {
                sortType = event.target.value

                // 重新渲染card
                renderCard()
            }
        })

        // 编程语言添加监听事件
        const languageElement = document.getElementById("_languageBox")
        languageElement.addEventListener("change", (event) => {
            if (event.target.type === "checkbox") {
                // 清空编程语言列表
                checkedLanguages = []

                // 获取所有选中的编程语言元素
                const checkedElements = languageElement.querySelectorAll('input[type="checkbox"]:checked')
                checkedElements.forEach((item) => {
                    checkedLanguages.push(item.value)
                })

                // 重新渲染cards
                renderCard()
            }
        })

        // 编程语言全选按钮添加监听事件
        const checkAllLanguageElement = document.getElementById("_checkAll")
        checkAllLanguageElement.addEventListener("change", (event) => {
            if (event.target.type === "checkbox" && languages.length > 0) {
                // 全选按钮的选中状态
                let state = event.target.checked

                // 全选则赋值所有编程语言,取消全选则清空
                checkedLanguages = state ? languages : []

                // 遍历所有编程语言元素,更改选中状态
                const languageLabelElements = languageElement.querySelectorAll("#_languageBox > label")
                languageLabelElements.forEach((item) => {
                    item.querySelector("input[type='checkbox']").checked = state
                })
            }

            // 重新渲染cards
            renderCard()
        })
    }

    // Map键名不存在则值赋值为1,存在则值自增1
    function increment(map, key) {
        if (map.has(key)) {
            map.set(key, map.get(key) + 1)
        } else {
            map.set(key, 1)
        }
    }

    // 转换具体的数量,1k = 1000,未包含数量的,默认为0
    function convertToNumber(str = '0') {
        return str.includes('K') ? parseFloat(str.replace('K', '')) * 1000 : parseInt(str)
    }

    const htmlCode = `
    <div id="_selectBox">
        <p>排序类型</p>
        <div id="_sortTypeBox">
            <label>
                <input type="radio" name="sortType" value="star" checked />
                <span>Star</span>
            </label>
            <label>
                <input type="radio" name="sortType" value="fork" />
                <span>Fork</span>
            </label>
        </div>
        <p>编程语言</p>
        <label>
            <input id="_checkAll" type="checkbox" value="checkAll" checked />
            <span>全选</span>
        </label>
        <div id="_languageBox"></div>
    </div>
    `

    const cssCode = `
    #_selectBox {
        position: absolute;
        top: 0px;
        right: -200px;
        width: 200px;
        background-color: #fff;
        padding: 0px 2px 2px 10px;
        border: 1px solid #e3e9ed;
        border-radius: 10px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    #_selectBox p {
        display: inline-block;
        margin: 12px 0;
        font-size: 14px;
        font-weight: bold;
    }

    #_languageBox {
        display: flex;
        flex-wrap: wrap;
    }

    #_languageBox label {
        margin-bottom: 8px;
        margin-right: 8px;
    }

    #_sortTypeBox input[type="radio"],
    #_languageBox input[type="checkbox"],
    #_checkAll[type="checkbox"] {
        display: none;
    }

    #_sortTypeBox input[type="radio"]+span,
    #_languageBox input[type="checkbox"]+span,
    #_checkAll+span {
        padding: 3px 6px;
        background-color: #ddd;
        color: #666;
        border-radius: 10px;
        cursor: pointer;
        font-size: 12px;
    }

    #_sortTypeBox input[type="radio"]:checked+span {
        background-color: #4caf50;
        color: #fff;
    }

    #_languageBox input[type="checkbox"]:checked+span {
        background-color: #4caf50;
        color: #fff;
    }

    #_checkAll:checked+span {
        background-color: #4caf50;
        color: #fff;
    }
    `

})()