Greasy Fork

Greasy Fork is available in English.

知乎历史记录

给知乎添加历史记录

当前为 2025-05-10 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name       知乎历史记录
// @namespace  https://maxchang.me
// @version    1.0.3
// @author     Max Chang
// @license    MIT
// @icon       https://static.zhihu.com/heifetz/favicon.ico
// @match      https://www.zhihu.com/
// @match      https://www.zhihu.com/follow*
// @grant      GM_addStyle
// @grant      GM_getValue
// @grant      GM_info
// @grant      GM_registerMenuCommand
// @grant      GM_setValue
// @grant      unsafeWindow
// @description 给知乎添加历史记录
// ==/UserScript==

(o=>{if(typeof GM_addStyle=="function"){GM_addStyle(o);return}const r=document.createElement("style");r.textContent=o,document.head.append(r)})(' :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}._srOnly_vo2sz_19{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}._historyCard_vo2sz_31{background:#fff;border-radius:var(--border-radius-sm);box-shadow:0 1px 3px var(--shadow-color);margin-bottom:10px;padding:5px 0}._historyButton_vo2sz_39{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_vo2sz_55,._dialog_vo2sz_55::backdrop{transition:display .25s allow-discrete,overlay .25s allow-discrete,opacity .25s;opacity:0}._dialog_vo2sz_55[open],._dialog_vo2sz_55[open]::backdrop{opacity:1;scale:1}@starting-style{._dialog_vo2sz_55[open],._dialog_vo2sz_55[open]::backdrop{opacity:0}}._dialog_vo2sz_55{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_vo2sz_55::backdrop{background-color:var(--backdrop-color)}._dialogContent_vo2sz_88{padding:var(--spacing-lg) var(--spacing-xl);outline:none}._dialogHeader_vo2sz_93{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_vo2sz_102{margin:0;font-size:18px;color:var(--text-color)}._closeButton_vo2sz_108{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_vo2sz_108:hover,._closeButton_vo2sz_108:focus{background-color:#f0f0f0}._dialogBody_vo2sz_127{max-height:70vh;overflow-y:auto}._historyList_vo2sz_132{list-style:none;margin:0;display:flex;flex-direction:column;padding:0 1.5em}._historyItem_vo2sz_140{padding:var(--spacing-md) 0;border-bottom:1px solid #f0f0f0;display:flex;align-items:baseline;justify-content:space-between;gap:var(--spacing-md)}._historyItem_vo2sz_140:last-child{border-bottom:none}._link_vo2sz_153{text-decoration:none;color:var(--text-color);font-weight:500;transition:color .2s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block}._link_vo2sz_153:hover,._link_vo2sz_153:focus{color:var(--primary-color)}._authorInfo_vo2sz_169{color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;flex-shrink:0}._answer_vo2sz_176:before,._article_vo2sz_177:before,._pin_vo2sz_178:before{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}._answer_vo2sz_176:before{content:"\u95EE\u9898";color:#2196f3}._article_vo2sz_177:before{content:"\u6587\u7AE0";color:#004b87}._pin_vo2sz_178:before{content:"\u60F3\u6CD5";color:#60a912}._emptyState_vo2sz_203{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 srOnly = "_srOnly_vo2sz_19";
  const historyCard = "_historyCard_vo2sz_31";
  const historyButton = "_historyButton_vo2sz_39";
  const dialog = "_dialog_vo2sz_55";
  const dialogContent = "_dialogContent_vo2sz_88";
  const dialogHeader = "_dialogHeader_vo2sz_93";
  const dialogTitle = "_dialogTitle_vo2sz_102";
  const closeButton = "_closeButton_vo2sz_108";
  const dialogBody = "_dialogBody_vo2sz_127";
  const historyList = "_historyList_vo2sz_132";
  const historyItem = "_historyItem_vo2sz_140";
  const link = "_link_vo2sz_153";
  const authorInfo = "_authorInfo_vo2sz_169";
  const answer = "_answer_vo2sz_176";
  const article = "_article_vo2sz_177";
  const pin = "_pin_vo2sz_178";
  const emptyState = "_emptyState_vo2sz_203";
  const styles = {
    srOnly,
    historyCard,
    historyButton,
    dialog,
    dialogContent,
    dialogHeader,
    dialogTitle,
    closeButton,
    dialogBody,
    historyList,
    historyItem,
    link,
    authorInfo,
    answer,
    article,
    pin,
    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).reverse() : [];
    } catch (error) {
      logger.error("获取浏览历史失败:", error);
      return [];
    }
  };
  const clearHistory = () => {
    try {
      _GM_setValue(STORAGE_KEY, null);
    } catch (error) {
      logger.error("清空浏览历史失败:", error);
    }
  };
  const saveHistoryFromElement = (item) => {
    var _a, _b;
    const zop = item.dataset.zop;
    if (!zop) {
      logger.error("无法读取回答或文章信息", item.dataset);
      return;
    }
    try {
      const data = JSON.parse(zop);
      if (data.type === "pin") {
        const userLink = (_a = item.closest(".Feed")) == null ? void 0 : _a.querySelector(".UserLink-link");
        if (userLink) data.authorName = userLink.innerText;
        data.url = `https://www.zhihu.com/pin/${data.itemId}`;
        const contentText = (_b = item.querySelector(`.RichText`)) == null ? void 0 : _b.innerText;
        if (contentText) data.title = contentText;
      } else {
        const link2 = item.querySelector(".ContentItem-title a");
        if (link2) data.url = link2.href;
      }
      saveHistory(data);
    } catch (err) {
      logger.error("解析历史记录失败:", err);
    }
  };
  const trackHistory = () => {
    const container = document.querySelector("#TopstoryContent");
    if (!container) {
      logger.error("未找到首页推荐容器");
      return;
    }
    container.addEventListener("click", (e) => {
      const target = e.target;
      if (!(target instanceof HTMLElement)) return;
      const item = target.closest(".ContentItem");
      if (item) saveHistoryFromElement(item);
    });
  };
  const HistoryItem = require$$1.forwardRef(({ item }, ref) => {
    const itemTypeClass = styles[item.type];
    const chineseType = {
      answer: "问题",
      article: "文章",
      pin: "想法"
    };
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: styles.historyItem, children: [
      /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: item.url, className: `${styles.link} ${itemTypeClass}`, ref, children: [
        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: styles.srOnly, children: chineseType[item.type] }),
        item.title,
        /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: styles.srOnly, children: [
          "作者:",
          item.authorName
        ] })
      ] }),
      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: styles.authorInfo, "aria-hidden": true, tabIndex: -1, children: item.authorName })
    ] });
  });
  const HistoryDialog = ({ isOpen, onClose }) => {
    const historyItems = getHistory();
    const dialogRef = require$$1.useRef(null);
    const firstItemRef = require$$1.useRef(null);
    require$$1.useEffect(() => {
      var _a;
      const dialogElement = dialogRef.current;
      if (!dialogElement) return;
      if (isOpen) {
        dialogElement.showModal();
        document.body.style.overflow = "hidden";
        (_a = firstItemRef.current) == null ? void 0 : _a.focus();
      } 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, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryItem, { item, ref: i === 0 ? firstItemRef : null }, item.itemId)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.emptyState, children: "暂无浏览历史" }) })
        ] })
      }
    );
  };
  const HistoryCard = () => {
    const [isDialogOpen, setIsDialogOpen] = require$$1.useState(false);
    require$$1.useEffect(() => {
      const handleKeyDown = (event) => {
        const target = event.target;
        const isEditableTarget = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.tagName === "SELECT";
        if (event.key === "h" && !isEditableTarget) {
          setIsDialogOpen((prev) => !prev);
        }
      };
      window.addEventListener("keydown", handleKeyDown);
      return () => {
        window.removeEventListener("keydown", handleKeyDown);
      };
    }, []);
    return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.historyCard, role: "complementary", children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx(
        "button",
        {
          className: styles.historyButton,
          onClick: () => setIsDialogOpen(true),
          "aria-label": "历史记录,打开后按 Esc 关闭",
          "aria-haspopup": "dialog",
          type: "button",
          children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "历史记录" })
        }
      ),
      /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryDialog, { isOpen: isDialogOpen, onClose: () => setIsDialogOpen(false) })
    ] });
  };
  const App = () => {
    trackHistory();
    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);