Greasy Fork is available in English.
清理标题括号数字,替换消息/私信按钮,收藏夹可配置,解除复制限制,隐藏侧边栏,并增强过滤干扰内容、调整布局,重新对阅读页面卡片化设计,美化界面极大提升阅读体验,新增护眼色开关(全局持久化)
// ==UserScript==
// @name 知乎++
// @description 清理标题括号数字,替换消息/私信按钮,收藏夹可配置,解除复制限制,隐藏侧边栏,并增强过滤干扰内容、调整布局,重新对阅读页面卡片化设计,美化界面极大提升阅读体验,新增护眼色开关(全局持久化)
// @namespace http://tampermonkey.net/
// @icon https://www.zhihu.com/favicon.ico
// @license MIT
// @version 4.0
// @author ddrwin
// @match *://*/*
// @match https://*.zhihu.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @note 2026.03.22 V1.0 全局清理网页标题开头的未读数字提示(如 (3条消息))
// @note 2026.03.23 V1.2 将首页消息/私信按钮替换为收藏夹、最近浏览
// @note 2026.03.24 V1.3 隐藏右侧边栏及创作中心,清理消息/私信按钮上的通知数字
// @note 2026.03.25 V2.1 创作中心改为可配置的收藏设置,允许清空地址,解除复制版权限制,仅保留纯净文本
// @note 2026.03.25 V2.2 增加文章页居中并自适应宽度,自动点击“查看全部回答”,隐藏首页“热榜”和“视频”选项卡,只保留“推荐”
// @note 2026.03.25 V3.0 首页、搜索结果页、问题页答案卡片美化,热榜条目卡片美化;过滤推荐页中的视频、文章、链接卡片、教育卡片、专栏、电商等干扰内容,过滤答案页中的视频答案,调整推荐页、问题页、搜索页主列宽度为900px,在问题页标题下方显示修改时间, 隐藏搜索框占位符,加大问题标题字体
// @note 2026.03.26 V3.1 统一卡片样式为 Baidu++ 风格(浅绿色背景、蓝色边框、阴影)
// @note 2026.03.27 V3.5 动态更新页面类型类,改用 CSS 变量方案实现护眼色,彻底消除 SPA 路由切换后类残留导致的样式错乱失效的问题
// @note 2026.03.28 V3.9 彻底解决 SPA 路由切换后类残留导致的样式错乱;新增圈子页面(/ring)完整样式支持;优化专栏主页及个人主页卡片布局。
// @note 2026.03.28 V4.0 设置面板增加动画开关,新增 ANIM_KEY 存储键,默认值 true(默认开启动画),与护眼色独立存储、独立控制。
// ==/UserScript==
(function() {
'use strict';
// ============================================================
// 1. 颜色配置:两套色板,面板切换时只改 CSS 变量,样式规则不变
// ============================================================
/**
* 在这里集中管理两套颜色。
* - softEye:护眼绿色系
* - white:默认白色系
*
* 新增/修改颜色时只需改这里,CSS 规则无需动。
*/
const COLOR_PALETTES = {
softEye: {
cardBg: '#F5FCFA',
cardBgHover: '#E9F7F3',
cardBorder: '#3D7CD4',
cardBorderHover: '#2c6fc7',
cardShadow: '0 8px 20px rgba(0,0,0,0.15)',
cardShadowHover: '0 12px 28px rgba(0,0,0,0.2)',
},
white: {
cardBg: '#ffffff',
cardBgHover: '#ffffff',
cardBorder: '#e1e8ed',
cardBorderHover: '#cbd5e0',
cardShadow: '0 2px 8px rgba(0,0,0,0.08)',
cardShadowHover: '0 8px 20px rgba(0,0,0,0.12)',
},
};
// ============================================================
// 2. 配置存储
// ============================================================
const STORAGE_KEY = 'zhihu_collection_url';
const SOFT_EYE_KEY = 'zhihu_soft_eye';
const ANIM_KEY = 'zhihu_anim';
const getCollectionUrl = () => GM_getValue(STORAGE_KEY, '');
const saveCollectionUrl = (url) => {
if (url === null || url === undefined) return false;
GM_setValue(STORAGE_KEY, url.trim());
return true;
};
const getSoftEyeEnabled = () => GM_getValue(SOFT_EYE_KEY, true);
const setSoftEyeEnabled = (enabled) => {
GM_setValue(SOFT_EYE_KEY, enabled);
applyTheme();
};
const getAnimEnabled = () => GM_getValue(ANIM_KEY, true);
const setAnimEnabled = (enabled) => {
GM_setValue(ANIM_KEY, enabled);
applyTheme();
};
// ============================================================
// 3. 核心:用 CSS 变量驱动主题切换
//
// CSS 变量写在 document.documentElement(即 <html>)上,
// 不依赖 body class,SPA 路由切换、框架重渲染都不会丢失,
// 无需任何 MutationObserver 来"防御"。
// ============================================================
/**
* 把对应色板的值写入 CSS 自定义属性。
* 调用一次即永久生效,直到页面关闭或再次调用。
*/
const applyTheme = () => {
const softEye = getSoftEyeEnabled();
const anim = getAnimEnabled();
const palette = COLOR_PALETTES[softEye ? 'softEye' : 'white'];
const root = document.documentElement;
root.style.setProperty('--zh-card-bg', palette.cardBg);
root.style.setProperty('--zh-card-border', palette.cardBorder);
root.style.setProperty('--zh-card-shadow', palette.cardShadow);
if (anim) {
root.style.setProperty('--zh-card-bg-hover', palette.cardBgHover);
root.style.setProperty('--zh-card-border-hover', palette.cardBorderHover);
root.style.setProperty('--zh-card-shadow-hover', palette.cardShadowHover);
root.style.setProperty('--zh-card-transform-hover', 'translateY(-2px)');
root.style.setProperty('--zh-transition', 'all 0.3s ease');
} else {
root.style.setProperty('--zh-card-bg-hover', palette.cardBg);
root.style.setProperty('--zh-card-border-hover', palette.cardBorder);
root.style.setProperty('--zh-card-shadow-hover', palette.cardShadow);
root.style.setProperty('--zh-card-transform-hover', 'translateY(0)');
root.style.setProperty('--zh-transition', 'none');
}
};
const applySoftEye = () => applyTheme();
// ============================================================
// 4. 设置面板
// ============================================================
const showSettingsPanel = () => {
if (document.getElementById('zh-settings-panel')) return;
const currentUrl = getCollectionUrl();
const overlay = document.createElement('div');
overlay.id = 'zh-settings-overlay';
overlay.style.cssText = `
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.4);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
`;
const panel = document.createElement('div');
panel.id = 'zh-settings-panel';
panel.style.cssText = `
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
width: 360px;
padding: 24px 20px;
text-align: left;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
`;
panel.innerHTML = `
<h3 style="margin: 0 0 20px 0; font-size: 20px; font-weight: 600; text-align: center;">知乎++ 设置</h3>
<div style="margin-bottom: 20px;">
<label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 8px;">📁 收藏夹地址</label>
<input type="text" id="collection-url-input" value="${currentUrl.replace(/"/g, '"')}" style="
width: 100%;
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
">
<div style="font-size: 12px; color: #6c757d; margin-top: 6px;">
例如:https://www.zhihu.com/collection/12345678<br>
留空并点击确认可清除地址
</div>
</div>
<label style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
<span style="font-size: 14px;">🌿 护眼色模式</span>
<input type="checkbox" id="soft-eye-checkbox" style="width: 18px; height: 18px;">
</label>
<label style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
<span style="font-size: 14px;">✨ 悬浮动画效果</span>
<input type="checkbox" id="anim-checkbox" style="width: 18px; height: 18px;">
</label>
<div style="display: flex; gap: 12px;">
<button id="confirm-settings-btn" style="
background: #3D7CD4;
border: none; color: white;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer; flex: 1;
">确认</button>
<button id="close-settings-btn" style="
background: #e9ecef;
border: none; color: #495057;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer; flex: 1;
">取消</button>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
const input = document.getElementById('collection-url-input');
const checkbox = document.getElementById('soft-eye-checkbox');
const animCheckbox = document.getElementById('anim-checkbox');
const confirmBtn = document.getElementById('confirm-settings-btn');
const cancelBtn = document.getElementById('close-settings-btn');
checkbox.checked = getSoftEyeEnabled();
animCheckbox.checked = getAnimEnabled();
confirmBtn.addEventListener('click', () => {
saveCollectionUrl(input.value.trim());
GM_setValue(SOFT_EYE_KEY, checkbox.checked);
GM_setValue(ANIM_KEY, animCheckbox.checked);
applyTheme();
overlay.remove();
});
cancelBtn.addEventListener('click', () => overlay.remove());
};
// ============================================================
// 5. 全局标题清理
// ============================================================
const cleanTitle = () => {
const titleElem = document.getElementsByTagName('title')[0];
if (!titleElem) return;
const newTitle = titleElem.innerText.replace(/^\(\d+(?:[^)]*)?\)\s*/, '');
if (newTitle !== titleElem.innerText) titleElem.innerText = newTitle;
};
cleanTitle();
const titleElem = document.querySelector('title');
if (titleElem) {
const titleObserver = new MutationObserver(() => {
titleObserver.disconnect();
cleanTitle();
titleObserver.observe(titleElem, { childList: true });
});
titleObserver.observe(titleElem, { childList: true });
}
// ============================================================
// 6. 知乎专属功能
// ============================================================
if (window.location.hostname.includes('zhihu.com')) {
// ---------- 6.1 解除复制版权限制 ----------
document.addEventListener('copy', (e) => {
const selection = window.getSelection();
if (selection && selection.toString().trim() !== '') {
const plainText = selection.toString();
e.clipboardData.setData('text/plain', plainText);
e.clipboardData.setData('text/html', plainText.replace(/\n/g, '<br>'));
e.preventDefault();
e.stopPropagation();
console.log('[知乎定制] 已拦截复制,仅保留纯净文本');
}
}, { capture: true });
// ---------- 6.2 样式注入 ----------
//
// 所有卡片颜色属性统一引用 CSS 变量(--zh-card-*)。
// 切换护眼色时只需改变量值,这段 CSS 无需修改。
// 新增颜色属性:在 COLOR_PALETTES 里加字段,
// 在 applySoftEye 里加一行 setProperty,在这里加一处 var() 引用即可。
//
const style = document.createElement('style');
style.textContent = `
/* 隐藏消息/私信弹窗 */
.PushNotifications-menuContainer,
.Messages-menuContainer,
[id^="Popover"][class*="PushNotifications"],
[id^="Popover"][class*="Messages"] {
display: none !important;
}
/* 隐藏右侧边栏 */
.Post-Row-Content-right,
.css-1qyytj7,
div[data-za-detail-view-path-module="RightSideBar"] {
display: none !important;
}
/* 隐藏顶部导航栏的"付费咨询"和"知学堂"按钮 */
a[href="https://www.zhihu.com/consult"],
a[href="https://www.zhihu.com/education/learning"] {
display: none !important;
}
/* 内容宽度自适应 */
.Post-Row-Content-left {
max-width: 100% !important;
width: 90% !important;
margin-left: 0 !important;
margin-right: auto !important;
}
.Post-Row-Content-left-article {
max-width: 100% !important;
}
/* 首页:只保留"推荐"选项卡 */
a[aria-controls="Topstory-hot"],
a[aria-controls="Topstory-zvideo"],
ul[class~="AppHeader-Tabs"] li:not(:first-child) {
display: none !important;
}
/* 隐藏搜索框占位符 */
::placeholder {
color: transparent !important;
}
/* 推荐页过滤干扰内容 */
.TopstoryItem--advertCard,
.TopstoryItem-isRecommend:has(.VideoAnswerPlayer-video),
.TopstoryItem-isRecommend:has(.ZVideoItem-video),
.TopstoryItem-isRecommend:has(.RichText-video),
.TopstoryItem-isRecommend:has(.CopyrightRichText-richTex),
.TopstoryItem-isRecommend:has(.RichText-LinkCardContainer),
.TopstoryItem-isRecommend:has(.RichText-EduCardContainer),
.TopstoryItem-isRecommend:has(div[data-za-extra-module*="Post"]),
.TopstoryItem-isRecommend:has(.RichText-Ecommerce),
.TopstoryItem-isRecommend:has(.ecommerce-ad-box) {
display: none !important;
}
/* 问题页隐藏视频答案 */
.AnswerItem:has(.VideoCard-video-content),
.AnswerItem:has(.VideoAnswerPlayer) {
display: none !important;
}
/* 主列宽度调整 */
.Topstory-container,
.Topstory-mainColumn,
.Question-main,
.Question-mainColumn,
.Search-container,
.SearchMain {
width: 900px !important;
}
/* 问题页显示修改时间 */
meta[itemprop="dateModified"] {
display: block;
height: 20px;
padding: 10px 0;
margin-top: 10px;
}
meta[itemprop="dateModified"]::after {
content: "修改时间: " attr(content);
color: #8590a6;
font-size: 14px;
}
/* 问题页标题字体加大 */
.ContentItem-title {
font-size: x-large !important;
}
/* ===== 卡片样式:全部使用 CSS 变量,护眼/白色均由变量决定 ===== */
.TopstoryItem:not(.TopstoryItem-feedList),
.SearchResult-Card,
.Question-mainColumn .AnswerItem,
.RelevantQuery,
.HotItem,
.css-i83kfi,
.TopicFeedItem,
.Post-Main.Post-NormalMain,
.CollectionDetailPageItem {
background: var(--zh-card-bg) !important;
border: 1px solid var(--zh-card-border) !important;
border-radius: 12px !important;
box-shadow: var(--zh-card-shadow) !important;
margin-bottom: 20px !important;
padding: 20px 24px !important;
transition: var(--zh-transition) !important;
}
.TopstoryItem:not(.TopstoryItem-feedList):hover,
.SearchResult-Card:hover,
.Question-mainColumn .AnswerItem:hover,
.RelevantQuery:hover,
.HotItem:hover,
.css-i83kfi:hover,
.TopicFeedItem:hover,
.Post-Main.Post-NormalMain:hover,
.CollectionDetailPageItem:hover {
background: var(--zh-card-bg-hover) !important;
border-color: var(--zh-card-border-hover) !important;
box-shadow: var(--zh-card-shadow-hover) !important;
transform: var(--zh-card-transform-hover) !important;
}
/* 问题页答案卡片特殊处理 */
.Question-mainColumn .AnswerItem {
margin-bottom: 0px !important;
}
/* 专栏页已更内容特殊处理 */
.css-1a2rdla,
.css-oarhve {
margin-bottom: 20px !important;
}
/* 拼接答案卡片:与现有卡片共享 CSS 变量,hover 效果一致 */
.zh-injected-answer-card {
background: var(--zh-card-bg) !important;
border: 1px solid var(--zh-card-border) !important;
border-radius: 12px !important;
box-shadow: var(--zh-card-shadow) !important;
margin-bottom: 20px !important;
padding: 20px 24px !important;
transition: var(--zh-transition) !important;
}
.zh-injected-answer-card:hover {
background: var(--zh-card-bg-hover) !important;
border-color: var(--zh-card-border-hover) !important;
box-shadow: var(--zh-card-shadow-hover) !important;
transform: var(--zh-card-transform-hover) !important;
}
/* 相关搜索模块内部样式 */
.RelevantQuery h2 {
font-size: 18px !important;
font-weight: 600 !important;
margin-bottom: 16px !important;
}
.RelevantQuery ul {
display: flex !important;
flex-wrap: wrap !important;
gap: 12px !important;
list-style: none !important;
padding-left: 0 !important;
}
.RelevantQuery li { margin: 0 !important; }
.RelevantQuery li a {
display: inline-block !important;
padding: 6px 14px !important;
background: #ffffff !important;
border-radius: 20px !important;
color: #3476d2 !important;
text-decoration: none !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
}
.RelevantQuery li a:hover {
background: #eef2f6 !important;
color: #0f4c81 !important;
transform: scale(1.02) !important;
}
/* 隐藏搜索结果页右侧边栏 */
.css-knqde { display: none !important; }
/* 浏览历史页主内容区域加宽至900px */
.css-9511cm {
width: 900px !important;
max-width: 900px !important;
margin: 0 auto !important;
}
/* 收藏页主内容区域加宽至900px */
.CollectionsDetailPage-mainColumn {
width: 900px !important;
max-width: 900px !important;
margin: 0 auto !important;
}
/* 专栏主页内容区域加宽至900px(仅 html.zh-column 生效) */
html.zh-column .css-1pariuy,
html.zh-column .css-jqhguc,
html.zh-column .css-44kk6u,
html.zh-column .css-1u9sxdg{
width: 900px !important;
max-width: 900px !important;
}
/* 专栏主页文章卡片样式(仅 html.zh-column 生效,不影响其他页面) */
html.zh-column .css-9w3zhd {
width: 860px !important;
background: var(--zh-card-bg) !important;
border: 1px solid var(--zh-card-border) !important;
border-radius: 12px !important;
box-shadow: var(--zh-card-shadow) !important;
margin-bottom: 20px !important;
margin-left: 20px !important;
padding: 20px 24px !important;
transition: var(--zh-transition) !important;
}
html.zh-column .css-9w3zhd:hover {
background: var(--zh-card-bg-hover) !important;
border-color: var(--zh-card-border-hover) !important;
box-shadow: var(--zh-card-shadow-hover) !important;
transform: var(--zh-card-transform-hover) !important;
}
/* 专题页文章外壳容器:仅保留上下间距,不加卡片装饰 */
html.zh-column .ContentItem.ArticleItem {
margin-bottom: 20px !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
/* 个人主页:页头宽度 900px */
html.zh-people .ProfileHeader,
html.zh-people .Profile-main,
html.zh-people .css-16mzn5c,
html.zh-people .Profile-mainColumn {
max-width: 900px !important;
width: 900px !important;
margin: 0 auto !important;
}
/* 个人主页:回答卡片样式 */
html.zh-people .ContentItem.AnswerItem,
html.zh-people .ContentItem.ArticleItem,
html.zh-people .ContentItem.PinItem {
background: var(--zh-card-bg) !important;
border: 1px solid var(--zh-card-border) !important;
border-radius: 12px !important;
box-shadow: var(--zh-card-shadow) !important;
margin-bottom: 0px !important;
padding: 20px 24px !important;
transition: var(--zh-transition) !important;
}
html.zh-people .ContentItem.AnswerItem:hover,
html.zh-people .ContentItem.ArticleItem:hover,
html.zh-people .ContentItem.PinItem:hover {
background: var(--zh-card-bg-hover) !important;
border-color: var(--zh-card-border-hover) !important;
box-shadow: var(--zh-card-shadow-hover) !important;
transform: var(--zh-card-transform-hover) !important;
}
/* 圈子页面:页头宽度 900px */
html.zh-ring .css-1g878q7 {
max-width: 900px !important;
width: 900px !important;
margin: 0 auto !important;
}
/* 圈子页面(/ring-feeds):PinItem 卡片样式 */
html.zh-ring .ContentItem.PinItem {
background: var(--zh-card-bg) !important;
border: 1px solid var(--zh-card-border) !important;
border-radius: 12px !important;
box-shadow: var(--zh-card-shadow) !important;
margin-bottom: 0px !important;
padding: 20px 24px !important;
transition: var(--zh-transition) !important;
}
html.zh-ring .ContentItem.PinItem:hover {
background: var(--zh-card-bg-hover) !important;
border-color: var(--zh-card-border-hover) !important;
box-shadow: var(--zh-card-shadow-hover) !important;
transform: var(--zh-card-transform-hover) !important;
}
`;
document.head.appendChild(style);
// 页面加载时立即把对应色板写入 CSS 变量,一次搞定
applyTheme();
// ---------- 6.2.1 动态更新页面类型类(解决 SPA 路由切换后类残留问题) ----------
/**
* 根据当前 URL 给 <html> 添加对应的类(zh-column / zh-people / zh-ring),
* 并移除旧的类,确保样式只作用于正确页面。
*/
const updatePageClass = () => {
const host = window.location.hostname;
const path = window.location.pathname;
// 先移除所有可能已有的类(避免累积)
document.documentElement.classList.remove('zh-column', 'zh-people', 'zh-ring');
// zh-column:专栏/专题/文章详情相关页面
if (
// zhuanlan.zhihu.com/c_xxx(专栏主页)、/p/xxx(文章详情)
(host === 'zhuanlan.zhihu.com' && /^\/(c_|p\/)/.test(path))
// www.zhihu.com/column/c_xxx(专题主页)、/column-square(专栏广场)
|| (host === 'www.zhihu.com' && /^\/(column\/|column-square)/.test(path))
) {
document.documentElement.classList.add('zh-column');
}
// zh-people:个人主页
if (host === 'www.zhihu.com' && /^\/people\//.test(path)) {
document.documentElement.classList.add('zh-people');
}
// zh-ring:圈子页面(/ring-feeds 和 /ring/host/xxx)
if (host === 'www.zhihu.com' && /^\/ring/.test(path)) {
document.documentElement.classList.add('zh-ring');
}
};
// 初始调用一次
updatePageClass();
// 劫持 history.pushState 和 replaceState,以便在 SPA 路由切换时更新类
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
// 延迟更新,等待 DOM 变化完成
setTimeout(updatePageClass, 100);
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
setTimeout(updatePageClass, 100);
};
// 监听浏览器前进/后退事件
window.addEventListener('popstate', () => setTimeout(updatePageClass, 100));
// 补充:监听 title 变化作为"页面已渲染"信号(比 setTimeout 更准确)
// 知乎每次 SPA 路由完成后都会更新 <title>,此时 DOM 已稳定
const titleElemForRoute = document.querySelector('title');
if (titleElemForRoute) {
new MutationObserver(() => updatePageClass()).observe(titleElemForRoute, { childList: true });
}
// ---------- 6.3 答案文章页:原地注入全部回答 + 无限滚动 ----------
//
// 仅在单答案页 (/question/xxx/answer/xxx) 生效。
// - 拦截"查看全部回答"按钮,阻止跳转,改为在当前页尾部拼接答案列表。
// - 利用知乎 /api/v4/questions/{id}/feeds 接口分批加载,
// 用 paging.next 游标驱动无限滚动,行为与问题页一致。
//
const answerPageMatch = window.location.pathname.match(/\/question\/(\d+)\/answer\/\d+/);
if (answerPageMatch) {
const questionId = answerPageMatch[1];
let injected = false;
let nextFeedUrl = null;
let loadingMore = false;
// 当前答案 ID,用于过滤拼接列表中的重复项
const currentAnswerId = String(window.location.pathname.match(/\/answer\/(\d+)/)[1]);
// 修复懒加载图片:把 data-actualsrc 替换为真实 src
const fixLazyImages = (container) => {
// 方式一:处理 <img data-actualsrc="...">
container.querySelectorAll('img[data-actualsrc]').forEach(img => {
img.src = img.dataset.actualsrc;
img.removeAttribute('data-actualsrc');
});
// 方式二:处理被 <noscript> 包裹的图片(知乎 SSR 场景)
container.querySelectorAll('noscript').forEach(ns => {
const tmp = document.createElement('div');
tmp.innerHTML = ns.textContent;
const img = tmp.querySelector('img');
if (img) {
// 用 data-original 或 src 中第一个真实地址
const realSrc = img.dataset.original || img.src;
if (realSrc && !realSrc.startsWith('data:')) {
img.src = realSrc;
img.removeAttribute('data-original');
ns.replaceWith(img);
}
}
});
// 方式三:使用 IntersectionObserver 按需加载剩余懒图
const lazyImgs = container.querySelectorAll('img[data-original]');
if (lazyImgs.length === 0) return;
const imgObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
if (img.dataset.original) {
img.src = img.dataset.original;
img.removeAttribute('data-original');
}
obs.unobserve(img);
});
}, { rootMargin: '200px' });
lazyImgs.forEach(img => imgObserver.observe(img));
};
// 构建单条答案卡片 DOM(与现有卡片样式共享 CSS 变量)
const buildAnswerCard = (ans) => {
const author = ans.author || {};
const name = author.name || '匿名用户';
const avatar = author.avatar_url || '';
const profileUrl = author.url_token
? `https://www.zhihu.com/people/${author.url_token}` : '#';
const voteup = (ans.voteup_count || 0).toLocaleString();
const comments = ans.comment_count || 0;
const rawContent = ans.content || ans.excerpt || '';
const answerUrl = `https://www.zhihu.com/question/${questionId}/answer/${ans.id}`;
const timeStr = ans.updated_time
? new Date(ans.updated_time * 1000)
.toLocaleDateString('zh-CN', { year:'numeric', month:'2-digit', day:'2-digit' })
: '';
const div = document.createElement('div');
div.className = 'zh-injected-answer-card';
// 头部:头像 + 作者信息 + 赞同数
const header = document.createElement('div');
header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:14px';
header.innerHTML = `
${avatar
? `<img src="${avatar}" style="width:36px;height:36px;border-radius:50%;flex-shrink:0" alt="${name}">`
: `<div style="width:36px;height:36px;border-radius:50%;background:#e0e7ef;flex-shrink:0"></div>`}
<div style="min-width:0">
<a href="${profileUrl}" target="_blank"
style="font-weight:600;color:inherit;text-decoration:none;display:block">${name}</a>
${author.headline
? `<div style="font-size:12px;color:#8590a6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${author.headline}</div>`
: ''}
</div>
<span style="margin-left:auto;color:#8590a6;font-size:13px;white-space:nowrap">▲ ${voteup}</span>
`;
div.appendChild(header);
// 正文:用 innerHTML 注入,然后再修复图片(避免 innerHTML 字符串拼接破坏内容)
const body = document.createElement('div');
body.className = 'RichText ztext';
body.style.cssText = 'font-size:15px;line-height:1.75;word-break:break-word';
body.innerHTML = rawContent;
fixLazyImages(body); // 注入后立刻修复图片
div.appendChild(body);
// 底部:时间 + 评论数 + 原答案链接
const footer = document.createElement('div');
footer.style.cssText = 'margin-top:14px;font-size:13px;color:#8590a6;display:flex;gap:16px;align-items:center;flex-wrap:wrap';
footer.innerHTML = `
${timeStr ? `<span>${timeStr}</span>` : ''}
<span>${comments} 条评论</span>
<a href="${answerUrl}" target="_blank" style="color:#3D7CD4;text-decoration:none;margin-left:auto">查看原答案 →</a>
`;
div.appendChild(footer);
return div;
};
// 请求一批答案并追加到列表容器
const fetchAndAppend = async (list, url) => {
if (loadingMore) return;
loadingMore = true;
const loader = document.createElement('div');
loader.style.cssText = 'text-align:center;padding:24px;color:#8590a6;font-size:14px';
loader.textContent = '加载中…';
list.appendChild(loader);
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
loader.remove();
(json.data || []).forEach(item => {
const t = item.target;
// 跳过当前正在阅读的答案,避免重复展示
if (t && t.id && t.content !== undefined && String(t.id) !== currentAnswerId) {
list.appendChild(buildAnswerCard(t));
}
});
// paging.next 是下一页完整 URL,直接使用
nextFeedUrl = (json.paging && !json.paging.is_end)
? json.paging.next : null;
if (!nextFeedUrl) {
const end = document.createElement('div');
end.style.cssText = 'text-align:center;padding:24px;color:#8590a6;font-size:14px';
end.textContent = '— 已加载全部回答 —';
list.appendChild(end);
}
} catch (e) {
loader.textContent = '⚠️ 加载失败,请稍后重试';
console.error('[知乎定制] 答案加载失败', e);
nextFeedUrl = url; // 保留当前 URL,下次滚动可重试
}
loadingMore = false;
};
// 构建注入容器并启动首次加载
const injectAnswerSection = async () => {
if (injected) return;
injected = true;
// 找到当前答案卡片作为锚点
const anchor = document.querySelector('.Card.AnswerCard')
|| document.querySelector('.QuestionAnswer-content')?.closest('.Card');
if (!anchor) { injected = false; return; }
// 外层包装
const wrapper = document.createElement('div');
wrapper.id = 'zh-all-answers-wrapper';
wrapper.style.marginTop = '24px';
// 标题行
const heading = document.createElement('div');
heading.style.cssText = `
font-size: 18px; font-weight: 600;
margin-bottom: 16px; padding-bottom: 12px;
border-bottom: 2px solid var(--zh-card-border);
color: inherit;
`;
heading.textContent = '全部回答';
wrapper.appendChild(heading);
// 答案列表区域
const list = document.createElement('div');
list.id = 'zh-answer-list';
wrapper.appendChild(list);
// 滚动哨兵(位于列表最底部)
const sentinel = document.createElement('div');
sentinel.id = 'zh-answer-sentinel';
sentinel.style.height = '4px';
wrapper.appendChild(sentinel);
anchor.parentNode.insertBefore(wrapper, anchor.nextSibling);
// 首次加载
const include = encodeURIComponent(
'data[*].content,is_normal,author,voteup_count,comment_count,updated_time,excerpt'
);
const firstUrl = `https://www.zhihu.com/api/v4/questions/${questionId}/feeds`
+ `?include=${include}&limit=5&offset=0&platform=desktop&sort_by=default`;
await fetchAndAppend(list, firstUrl);
// IntersectionObserver 驱动无限滚动
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && nextFeedUrl && !loadingMore) {
fetchAndAppend(list, nextFeedUrl);
}
}, { rootMargin: '400px' });
scrollObserver.observe(sentinel);
};
// 拦截"查看全部回答"按钮:阻止跳转,改为原地注入
const interceptViewAllBtns = () => {
document.querySelectorAll('.Card.ViewAll .QuestionMainAction.ViewAll-QuestionMainAction')
.forEach(btn => {
if (btn.dataset.zhIntercepted) return;
btn.dataset.zhIntercepted = 'true';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
injectAnswerSection().then(() => {
setTimeout(() => {
document.getElementById('zh-all-answers-wrapper')
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
});
}, { capture: true });
});
};
// 自动触发(无需手动点按钮)
const autoInject = () => {
interceptViewAllBtns();
injectAnswerSection();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInject);
} else {
autoInject();
}
// 按钮可能是异步渲染的,持续监听
const viewAllObserver = new MutationObserver(interceptViewAllBtns);
viewAllObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => viewAllObserver.disconnect(), 15 * 1000);
}
// ---------- 6.4 消息/私信按钮替换 ----------
const configs = [
{
ariaLabel: '通知',
svgClass: 'Bell',
getUrl: () => getCollectionUrl(),
newText: '收藏',
newAriaLabel: '收藏夹',
},
{
ariaLabel: '私信',
svgClass: 'Chat',
getUrl: () => 'https://www.zhihu.com/recent-viewed',
newText: '最近浏览',
newAriaLabel: '最近浏览',
},
];
const modifiedContainers = new Set();
const findButton = (cfg) => {
let btn = document.querySelector(`button[aria-label="${cfg.ariaLabel}"]`);
if (!btn) {
const svg = document.querySelector(`svg[class*="${cfg.svgClass}"]`);
if (svg) btn = svg.closest('button');
}
return btn;
};
const hideBadge = (container) => {
const badge = container.querySelector('.css-dkw0ir');
if (badge) badge.style.display = 'none';
};
const modifyButton = (cfg) => {
const btn = findButton(cfg);
if (!btn) return null;
const container = btn.closest('.Popover') || btn;
if (modifiedContainers.has(container)) return container;
const fix = () => {
const span = container.querySelector('.css-vurnku');
if (span) {
if (span.textContent !== cfg.newText) span.textContent = cfg.newText;
} else {
for (let node of container.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === cfg.ariaLabel) {
if (node.textContent !== cfg.newText) node.textContent = cfg.newText;
break;
}
}
}
if (container.getAttribute('aria-label') !== cfg.newAriaLabel) {
container.setAttribute('aria-label', cfg.newAriaLabel);
}
hideBadge(container);
};
fix();
container.addEventListener('click', (e) => {
fix();
e.stopPropagation();
e.preventDefault();
const targetUrl = cfg.getUrl();
if (targetUrl && targetUrl.trim() !== '') {
window.location.href = targetUrl;
} else if (cfg.ariaLabel === '通知') {
showSettingsPanel();
} else {
console.warn('[知乎定制] 未获取到有效URL');
}
}, { capture: true });
container.style.cursor = 'pointer';
modifiedContainers.add(container);
console.log(`[知乎定制] 已修改"${cfg.ariaLabel}"按钮为"${cfg.newText}"`);
return container;
};
const initButtons = () => { for (const cfg of configs) modifyButton(cfg); };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initButtons);
} else {
initButtons();
}
const btnObserver = new MutationObserver(() => {
for (const cfg of configs) {
const btn = findButton(cfg);
if (!btn) continue;
const container = btn.closest('.Popover') || btn;
if (!modifiedContainers.has(container)) {
modifyButton(cfg);
} else {
const span = container.querySelector('.css-vurnku');
let current = span ? span.textContent : '';
if (!span) {
for (let node of container.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === cfg.ariaLabel) {
current = node.textContent.trim();
break;
}
}
}
if (current !== cfg.newText) {
if (span) span.textContent = cfg.newText;
else {
for (let node of container.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === cfg.ariaLabel) {
node.textContent = cfg.newText;
break;
}
}
}
container.setAttribute('aria-label', cfg.newAriaLabel);
console.log(`[知乎定制] 修正了"${cfg.ariaLabel}"按钮文字(被重置)`);
}
hideBadge(container);
}
}
});
btnObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => btnObserver.disconnect(), 5 * 60 * 1000);
// ---------- 6.5 创作中心改为"++设置"按钮 ----------
const modifyCreator = () => {
const creatorBtn = document.querySelector('a[href="https://www.zhihu.com/creator"]');
if (!creatorBtn) return;
if (creatorBtn.hasAttribute('data-customized')) return;
creatorBtn.setAttribute('data-customized', 'true');
const textSpan = creatorBtn.querySelector('.css-vurnku');
if (textSpan) textSpan.textContent = '++设置';
else {
for (let node of creatorBtn.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '创作中心') {
node.textContent = '++设置';
break;
}
}
}
creatorBtn.setAttribute('aria-label', '设置');
creatorBtn.style.cursor = 'pointer';
creatorBtn.style.marginLeft = '12px';
creatorBtn.removeAttribute('href');
creatorBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showSettingsPanel();
});
console.log('[知乎定制] 创作中心已改为"++设置"按钮');
};
modifyCreator();
const creatorObserver = new MutationObserver(() => modifyCreator());
creatorObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => creatorObserver.disconnect(), 5 * 60 * 1000);
}
})();