// ==UserScript== // @name 知乎历史记录 // @namespace https://maxchang.me // @version 1.0.0 // @author Max Chang // @icon https://static.zhihu.com/heifetz/favicon.ico // @match https://www.zhihu.com/ // @match https://www.zhihu.com/search* // @grant GM_addStyle // @grant GM_getValue // @grant GM_info // @grant GM_registerMenuCommand // @grant GM_setValue // @grant unsafeWindow // @description 给知乎添加历史记录 // @downloadURL none // ==/UserScript== (r=>{if(typeof GM_addStyle=="function"){GM_addStyle(r);return}const o=document.createElement("style");o.textContent=r,document.head.append(o)})(' :root{--primary-color: rgb(5, 109, 232);--primary-light: rgba(5, 109, 232, .5);--primary-bg: rgba(33, 150, 243, .2);--text-color: #333;--text-secondary: #666;--shadow-color: hsla(0, 0%, 7%, .1);--backdrop-color: hsla(0, 0%, 7%, .65);--border-radius-sm: 2px;--border-radius: 4px;--spacing-sm: 4px;--spacing-md: 8px;--spacing-lg: 16px;--spacing-xl: 25px;--font-size-sm: 13px;--font-size-md: 14px}._historyCard_qpr22_19{background:#fff;border-radius:var(--border-radius-sm);box-shadow:0 1px 3px var(--shadow-color);margin-bottom:10px;padding:5px 0}._historyButton_qpr22_27{margin:0 18px;display:flex;justify-content:center;align-items:center;border:1px solid var(--primary-light);background:transparent;color:var(--primary-color);border-radius:var(--border-radius);height:40px;font-size:var(--font-size-md);cursor:pointer;width:calc(100% - 36px)}._dialog_qpr22_42{padding:0;border:0;border-radius:var(--border-radius);box-shadow:0 4px 12px var(--shadow-color);background-color:#fff;max-width:800px;width:80%}._dialog_qpr22_42::backdrop{background-color:var(--backdrop-color)}._dialogContent_qpr22_56{padding:var(--spacing-lg) var(--spacing-xl);outline:none}._dialogHeader_qpr22_61{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--spacing-sm);border-bottom:1px solid #eee;padding-bottom:var(--spacing-md)}._dialogTitle_qpr22_70{margin:0;font-size:18px;color:var(--text-color)}._closeButton_qpr22_76{background:none;border:none;cursor:pointer;font-size:16px;color:var(--text-secondary);padding:var(--spacing-sm);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s}._closeButton_qpr22_76:hover{background-color:#f0f0f0}._dialogBody_qpr22_94{max-height:70vh;overflow-y:auto}._historyList_qpr22_99{list-style:none;margin:0;display:flex;flex-direction:column-reverse;padding:0 1.5em}._historyItem_qpr22_108{padding:var(--spacing-md) 0;border-bottom:1px solid #f0f0f0;display:flex;align-items:baseline;gap:var(--spacing-md)}._historyItem_qpr22_108:last-child{border-bottom:none}._link_qpr22_120{text-decoration:none;color:var(--text-color);font-weight:500;transition:color .2s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;display:inline-block}._link_qpr22_120:hover{color:var(--primary-color)}._authorInfo_qpr22_136{color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;flex-shrink:0}._answer_qpr22_143:before{content:"\u95EE\u9898";color:#2196f3;background-color:var(--primary-bg);font-weight:700;font-size:var(--font-size-sm);padding:1px var(--spacing-sm) 0;border-radius:var(--border-radius-sm);margin-right:var(--spacing-sm);display:inline-block}._article_qpr22_155:before{content:"\u6587\u7AE0";color:#004b87;background-color:var(--primary-bg);font-weight:700;font-size:var(--font-size-sm);padding:1px var(--spacing-sm) 0;border-radius:var(--border-radius-sm);margin-right:var(--spacing-sm);display:inline-block}._emptyState_qpr22_167{text-align:center;padding:var(--spacing-xl);color:var(--text-secondary);font-style:italic} '); (function (require$$1, ReactDOM) { 'use strict'; var jsxRuntime = { exports: {} }; var reactJsxRuntime_production_min = {}; /* object-assign (c) Sindre Sorhus @license MIT */ var objectAssign; var hasRequiredObjectAssign; function requireObjectAssign() { if (hasRequiredObjectAssign) return objectAssign; hasRequiredObjectAssign = 1; var getOwnPropertySymbols = Object.getOwnPropertySymbols; var hasOwnProperty = Object.prototype.hasOwnProperty; var propIsEnumerable = Object.prototype.propertyIsEnumerable; function toObject(val) { if (val === null || val === void 0) { throw new TypeError("Object.assign cannot be called with null or undefined"); } return Object(val); } function shouldUseNative() { try { if (!Object.assign) { return false; } var test1 = new String("abc"); test1[5] = "de"; if (Object.getOwnPropertyNames(test1)[0] === "5") { return false; } var test2 = {}; for (var i = 0; i < 10; i++) { test2["_" + String.fromCharCode(i)] = i; } var order2 = Object.getOwnPropertyNames(test2).map(function(n) { return test2[n]; }); if (order2.join("") !== "0123456789") { return false; } var test3 = {}; "abcdefghijklmnopqrst".split("").forEach(function(letter) { test3[letter] = letter; }); if (Object.keys(Object.assign({}, test3)).join("") !== "abcdefghijklmnopqrst") { return false; } return true; } catch (err) { return false; } } objectAssign = shouldUseNative() ? Object.assign : function(target, source) { var from; var to = toObject(target); var symbols; for (var s = 1; s < arguments.length; s++) { from = Object(arguments[s]); for (var key in from) { if (hasOwnProperty.call(from, key)) { to[key] = from[key]; } } if (getOwnPropertySymbols) { symbols = getOwnPropertySymbols(from); for (var i = 0; i < symbols.length; i++) { if (propIsEnumerable.call(from, symbols[i])) { to[symbols[i]] = from[symbols[i]]; } } } } return to; }; return objectAssign; } /** @license React v17.0.2 * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var hasRequiredReactJsxRuntime_production_min; function requireReactJsxRuntime_production_min() { if (hasRequiredReactJsxRuntime_production_min) return reactJsxRuntime_production_min; hasRequiredReactJsxRuntime_production_min = 1; requireObjectAssign(); var f = require$$1, g = 60103; reactJsxRuntime_production_min.Fragment = 60107; if ("function" === typeof Symbol && Symbol.for) { var h = Symbol.for; g = h("react.element"); reactJsxRuntime_production_min.Fragment = h("react.fragment"); } var m = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, n = Object.prototype.hasOwnProperty, p = { key: true, ref: true, __self: true, __source: true }; function q(c, a, k) { var b, d = {}, e = null, l = null; void 0 !== k && (e = "" + k); void 0 !== a.key && (e = "" + a.key); void 0 !== a.ref && (l = a.ref); for (b in a) n.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]); if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]); return { $$typeof: g, type: c, key: e, ref: l, props: d, _owner: m.current }; } reactJsxRuntime_production_min.jsx = q; reactJsxRuntime_production_min.jsxs = q; return reactJsxRuntime_production_min; } var hasRequiredJsxRuntime; function requireJsxRuntime() { if (hasRequiredJsxRuntime) return jsxRuntime.exports; hasRequiredJsxRuntime = 1; { jsxRuntime.exports = requireReactJsxRuntime_production_min(); } return jsxRuntime.exports; } var jsxRuntimeExports = requireJsxRuntime(); var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)(); var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)(); var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); const historyCard = "_historyCard_qpr22_19"; const historyButton = "_historyButton_qpr22_27"; const dialog = "_dialog_qpr22_42"; const dialogContent = "_dialogContent_qpr22_56"; const dialogHeader = "_dialogHeader_qpr22_61"; const dialogTitle = "_dialogTitle_qpr22_70"; const closeButton = "_closeButton_qpr22_76"; const dialogBody = "_dialogBody_qpr22_94"; const historyList = "_historyList_qpr22_99"; const historyItem = "_historyItem_qpr22_108"; const link = "_link_qpr22_120"; const authorInfo = "_authorInfo_qpr22_136"; const answer = "_answer_qpr22_143"; const article = "_article_qpr22_155"; const emptyState = "_emptyState_qpr22_167"; const styles = { historyCard, historyButton, dialog, dialogContent, dialogHeader, dialogTitle, closeButton, dialogBody, historyList, historyItem, link, authorInfo, answer, article, emptyState }; const log = (logMethod, tag, ...args) => { const colors = { log: "#2c3e50", error: "#ff4500", warn: "#f39c12" }; const fontFamily = "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;"; console[logMethod]( `%c ${_GM_info.script.name} %c ${tag} `, `padding: 2px 6px; border-radius: 3px 0 0 3px; color: #fff; background: #056de8; font-weight: bold; ${fontFamily}`, `padding: 2px 6px; border-radius: 0 3px 3px 0; color: #fff; background: ${colors[logMethod]}; font-weight: bold; ${fontFamily}`, ...args ); }; const logger = { log: (...args) => log("log", "日志", ...args), error: (...args) => log("error", "错误", ...args), warn: (...args) => log("warn", "警告", ...args) }; const STORAGE_KEY = "ZH_HISTORY"; const HISTORY_LIMIT_KEY = "HISTORY_LIMIT"; const DEFAULT_HISTORY_LIMIT = 20; const HISTORY_LIMIT = _GM_getValue(HISTORY_LIMIT_KEY) || DEFAULT_HISTORY_LIMIT; const setHistoryLimit = (limit) => { const numericLimit = Number(limit); if (!Number.isNaN(numericLimit) && numericLimit > 0) { _GM_setValue(HISTORY_LIMIT_KEY, numericLimit); return [true, null]; } return [false, "输入无效,请输入一个正整数"]; }; const saveHistory = (item) => { try { const raw = _GM_getValue(STORAGE_KEY); const historyItems = raw ? JSON.parse(raw) : []; const existingIndex = historyItems.findIndex((i) => i.itemId === item.itemId); if (existingIndex !== -1) { historyItems.splice(existingIndex, 1); } historyItems.push(item); if (historyItems.length > HISTORY_LIMIT) { historyItems.splice(0, historyItems.length - HISTORY_LIMIT); } _GM_setValue(STORAGE_KEY, JSON.stringify(historyItems)); } catch (error) { logger.error("保存浏览历史失败:", error); } }; const migrateToGMStorage = () => { try { logger.log("检测到旧的浏览历史数据,正在转换..."); const raw = localStorage.getItem(STORAGE_KEY); if (raw) { _GM_setValue(STORAGE_KEY, raw); localStorage.removeItem(STORAGE_KEY); } logger.log("转换浏览历史数据成功"); } catch (error) { logger.error("转换浏览历史失败:", error); } }; const getHistory = () => { try { if (localStorage.getItem(STORAGE_KEY) !== null) { migrateToGMStorage(); } const raw = _GM_getValue(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch (error) { logger.error("获取浏览历史失败:", error); return []; } }; const clearHistory = () => { try { _GM_setValue(STORAGE_KEY, null); } catch (error) { logger.error("清空浏览历史失败:", error); } }; const HistoryItem = ({ item }) => { const itemTypeClass = item.type === "answer" ? styles.answer : styles.article; return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: styles.historyItem, children: [ /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: item.url, className: `${styles.link} ${itemTypeClass}`, children: item.title }), /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: styles.authorInfo, children: item.authorName }) ] }); }; const HistoryDialog = ({ isOpen, onClose }) => { const historyItems = getHistory(); const dialogRef = require$$1.useRef(null); require$$1.useEffect(() => { const dialogElement = dialogRef.current; if (!dialogElement) return; if (isOpen) { dialogElement.showModal(); document.body.style.overflow = "hidden"; } else if (dialogElement.open) { dialogElement.close(); document.body.style.overflow = ""; } }, [isOpen]); const handleClose = () => { onClose(); }; return /* @__PURE__ */ jsxRuntimeExports.jsx( "dialog", { ref: dialogRef, className: styles.dialog, onClose: handleClose, onClick: (e) => { if (e.target === dialogRef.current) { handleClose(); } }, onKeyDown: (e) => { if (e.key === "Escape") { handleClose(); } }, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.dialogContent, children: [ /* @__PURE__ */ jsxRuntimeExports.jsxs("header", { className: styles.dialogHeader, children: [ /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: styles.dialogTitle, children: "浏览历史" }), /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: styles.closeButton, "aria-label": "关闭", onClick: handleClose, children: "✕" }) ] }), /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.dialogBody, children: historyItems.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: styles.historyList, children: historyItems.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryItem, { item }, item.itemId)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.emptyState, children: "暂无浏览历史" }) }) ] }) } ); }; const HistoryCard = () => { const [isDialogOpen, setIsDialogOpen] = require$$1.useState(false); const handleOpenDialog = () => setIsDialogOpen(true); const handleCloseDialog = () => setIsDialogOpen(false); return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.historyCard, children: [ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: styles.historyButton, onClick: handleOpenDialog, "aria-label": "查看历史记录", type: "button", children: "历史记录" }), /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryDialog, { isOpen: isDialogOpen, onClose: handleCloseDialog }) ] }); }; const useHistoryTracker = () => { require$$1.useEffect(() => { const bindEvent = (el) => { el.addEventListener("click", onClick, { once: true }); }; const onClick = (e) => { const target = e.target; const item = target.closest(".ContentItem"); if (!item) return; const zop = item.dataset.zop; if (!zop) { logger.error("无法读取回答或文章信息"); return; } try { const data = JSON.parse(zop); const link2 = item.querySelector(".ContentItem-title a"); if (link2) data.url = link2.href; saveHistory(data); } catch (err) { logger.error("解析历史记录失败:", err); } }; document.querySelectorAll(".ContentItem").forEach(bindEvent); const container = document.querySelector(".Topstory-recommend"); if (!container) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((node) => { var _a; if (!(node instanceof HTMLElement)) return; const item = (_a = node.querySelector) == null ? void 0 : _a.call(node, ".ContentItem"); if (item) bindEvent(item); }); } }); observer.observe(container, { childList: true, subtree: true }); return () => observer.disconnect(); }, []); }; const App = () => { useHistoryTracker(); return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryCard, {}) }); }; const clearHistoryCommand = [ "🗑 清空浏览历史记录", () => { clearHistory(); alert("清空浏览历史成功"); } ]; const setHistoryLimitCommand = [ `🔢 设置记录数量限制(当前:${HISTORY_LIMIT})`, () => { const input = prompt(`请输入新的历史记录最大数量(默认 ${DEFAULT_HISTORY_LIMIT})`); if (!input) return; const [isOK, message] = setHistoryLimit(input); if (isOK) { alert("设置成功"); } else { alert(message); } } ]; const registerMenuCommands = () => { Reflect.apply(_GM_registerMenuCommand, null, clearHistoryCommand); Reflect.apply(_GM_registerMenuCommand, null, setHistoryLimitCommand); }; console.log( "%c知乎历史记录", "color:#1772F6; font-weight:bold; font-size:3em; padding:5px; text-shadow:1px 1px 3px rgba(0,0,0,0.7)" ); const mountApp = () => { const container = document.createElement("div"); container.id = "zh-history-root"; const target = document.querySelector(".Topstory-container > div:nth-child(2) > div:nth-child(2)"); if (!target) { logger.warn("未找到挂载点"); return; } target.appendChild(container); ReactDOM.render(/* @__PURE__ */ jsxRuntimeExports.jsx(App, {}), container); }; mountApp(); registerMenuCommands(); logger.log(`初始化成功,版本:${_GM_info.script.version}`); })(unsafeWindow.React, unsafeWindow.ReactDOM);