// ==UserScript==
// @name 知乎美化整合版(展开/折叠 + 宽屏/侧边栏/顶栏/图片高度)
// @namespace https://www.zhihu.com/
// @version v2026.7.21
// @description 默认折叠;点击左外侧空白切换展开/折叠,收起时回到展开前锚点 + 背景闪动;菜单可开关宽屏、移除侧边栏、隐藏顶栏、调整图片高度。
// @author quellan
// @match *://www.zhihu.com/*
// @match *://zhuanlan.zhihu.com/*
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @license GPL-3.0
// ==/UserScript==
(function () {
'use strict';
// ====== 1. 菜单定义 ======
var menu_ALL = [
['menu_leftToggle', '左侧空白切换展开/折叠', '点击帖子左外侧空白切换对应帖子的展开/折叠', true],
['menu_flash', '收起时背景闪动提示', '收起帖子时背景闪动三下提示位置', true],
['menu_widescreenDisplay', '宽屏显示', '各页面开启宽屏显示(刷新后查看效果)', ''],
['menu_widescreenDisplayIndex', '首页', '宽屏显示', true],
['menu_widescreenDisplayQuestion', '问题页', '宽屏显示', true],
['menu_widescreenDisplaySearch', '搜索页、话题页', '宽屏显示', true],
['menu_widescreenDisplayCollection','收藏页', '宽屏显示', true],
['menu_widescreenDisplayPost', '文章页', '宽屏显示', false],
['menu_widescreenDisplayWidth', '宽屏宽度', '宽屏宽度 (默认 1000)', '1000'],
['menu_hideSidebar', '移除右侧侧边栏', '隐藏知乎所有页面右侧边栏、广告、作者卡片', true],
['menu_picHeight', '调整图片最大高度', '限制帖子内图片最大高度为 500px', true],
['menu_hideTitle', '向下翻时自动隐藏顶栏', '向下翻时自动隐藏顶栏', true]
], menu_ID = [];
// ====== 2. 函数声明(hoisted)======
function safe(fn) {
try { fn(); } catch (e) { console.error('[zhihu-enhance]', fn.name, e); }
}
function registerMenuCommand() {
for (const id of menu_ID) { try { GM_unregisterMenuCommand(id); } catch (e) {} }
menu_ID = [];
for (let i = 0; i < menu_ALL.length; i++) {
const item = menu_ALL[i];
item[3] = GM_getValue(item[0]);
if (item[0] === 'menu_widescreenDisplay') {
menu_ID.push(GM_registerMenuCommand(`#️⃣ ${item[1]}`, function () {
menu_setting(item[1], item[2], [
menu_ALL[i + 1], menu_ALL[i + 2], menu_ALL[i + 3],
menu_ALL[i + 4], menu_ALL[i + 5], menu_ALL[i + 6]
]);
}));
} else if (item[0].indexOf('menu_widescreenDisplay') === -1) {
menu_ID.push(GM_registerMenuCommand(`${item[3] ? '✅' : '❌'} ${item[1]}`, function () {
menu_switch(item[3], item[0], item[2]);
}));
}
}
}
function menu_value(menuName) {
for (const menu of menu_ALL) {
if (menu[0] === menuName) return menu[3];
}
}
function menu_switch(currentVal, Name, Tips) {
GM_setValue(Name, !currentVal);
GM_notification({
text: `${currentVal ? '已关闭' : '已开启'} [${Tips}] 功能\n(点击刷新网页后生效)`,
timeout: 3500,
onclick: function () { location.reload(); }
});
registerMenuCommand();
}
function menu_setting(title, tips, menu) {
const styleEl = document.createElement('style');
styleEl.textContent = `
.zhihuE_SettingBackdrop_1 {position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 203; display: flex; flex-direction: column; justify-content: center; overflow: auto;}
.zhihuE_SettingBackdrop_2 {position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 0; background-color: rgba(18,18,18,.65);}
.zhihuE_SettingRoot {position: relative; z-index: 1; margin: 0 auto; min-width: 400px; max-width: 600px; color: #535353; background-color: #fff; border-radius: 3px;}
.zhihuE_SettingHeader {padding: 10px 20px; color: #fff; font-weight: bold; background-color: #3994ff; border-radius: 3px 3px 0 0;}
.zhihuE_SettingHeader span {float: right; cursor: pointer;}
.zhihuE_SettingMain {padding: 10px 20px;}
.zhihuE_SettingMain input[type=text] {width: 50px; margin: 0 4px;}
.zhihuE_SettingMain label {margin-right: 20px; user-select: none; cursor: pointer; vertical-align: middle;}
.zhihuE_SettingMain hr {border: 0.5px solid #f4f4f4;}
`;
document.head.appendChild(styleEl);
let html = `
`;
document.body.insertAdjacentHTML('beforeend', html);
setTimeout(function () {
document.querySelector('.zhihuE_SettingClose').onclick = function () {
this.closest('.zhihuE_SettingBackdrop_1').remove();
styleEl.remove();
};
document.querySelector('.zhihuE_SettingBackdrop_2').onclick = function (e) {
if (e.target === this) document.querySelector('.zhihuE_SettingClose').click();
};
document.getElementsByName('zhihuE_Setting_Checkbox').forEach(function (cb) {
cb.addEventListener('click', function () { GM_setValue(this.value, this.checked); });
});
document.getElementsByName('menu_widescreenDisplayWidth')[0].onchange = function () {
GM_setValue(this.name, this.value);
};
}, 100);
}
function addStyle() {
let style = `
.Question-mainColumnLogin { display: none !important; }
.Pc-card.Card { display: none !important; }
.Recommendations-Main { display: none !important; }
`;
if (location.pathname === '' || location.pathname === 'hot' || location.pathname === 'follow') {
style += `
.TopstoryItem--advertCard { display: none !important; }
main.App-main > .Topstory > div:not(.Topstory-container) { display: none !important; }
`;
}
if (menu_value('menu_hideSidebar')) style += hideSidebarCSS;
if (menu_value('menu_widescreenDisplayIndex')) style += style_widescreenDisplayIndex;
if (menu_value('menu_widescreenDisplayQuestion') && location.pathname.indexOf('question') > -1) style += style_widescreenDisplayQuestion;
if (menu_value('menu_widescreenDisplaySearch') && (location.pathname === 'search' || location.pathname.indexOf('club') > -1 || location.pathname.indexOf('topic') > -1)) style += style_widescreenDisplaySearch;
if (menu_value('menu_widescreenDisplayCollection') && location.pathname.indexOf('collection') > -1) style += style_widescreenDisplayCollection;
if (menu_value('menu_widescreenDisplayPost') && location.hostname.indexOf('zhuanlan') > -1 && location.pathname.indexOf('edit') === -1) style += style_widescreenDisplayPost;
if (menu_value('menu_hideTitle')) style += style_hideTitle;
if (menu_value('menu_picHeight')) style += style_picHeight;
style += style_flash;
const styleEl = document.createElement('style');
styleEl.textContent = style;
// document-start 时 document.lastChild 可能是 DocumentType,appendChild 会抛错;
// 用 document.head(已存在则用)或 document.documentElement(,一定存在)
const target = document.head || document.documentElement;
if (target) target.appendChild(styleEl);
else requestAnimationFrame(function () { (document.head || document.documentElement).appendChild(styleEl); });
}
function initHideHeaderScroll() {
if (!menu_value('menu_hideTitle')) return;
const headerSel = 'header.AppHeader';
let lastScrollY = 0, headerEl = null;
const threshold = 80;
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(function () { fn.apply(this, args); timer = null; }, delay);
}
};
}
function handleScroll() {
if (!headerEl) headerEl = document.querySelector(headerSel);
if (!headerEl) return;
const currY = window.scrollY;
if (currY > lastScrollY && currY > threshold) headerEl.classList.add('is-hidden');
else headerEl.classList.remove('is-hidden');
lastScrollY = currY;
}
window.addEventListener('scroll', throttle(handleScroll, 30));
const observer = new MutationObserver(throttle(function () {
headerEl = document.querySelector(headerSel);
}, 100));
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
}
function initSidebarObserver() {
if (!menu_value('menu_hideSidebar')) return;
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(function () { func.apply(this, args); }, wait);
};
}
function cleanSidebarCard() {
requestAnimationFrame(function () {
document.querySelectorAll('div[style*="position: sticky"], div[style*="position:sticky"]').forEach(function (div) {
if (div.style.display !== 'none' && div.querySelector('.Card, .AnswerAuthor, .HotSearchCard')) {
div.style.display = 'none';
}
});
});
}
const observer = new MutationObserver(debounce(cleanSidebarCard, 50));
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', cleanSidebarCard);
else cleanSidebarCard();
}
// ====== 3. 折叠/展开 + 闪动 + 锚点(核心功能)======
function isInteractive(el) {
if (!el || el.nodeType !== 1) return false;
if (INTERACTIVE_TAGS.includes(el.tagName)) return true;
const role = el.getAttribute && el.getAttribute('role');
if (role && INTERACTIVE_ROLES.includes(role)) return true;
if (el.classList && INTERACTIVE_CLASSES.some(c => el.classList.contains(c))) return true;
return false;
}
function findByText(scope, texts) {
if (!scope) return null;
for (const b of scope.querySelectorAll('button')) {
if (texts.includes((b.textContent || '').trim())) return b;
}
return null;
}
// fireClick 派发的 click 会冒泡到 document 触发 onClick,用标志位阻止二次执行
let isFiringClick = false;
// 状态锁:同一帖子 1 秒内禁止重复触发,防止抖动
let lastToggle = { el: null, time: 0 };
// 模拟真实鼠标点击:mousedown → mouseup → click
// span.click() 对知乎的 span.RichContent-collapsedText 不生效,需派发完整事件序列
function fireClick(el) {
if (!el) return false;
const opts = { bubbles: true, cancelable: true, view: window, button: 0, buttons: 1 };
try {
const r = el.getBoundingClientRect();
opts.clientX = r.left + r.width / 2;
opts.clientY = r.top + r.height / 2;
} catch (e) {}
isFiringClick = true;
try {
el.dispatchEvent(new MouseEvent('mousedown', opts));
el.dispatchEvent(new MouseEvent('mouseup', opts));
el.dispatchEvent(new MouseEvent('click', opts));
return true;
} catch (e) {
try { el.click(); return true; } catch (e2) { return false; }
} finally {
isFiringClick = false;
}
}
// 独立覆盖层 div 闪动,不依赖目标元素 class,不受 React 重新渲染影响
// 用 absolute 定位 + 绝对坐标,滚动时覆盖层跟随帖子移动
function flashElement(el) {
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const h = Math.min(rect.height, 200); // 限制高度避免展开态跨多条帖子
const flash = document.createElement('div');
flash.className = 'zhihu-flash-overlay';
flash.style.cssText =
'position:absolute;top:' + (rect.top + window.scrollY) + 'px;left:' + rect.left + 'px;' +
'width:' + rect.width + 'px;height:' + h + 'px;';
document.body.appendChild(flash);
setTimeout(function () { flash.remove(); }, 1300);
}
function findMoreButton(scope) {
if (!scope) return null;
return scope.querySelector('.ContentItem-more') || findByText(scope, ['阅读全文', '展开阅读全文']);
}
function findCollapseButton(scope) {
if (!scope) return null;
return scope.querySelector('.RichContent-collapsedText') ||
scope.querySelector('.ContentItem-collapse') ||
findByText(scope, ['收起', '收起全文']);
}
function toggleContent(content, card) {
if (!content) return;
const anchorHolder = card || content;
if (content.classList.contains('is-collapsed')) {
// 折叠态 → 展开:记录展开前帖子顶部相对视口位置,供折叠时回到此锚点
anchorHolder.dataset.zhihuAnchor = String(content.getBoundingClientRect().top);
const moreBtn = findMoreButton(content) || findMoreButton(card);
if (moreBtn) fireClick(moreBtn);
else content.classList.remove('is-collapsed');
} else {
// 展开态 → 折叠:先回到展开前锚点,再触发收起
// scrollTo 在 fireClick 之前,不会打断知乎收起逻辑;同步执行无延迟
const anchor = parseFloat(anchorHolder.dataset.zhihuAnchor || '');
if (!isNaN(anchor)) {
const rect = content.getBoundingClientRect();
window.scrollTo(0, rect.top + window.scrollY - anchor);
}
const collapseBtn = findCollapseButton(content) || findCollapseButton(card);
if (collapseBtn) fireClick(collapseBtn);
else content.classList.add('is-collapsed');
if (GM_getValue('menu_flash', true)) flashElement(content);
}
}
function onClick(e) {
if (e.button !== 0) return;
if (isInteractive(e.target)) return;
if (isFiringClick) return; // 忽略 fireClick 派发的 click,避免二次触发
const x = e.clientX, y = e.clientY;
const contents = document.querySelectorAll('.RichContent');
for (const content of contents) {
const rect = content.getBoundingClientRect();
if (y < rect.top || y > rect.bottom) continue; // y 不在该帖子范围,跳过
// y 匹配 + 点击在左外侧空白,才执行切换
if (x < rect.left) {
// 状态锁:同一帖子 1 秒内禁止重复触发
const now = Date.now();
if (lastToggle.el === content && now - lastToggle.time < 1000) return;
lastToggle = { el: content, time: now };
const card = content.closest('.ContentItem, .List-item, .TopstoryItem, .Card');
toggleContent(content, card);
e.preventDefault();
e.stopPropagation();
return; // 命中目标帖子后终止,避免下方帖子重复触发
}
// 仅 y 匹配但 x 在内容区,继续遍历下一条(不 return)
}
}
function startClickToggle() {
if (!menu_value('menu_leftToggle')) return;
function start() {
if (!document.body) { requestAnimationFrame(start); return; }
document.addEventListener('click', onClick, true);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
}
// ====== 4. CSS 常量(必须在 safe 调用之前定义,避免 TDZ)======
const INTERACTIVE_TAGS = ['A', 'BUTTON', 'IMG', 'INPUT', 'VIDEO', 'SVG', 'TEXTAREA', 'SELECT', 'CANVAS'];
const INTERACTIVE_ROLES = ['button', 'link', 'checkbox', 'tab', 'menuitem'];
const INTERACTIVE_CLASSES = ['VoteButton', 'Button', 'ContentItem-actions', 'ContentItem-meta'];
const sidebarSelectors = [
'.GlobalSideBar', '.Question-sideColumn', '.Post-SideActions', '.Post-Sub',
'.Post-Row-Content-right', '.Search-sideColumn', '.Topstory-sideColumn',
'.Card.AnswerAuthor', '.Card.AuthorCard', '.HotSearchCard', '.Question-sideColumnAdContainer'
].join(', ');
const hideSidebarCSS = `
${sidebarSelectors},
div[style*="position: sticky"] .Card, div[style*="position:sticky"] .Card,
.Post-SideActions + div[style*="position: sticky"], .Post-SideActions + div[style*="position:sticky"] {
display: none !important;
}
html { overflow-y: scroll !important; overflow-x: hidden !important; }
`;
const W = () => GM_getValue('menu_widescreenDisplayWidth');
const style_widescreenDisplayIndex = `.Topstory-mainColumn, .QuestionWaiting-mainColumn { width: inherit !important; } .GlobalSideBar { display: none !important; } .Topstory-container { width: ${W()}px; }`;
const style_widescreenDisplayQuestion = `.Question-mainColumn, .ListShortcut, .QuestionWaiting-mainColumn { width: inherit !important; } .Question-sideColumn, .GlobalSideBar { display: none !important; } .QuestionWaiting-mainColumn { margin-right: 0 !important; } .Question-main { width: ${W()}px; }`;
const style_widescreenDisplaySearch = `.SearchMain, .ContentLayout-mainColumn, .Club-mainColumn, .Post-mainColumn { width: inherit !important; } .SearchSideBar, .ContentLayout-sideColumn, .Card.QuestionHeaderTopicMeta, .ClubSideBar { display: none !important; } .Search-container, .ContentLayout, .Club-container, .Post-container { width: ${W()}px; }`;
const style_widescreenDisplayCollection = `.CollectionsDetailPage-mainColumn { width: inherit !important; } .CollectionDetailPageSideBar { display: none !important; } .CollectionsDetailPage { width: ${W()}px; }`;
const style_widescreenDisplayPost = `.Post-SideActions { left: calc(10vw) !important; } .Post-NormalMain .Post-Header, .Post-NormalMain > div, .Post-NormalSub > div { width: ${W()}px !important; }`;
const style_hideTitle = `
header.AppHeader { transition: transform 0.25s ease !important; position: sticky !important; top: 0 !important; z-index: 999 !important; }
header.AppHeader.is-hidden { transform: translateY(-100%) !important; }
body { transition: padding-top 0.25s ease !important; }
header.AppHeader.is-hidden ~ main { margin-top: -60px !important; }
`;
const style_picHeight = `.ztext .content_image, .ztext .origin_image, .GifPlayer img { max-height: 500px !important; width: auto !important; }`;
const style_flash = `
@keyframes zhihu-flash-anim { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
.zhihu-flash-overlay { background-color: rgba(255, 200, 60, 0.45); pointer-events: none; z-index: 99999; animation: zhihu-flash-anim 0.4s ease-in-out 3; }
`;
// ====== 5. 启动(必须在所有 const 定义之后,避免 TDZ ReferenceError)======
for (let i = 0; i < menu_ALL.length; i++) {
if (GM_getValue(menu_ALL[i][0]) == null) GM_setValue(menu_ALL[i][0], menu_ALL[i][3]);
}
safe(registerMenuCommand);
safe(addStyle);
safe(initHideHeaderScroll);
safe(initSidebarObserver);
safe(startClickToggle);
})();