知乎历史记录

给知乎添加爱历史记录

目前为 2023-02-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         知乎历史记录
// @namespace    http://zhangmaimai.com
// @version      0.1
// @description  给知乎添加爱历史记录
// @author       MaxChang3
// @match        https://www.zhihu.com/
// @icon         https://static.zhihu.com/heifetz/favicon.ico
// @grant        GM_addStyle
// @run-at       document-end
// @license      WTFPL
// ==/UserScript==

(function () {
    'use strict'
    const HISTORYS_LIMIT = 20
    const HISTORYS_CACHE = { VALUE: '', CNT: 0, LAST_CNT: -1 }
    /**
     * 给元素绑定添加历史记录的事件
     * @param {Event} e Event
     * @returns
     */
    const bindHistoryEvent = (e) => {
        /** @type {HTMLElement | undefined} */
        const ansterItem = e.target.closest('.ContentItem')
        if (!ansterItem) return
        const zop = ansterItem.dataset.zop
        if (!zop) console.error('无法读取回答或文章信息')
        /**
         * @typedef {{
         *   authorName:string,
         *   itemId:number,
         *   title:string,
         *   type: 'answter' | 'article',
         *   url?: string
         * }} ZHContentData
         */
        /** @type {ZHContentData} */
        const contentData = JSON.parse(zop)
        /** @type {string} */
        const url = ansterItem.querySelector('.ContentItem-title a').href
        contentData.url = url
        const historysData = window.localStorage.getItem('ZH_HISTORY')
        /** @type {ZHContentData[]} */
        const histroys = historysData ?
            JSON.parse(historysData)
                .filter(histroy => histroy.itemId != contentData.itemId)
                .concat(contentData) :
            [contentData]
        if (histroys.length > HISTORYS_LIMIT) histroys.shift()
        window.localStorage.setItem('ZH_HISTORY', JSON.stringify(histroys))
        HISTORYS_CACHE.CNT++
    }
    // 从 localStorage 中取回历史记录
    /** @returns {ZHContentData[]} */
    const getHistoryList = () => JSON.parse(window.localStorage.getItem('ZH_HISTORY'))
    const getHistoryListElements = () => {
        // 做了个简单的缓存机制,如果距离上次点开前进行了若干次点击动作,则重新取回数据
        // 否则就从缓存中拿回来
        if (HISTORYS_CACHE.LAST_CNT !== HISTORYS_CACHE.CNT) {
            const ret = getHistoryList().map(({ title, url, authorName, type }) =>
                `<li>
        <a class="${{
                    'answer': 'zhh-type-answer',
                    'article': 'zhh-type-article'
                }[type]}" href="${url}">${title}</a> - ${authorName}
    </li>`)
                .reverse()
                .join('\n')
            HISTORYS_CACHE.VALUE = ret
            HISTORYS_CACHE.LAST_CNT = HISTORYS_CACHE.CNT
            return ret
        }
        return HISTORYS_CACHE.VALUE
    }
    // 用 Web Component 包装一下,这样渲染是直接出来整个按钮,不过似乎有些麻烦了/
    class ZHHistoryCard extends HTMLElement {
        constructor() {
            super()
            const shadow = this.attachShadow({ mode: 'open' })
            const style = document.createElement('style')
            style.textContent = `
            .Card {
                background: #fff;
                border-radius: 2px;
                -webkit-box-shadow: 0 1px 3px hsl(0deg 0% 7% / 10%);
                box-shadow: 0 1px 3px hsl(0deg 0% 7% / 10%);
                -webkit-box-sizing: border-box;
                box-sizing: border-box;
                margin-bottom: 10px;
                overflow: hidden;
                padding: 5px 0;
            }
            .zhh-button {
                box-sizing: border-box;
                margin: 0px 18px;
                min-width: 0px;
                -webkit-box-pack: center;
                justify-content: center;
                -webkit-box-align: center;
                align-items: center;
                display: flex;
                border: 1px solid rgba(5, 109, 232, 0.5);
                color: rgb(5, 109, 232);
                border-radius: 4px;
                cursor: pointer;
                height: 40px;
                font-size: 14px;
            }
            `
            const wrapper = document.createElement('div')
            wrapper.setAttribute('class', 'Card')
            const button = document.createElement('div')
            button.setAttribute('class', 'zhh-button')
            button.innerText = '查看历史记录'
            wrapper.append(button)
            shadow.appendChild(wrapper)
            shadow.appendChild(style)
        }
    }
    customElements.define('zh-history-card', ZHHistoryCard,)
    // 给现存的卡片添加点击事件
    document.querySelectorAll('.ContentItem')
        .forEach(el => el.addEventListener('click', bindHistoryEvent))
    // 插入历史记录卡片
    document.querySelector('.Topstory-container').children[1].children[1]
        .insertAdjacentHTML('afterbegin', `<zh-history-card id="zhh-card"></zh-history-card>`)
    // 插件 dialog,用这个做弹出层还挺方便的
    document.body.insertAdjacentHTML('beforeend', `
<dialog>
    <div class="Modal Modal--default ChatBoxModal" tabindex="0">
        <div class="Modal-inner">
            <div class="Modal-content Modal-content--spread ChatBoxModal-content">
                <ul style="padding: 20px;" id="zhh-list">
                </ul>
            </div>
        </div>
    </div>
</dialog>
`)
    // 添加样式
    GM_addStyle(`
    dialog {
        padding: 0; 
        border: 0;
    }
    dialog::backdrop {
        background-color: hsla(0,0%,7%,.65);
    }
    #zhh-list li {
        padding-bottom: 5px;
    }
    .zhh-type-answer::before {
        content: '问题';
		color: #2196F3;
		background-color: #2196F333;
		font-weight: bold;
		font-size: 13px;
		padding: 1px 4px 0;
		border-radius: 2px;
		display: inline-block;
		vertical-align: 1.5px;
		margin: 0 4px 0 0;
    }
    .zhh-type-article::before  {
        content: '文章';
        color: #004b87;
        background-color: #2196F333;
        font-weight: bold;
        font-size: 13px;
        padding: 1px 4px 0;
        border-radius: 2px;
        display: inline-block;
        vertical-align: 1.5px;
        margin: 0 4px 0 0;
    }
    `)
    // 点击弹出层周围直接关闭
    const dialog = document.querySelector('dialog')
    dialog.addEventListener('click', (e => {
        if (!e.target.closest('div')) {
            e.target.close()
        }
    }))
    // 给历史记录卡片绑定一个点击事件 实时插入历史记录列表
    document.querySelector('#zhh-card')
        .shadowRoot
        .querySelector('.zhh-button').addEventListener('click', () => {
            dialog.querySelector('#zhh-list').innerHTML = getHistoryListElements()
            dialog.showModal()
        })
    // 监听元素更新,给新添加到内容绑定事件
    const targetNode = document.querySelector('.Topstory-recommend')
    const config = { childList: true, subtree: true }
    /** @type {MutationCallback} */
    const callback = (mutationsList, observer) => {
        for (let mutation of mutationsList) {
            if (mutation.type !== 'childList') continue
            mutation.addedNodes.forEach(node => {
                /** @type {HTMLElement | undefined} */
                const contentItem = node.querySelector('.ContentItem')
                if (contentItem) contentItem.addEventListener('click', bindHistoryEvent)
            })
        }
    }
    const observer = new MutationObserver(callback)
    observer.observe(targetNode, config)
})()

QingJ © 2025

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