// ==UserScript==
// @name 知乎历史记录
// @namespace https://maxchang.me
// @version 1.1.0
// @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_1fok4_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_1fok4_31{background:#fff;border-radius:var(--border-radius-sm);box-shadow:0 1px 3px var(--shadow-color);margin-bottom:10px;padding:5px 0}._historyButton_1fok4_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_1fok4_55,._dialog_1fok4_55::backdrop{transition:display .25s allow-discrete,overlay .25s allow-discrete,opacity .25s;opacity:0}._dialog_1fok4_55[open],._dialog_1fok4_55[open]::backdrop{opacity:1;scale:1}@starting-style{._dialog_1fok4_55[open],._dialog_1fok4_55[open]::backdrop{opacity:0}}._dialog_1fok4_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%;overflow:hidden;-webkit-user-select:text!important;user-select:text!important}._dialog_1fok4_55::backdrop{background-color:var(--backdrop-color)}._dialogContent_1fok4_90{padding:var(--spacing-lg) var(--spacing-xl);outline:none}._dialogHeader_1fok4_95{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_1fok4_104{margin:0;font-size:18px;color:var(--text-color)}._searchContainer_1fok4_110{position:relative;flex:1;margin:0 var(--spacing-lg);display:flex;align-items:center}._searchInput_1fok4_118{flex:1;width:100%;padding:var(--spacing-sm) var(--spacing-md);padding-right:calc(var(--spacing-md) * 3);border:1px solid #eee;border-radius:var(--border-radius);font-size:var(--font-size-md);color:var(--text-color);background-color:#f9f9f9;transition:border-color .2s}._searchInput_1fok4_118:focus{outline:none;border-color:var(--primary-light)}._clearSearchButton_1fok4_136{position:absolute;right:var(--spacing-sm);background:none;border:none;cursor:pointer;color:var(--text-secondary);font-size:12px;padding:var(--spacing-sm);display:flex;align-items:center;justify-content:center;border-radius:50%;transition:background-color .2s}._clearSearchButton_1fok4_136:hover{background-color:#eee}._dialogBody_1fok4_156{max-height:70vh;overflow-y:auto}._searchInfo_1fok4_161{padding:var(--spacing-sm) var(--spacing-lg);font-size:var(--font-size-sm);color:var(--text-secondary);background-color:#f5f5f5;margin-bottom:var(--spacing-md);border-radius:var(--border-radius-sm)}._historyList_1fok4_170{list-style:none;margin:0;display:flex;flex-direction:column;padding:0 1.5em}._emptyState_1fok4_178{text-align:center;padding:var(--spacing-xl);color:var(--text-secondary);font-style:italic}._item_5lnzt_1{padding:var(--spacing-md) 0;border-bottom:1px solid #f0f0f0;display:flex;flex-direction:column}._item_5lnzt_1:last-child{border-bottom:none}._link_5lnzt_12{flex:1;color:var(--text-color);text-decoration:none;min-width:0}._link_5lnzt_12:hover,._link_5lnzt_12:focus{color:var(--primary-color)}._header_5lnzt_24{display:flex}._visitTime_5lnzt_28{color:var(--text-secondary);font-size:var(--font-size-sm);white-space:nowrap;flex-shrink:0}._title_5lnzt_35{flex:1;font-weight:500;transition:color .2s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}._content_5lnzt_45{color:var(--text-secondary);font-size:var(--font-size-sm);margin:var(--spacing-sm) 0 0 0;overflow:hidden;text-overflow:ellipsis;word-break:break-word;display:-webkit-box;line-clamp:2;-webkit-box-orient:vertical;-webkit-line-clamp:2}._answer_5lnzt_59:before,._article_5lnzt_60:before,._pin_5lnzt_61: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_5lnzt_59:before{content:"\u95EE\u9898";color:#2196f3}._article_5lnzt_60:before{content:"\u6587\u7AE0";color:#004b87}._pin_5lnzt_61:before{content:"\u60F3\u6CD5";color:#60a912}._highlight_5lnzt_86{background-color:#ffe60066;border-radius:2px;padding:0 2px;margin:0 -2px;font-weight:500} ');
(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_1fok4_19";
const historyCard = "_historyCard_1fok4_31";
const historyButton = "_historyButton_1fok4_39";
const dialog = "_dialog_1fok4_55";
const dialogContent = "_dialogContent_1fok4_90";
const dialogHeader = "_dialogHeader_1fok4_95";
const dialogTitle = "_dialogTitle_1fok4_104";
const searchContainer = "_searchContainer_1fok4_110";
const searchInput = "_searchInput_1fok4_118";
const clearSearchButton = "_clearSearchButton_1fok4_136";
const dialogBody = "_dialogBody_1fok4_156";
const searchInfo = "_searchInfo_1fok4_161";
const historyList = "_historyList_1fok4_170";
const emptyState = "_emptyState_1fok4_178";
const styles = {
srOnly,
historyCard,
historyButton,
dialog,
dialogContent,
dialogHeader,
dialogTitle,
searchContainer,
searchInput,
clearSearchButton,
dialogBody,
searchInfo,
historyList,
emptyState
};
const item = "_item_5lnzt_1";
const link = "_link_5lnzt_12";
const header = "_header_5lnzt_24";
const visitTime = "_visitTime_5lnzt_28";
const title = "_title_5lnzt_35";
const content = "_content_5lnzt_45";
const answer = "_answer_5lnzt_59";
const article = "_article_5lnzt_60";
const pin = "_pin_5lnzt_61";
const highlight = "_highlight_5lnzt_86";
const Item = {
item,
link,
header,
visitTime,
title,
content,
answer,
article,
pin,
highlight
};
const formatTime = (date) => {
const now = /* @__PURE__ */ new Date();
const diff = Math.floor((now.getTime() - date.getTime()) / 1e3);
if (diff < 60) return "刚刚";
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
return date.toLocaleDateString("zh-CN");
};
const highlightTextWithPositions = (text, fieldPositions) => {
if (!fieldPositions || fieldPositions.length === 0) return text;
const highlightMarkers = new Array(text.length).fill(false);
for (const { start, end } of fieldPositions) {
const endIndex = Math.min(end, text.length);
for (let i = start; i < endIndex; i++) {
highlightMarkers[i] = true;
}
}
const segments = [];
let currentSegment = null;
for (let i = 0; i < text.length; i++) {
const shouldHighlight = highlightMarkers[i];
if (!currentSegment || currentSegment.highlight !== shouldHighlight) {
if (currentSegment) segments.push(currentSegment);
currentSegment = {
text: text[i],
highlight: shouldHighlight
};
continue;
}
currentSegment.text += text[i];
}
if (currentSegment) segments.push(currentSegment);
return segments.map(
(segment, index) => segment.highlight ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: Item.highlight, children: segment.text }, index) : /* @__PURE__ */ jsxRuntimeExports.jsx(require$$1.Fragment, { children: segment.text }, index)
);
};
const HistoryItem = require$$1.forwardRef(({ item: item2, searchResult }, ref) => {
const contentTypeMap = {
answer: "问题",
article: "文章",
pin: "想法"
};
const visitTime2 = !item2.visitTime ? null : new Date(item2.visitTime);
const formattedVisitTime = !visitTime2 ? null : {
short: formatTime(visitTime2),
full: visitTime2.toLocaleString("zh-CN")
};
const highlightedTitle = require$$1.useMemo(
() => {
var _a;
return highlightTextWithPositions(item2.title, (_a = searchResult == null ? void 0 : searchResult.matches) == null ? void 0 : _a.title);
},
[item2.title, searchResult]
);
const highlightedContent = require$$1.useMemo(() => {
var _a;
if (!item2.content) return null;
return highlightTextWithPositions(item2.content, (_a = searchResult == null ? void 0 : searchResult.matches) == null ? void 0 : _a.content);
}, [item2.content, searchResult]);
const highlightedAuthorName = require$$1.useMemo(() => {
var _a;
if (formattedVisitTime || !searchResult) return null;
return highlightTextWithPositions(item2.authorName, (_a = searchResult.matches) == null ? void 0 : _a.authorName);
}, [item2.authorName, formattedVisitTime, searchResult]);
return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: Item.item, children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: item2.url, className: Item.link, ref, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: styles.srOnly, children: contentTypeMap[item2.type] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: Item.header, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `${Item.title} ${Item[item2.type]}`, children: highlightedTitle }),
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: Item.visitTime, title: formattedVisitTime == null ? void 0 : formattedVisitTime.full, "aria-hidden": true, tabIndex: -1, children: (formattedVisitTime == null ? void 0 : formattedVisitTime.short) ?? (highlightedAuthorName || item2.authorName) })
] }),
// 没有访问时间的是之前的历史记录,没有包含作者的 content,所以需要提示作者
!formattedVisitTime && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: styles.srOnly, children: [
"作者:",
item2.authorName
] }),
formattedVisitTime && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: styles.srOnly, children: [
"浏览于",
/* @__PURE__ */ jsxRuntimeExports.jsx("time", { dateTime: formattedVisitTime.short, children: formattedVisitTime.short })
] })
] }),
item2.content && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: Item.content, children: highlightedContent })
] });
});
const SearchBox = ({ searchTerm, onSearchChange, placeholder = "搜索历史记录" }) => {
const handleChange = (e) => {
onSearchChange(e.target.value);
};
const clearSearch = () => {
onSearchChange("");
};
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.searchContainer, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
placeholder,
className: styles.searchInput,
value: searchTerm,
onChange: handleChange,
"aria-label": placeholder
}
),
searchTerm && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: styles.clearSearchButton, onClick: clearSearch, "aria-label": "清除搜索", children: "✕" })
] });
};
const SearchStatus = ({ totalCount, matchedCount }) => {
if (totalCount === 0) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.emptyState, children: "暂无浏览历史" });
if (matchedCount !== -1) {
if (matchedCount === 0) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: styles.emptyState, children: "没有找到匹配的历史记录" });
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.searchInfo, children: [
"找到 ",
matchedCount,
" 条匹配结果"
] });
}
return null;
};
function throttle(delay, callback, options) {
var _ref = options || {}, _ref$noTrailing = _ref.noTrailing, noTrailing = _ref$noTrailing === void 0 ? false : _ref$noTrailing, _ref$noLeading = _ref.noLeading, noLeading = _ref$noLeading === void 0 ? false : _ref$noLeading, _ref$debounceMode = _ref.debounceMode, debounceMode = _ref$debounceMode === void 0 ? void 0 : _ref$debounceMode;
var timeoutID;
var cancelled = false;
var lastExec = 0;
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
}
function cancel(options2) {
var _ref2 = options2 || {}, _ref2$upcomingOnly = _ref2.upcomingOnly, upcomingOnly = _ref2$upcomingOnly === void 0 ? false : _ref2$upcomingOnly;
clearExistingTimeout();
cancelled = !upcomingOnly;
}
function wrapper() {
for (var _len = arguments.length, arguments_ = new Array(_len), _key = 0; _key < _len; _key++) {
arguments_[_key] = arguments[_key];
}
var self = this;
var elapsed = Date.now() - lastExec;
if (cancelled) {
return;
}
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
function clear() {
timeoutID = void 0;
}
if (!noLeading && debounceMode && !timeoutID) {
exec();
}
clearExistingTimeout();
if (debounceMode === void 0 && elapsed > delay) {
if (noLeading) {
lastExec = Date.now();
if (!noTrailing) {
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
}
} else {
exec();
}
} else if (noTrailing !== true) {
timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === void 0 ? delay - elapsed : delay);
}
}
wrapper.cancel = cancel;
return wrapper;
}
function debounce(delay, callback, options) {
var _ref = options || {}, _ref$atBegin = _ref.atBegin, atBegin = _ref$atBegin === void 0 ? false : _ref$atBegin;
return throttle(delay, callback, {
debounceMode: atBegin !== false
});
}
function useDebouncedState(initialValue, delay = 300) {
const [value, setValue] = require$$1.useState(initialValue);
const [debouncedValue, setDebouncedValue] = require$$1.useState(initialValue);
require$$1.useEffect(() => {
const handler = debounce(delay, setDebouncedValue, {
atBegin: true
});
handler(value);
return () => {
var _a;
(_a = handler.cancel) == null ? void 0 : _a.call(handler);
};
}, [value, delay]);
return [value, debouncedValue, setValue];
}
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 = (item2) => {
try {
const raw = _GM_getValue(STORAGE_KEY);
const historyItems = raw ? JSON.parse(raw) : [];
const existingIndex = historyItems.findIndex((i) => i.itemId === item2.itemId);
if (existingIndex !== -1) {
historyItems.splice(existingIndex, 1);
}
historyItems.push(item2);
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 = (item2) => {
var _a, _b;
const zop = item2.dataset.zop;
if (!zop) {
logger.error("无法读取回答或文章信息", item2.dataset);
return;
}
try {
const data = JSON.parse(zop);
if (data.type === "pin") {
const userLinkEl = (_a = item2.closest(".Feed")) == null ? void 0 : _a.querySelector(".UserLink-link");
if (userLinkEl) data.authorName = userLinkEl.innerText;
data.url = `https://www.zhihu.com/pin/${data.itemId}`;
const contentTextEl = (_b = item2.querySelector(`.RichText`)) == null ? void 0 : _b.innerText;
if (contentTextEl) data.title = contentTextEl;
} else {
const linkEl = item2.querySelector(".ContentItem-title a");
if (linkEl) data.url = linkEl.href;
const contentEl = item2.querySelector(".RichText");
if (contentEl) {
let contentText = contentEl.innerText;
if (!contentText.startsWith(data.authorName)) {
contentText = `${data.authorName}:${contentText}`;
}
data.content = contentText.slice(0, 120) + "...";
}
}
data.visitTime = Date.now();
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 item2 = target.closest(".ContentItem");
if (item2) saveHistoryFromElement(item2);
});
};
const createSegmenter = () => {
if (Intl == null ? void 0 : Intl.Segmenter) {
const segmenterInstance = new Intl.Segmenter("zh", { granularity: "word" });
return (text) => {
const trimmedText = text.trim();
if (!trimmedText) return [];
const ignoreWords = /* @__PURE__ */ new Set([
"的",
"了",
"是",
"在",
"和",
"有",
"就",
"不",
"也",
"这",
"那",
"吗",
"吧",
"啊",
"哦",
"啦",
"呀",
"!",
"?",
",",
"。",
"、",
";",
":",
"“",
"”",
"‘",
"’",
"《",
"》",
"[",
"]",
"{",
"}",
".",
"(",
")",
"【",
"】",
"——",
"—",
"…",
"·"
]);
const segments = Array.from(segmenterInstance.segment(trimmedText)).map((item2) => item2.segment.trim()).filter((word) => word && !ignoreWords.has(word));
const uniqueTerms = /* @__PURE__ */ new Set([...segments, trimmedText]);
return Array.from(uniqueTerms);
};
}
return (text) => {
const trimmedText = text.trim();
if (!trimmedText) return [];
const parts = trimmedText.split(/\s+/).map((part) => part.trim()).filter(Boolean);
const uniqueTerms = /* @__PURE__ */ new Set([...parts, trimmedText]);
return Array.from(uniqueTerms);
};
};
const segmenter = createSegmenter();
const isItemMatch = (item2, term) => {
if (!term) return true;
const lowerTerm = term.toLowerCase();
const { title: title2, content: content2, authorName } = item2;
if (title2.toLowerCase().includes(lowerTerm)) return true;
if (content2 == null ? void 0 : content2.toLowerCase().includes(lowerTerm)) return true;
return authorName.toLowerCase().includes(lowerTerm);
};
const findAllMatches = (text, searchTerm) => {
if (!text) return [];
const result = [];
const termLower = searchTerm.toLowerCase();
let startIndex = 0;
let matchIndex;
while ((matchIndex = text.toLowerCase().indexOf(termLower, startIndex)) !== -1) {
result.push({
start: matchIndex,
end: matchIndex + searchTerm.length,
term: searchTerm
});
startIndex = matchIndex + 1;
}
return result;
};
const searchItem = (items, term) => {
if (!term) return /* @__PURE__ */ new Map();
const result = /* @__PURE__ */ new Map();
const searchTerms = segmenter(term);
items.forEach((item2, index) => {
let hasMatches = false;
const itemResult = {
terms: [],
matches: {}
};
for (const searchTerm of searchTerms) {
if (!isItemMatch(item2, searchTerm)) continue;
if (!itemResult.terms.includes(searchTerm)) {
itemResult.terms.push(searchTerm);
}
hasMatches = true;
const fields = ["title", "authorName"];
if (item2.content) fields.push("content");
fields.forEach((field) => {
const text = item2[field];
const matches = findAllMatches(text, searchTerm);
if (matches.length > 0) {
if (!itemResult.matches[field]) {
itemResult.matches[field] = [];
}
itemResult.matches[field].push(...matches);
}
});
}
if (hasMatches) result.set(index, itemResult);
});
return result;
};
const HistoryDialog = ({ isOpen, onClose }) => {
const [searchTerm, debouncedValue, setSearchTerm] = useDebouncedState("", 300);
const historyItems = getHistory();
const matchedItems = require$$1.useMemo(() => searchItem(historyItems, debouncedValue), [historyItems, debouncedValue]);
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(SearchBox, { searchTerm, onSearchChange: setSearchTerm })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: styles.dialogBody, children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(SearchStatus, { totalCount: historyItems.length, matchedCount: searchTerm ? matchedItems.size : -1 }),
/* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: styles.historyList, children: historyItems.map((item2, i) => {
const isMatch = !searchTerm || matchedItems.has(i);
if (!isMatch) return null;
return /* @__PURE__ */ jsxRuntimeExports.jsx(
HistoryItem,
{
item: item2,
searchResult: matchedItems.get(i),
ref: i === 0 ? firstItemRef : null
},
item2.itemId
);
}) })
] })
] })
}
);
};
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);