// ==UserScript== // @name 知乎历史记录 // @namespace http://zhangmaimai.com // @version 0.2 // @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 // @downloadURL none // ==/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 }) => `
  • ${title} - ${authorName}
  • `) .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', ``) // 插件 dialog,用这个做弹出层还挺方便的 document.body.insertAdjacentHTML('beforeend', ` `) // 添加样式 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) })()