// ==UserScript== // @name 知乎桌面版适配手机 // @name:zh-CN 知乎桌面版适配手机 // @namespace https://scriptcat.org/users/dreanzy // @version 0.3.6 // @description 在手机上用知乎 PC 桌面版:单列铺满屏幕、列表卡片化、字号随缩放自适配、顶栏两行带搜索框 // @author Dreanzy // @tag 知乎 // @tag 手机适配 // @tag 桌面版 // @tag 响应式 // @match *://www.zhihu.com/* // @match *://zhuanlan.zhihu.com/* // @run-at document-start // @grant none // @noframes // @license MIT // @original-author kkocdko // @original-license Unlicense // @original-script https://github.com/kkocdko/user-scripts/blob/master/scripts/zhihu-desktop-on-mobile/code.user.js // ==/UserScript== (function () { 'use strict'; // 只有真正的桌面版页面才激活;服务端返回移动版时不改动,避免把移动版弄坏。 const DESKTOP_ANCHOR = 'header.AppHeader, .Topstory-container, .Question-main'; // 桌面模式(UA 是 PC)时知乎不输出 viewport,浏览器按 980px 渲染再整体缩小, // 页面里所有 px 都会缩到六成以下。用设备实宽写死 viewport,把视口拉回屏幕宽度。 function viewportContent() { const w = Math.round(window.screen.width || 0); return w > 0 ? `width=${w}, initial-scale=1, maximum-scale=5, user-scalable=yes` : 'width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes'; } const CSS = `@media (orientation: portrait) { /* 兜底:没被下面规则收住的宽内容不许把整页撑出横向滚动 */ html, body { overflow-x: hidden !important; } /* 关键:把桌面版的固定宽度令牌换成屏幕宽, 1000px 内容容器 / 694px 主列随之塌陷成单列 */ :root { --container-width: calc(100vw - 16px) !important; --app-width: calc(100vw - 16px) !important; --container-padding-x: 8px !important; --right-sidebar-width: 0px !important; } .Topstory-container, .Question-main, .QuestionPage, .Search-container, .SearchMain, .Profile-main, #TopicMain { width: 100vw !important; max-width: 100vw !important; min-width: 0 !important; margin: 0 !important; padding: 0 8px !important; box-sizing: border-box !important; } /* 顶栏保持知乎原样,只放大字号;排不下时自己横向滑动 */ header.AppHeader { width: 100vw !important; max-width: 100vw !important; min-width: 0 !important; box-sizing: border-box !important; overflow-x: auto !important; overflow-y: hidden !important; scrollbar-width: none; } header.AppHeader::-webkit-scrollbar { display: none; } /* 原生搜索框收掉后,导航 + 消息 + 头像一屏放得下,不再靠 1280px 撑出横滑 */ header.AppHeader > * { min-width: 0 !important; } header.AppHeader nav { flex-shrink: 0 !important; } header.AppHeader a, header.AppHeader button, header.AppHeader input, header.AppHeader [role="button"] { font-size: calc(14px * var(--zh-k)) !important; } header.AppHeader svg { flex-shrink: 0; } /* 两行顶栏:第一行导航 + 消息/私信,第二行搜索框,头像绝对定位到第二行右端。 不再把头像 DOM 挪出顶栏 —— React 会把它移回去,形成无限重排 */ header.AppHeader { position: relative !important; overflow: visible !important; height: auto !important; } header.AppHeader > div > a { display: none !important; } header.AppHeader > div { flex-wrap: nowrap !important; } header.AppHeader > div > div { min-width: 0 !important; } header.AppHeader > div > div:last-child { position: absolute !important; right: 8px !important; bottom: calc(4px * var(--zh-k)) !important; } #zh-search { align-items: center !important; padding-right: calc(52px * var(--zh-k)) !important; } #zh-search input { flex: 1 1 auto; } /* 顶栏多出来的一行高度,正文要相应下移,否则被压住 */ main.App-main { margin-top: var(--zh-extra, 0px) !important; } /* 顶栏头像按容器缩放,别让 36px 的头像撑破 34px 的按钮被裁掉 */ header.AppHeader .AppHeader-profileMenu, header.AppHeader .AppHeader-profileEntry { width: auto !important; height: auto !important; padding: 0 !important; line-height: 0 !important; } header.AppHeader img.Avatar, header.AppHeader .AppHeader-profileAvatar { width: calc(32px * var(--zh-k)) !important; height: calc(32px * var(--zh-k)) !important; border-radius: 50% !important; } /* 消息 / 私信:图标在缩放后看不清,只留「消息(12)」这样的文字和数字 */ header.AppHeader button[data-zh-top] { display: inline-flex !important; align-items: center !important; width: auto !important; min-width: 0 !important; height: auto !important; padding: calc(6px * var(--zh-k)) calc(6px * var(--zh-k)) !important; font-size: 0 !important; } header.AppHeader button[data-zh-top] > * { display: none !important; } header.AppHeader button[data-zh-top]::before { content: attr(data-zh-top); font-size: calc(14px * var(--zh-k)); white-space: nowrap; } /* 头像下拉菜单:字号和行高跟着放大,否则缩到看不清 */ .Popover-content[class*="AppHeaderProfileMenu"], .Menu[class*="AppHeaderProfileMenu"] { min-width: calc(150px * var(--zh-k)) !important; padding: calc(6px * var(--zh-k)) 0 !important; } .AppHeaderProfileMenu-item { font-size: calc(14px * var(--zh-k)) !important; min-height: calc(40px * var(--zh-k)) !important; padding: calc(8px * var(--zh-k)) calc(12px * var(--zh-k)) !important; } /* 已经有了自己那一条搜索,顶栏里原生搜索框不用再占位置 */ header.AppHeader [class*="SearchBar"] { display: none !important; } /* 搜索做成底部固定条:知乎顶栏里的搜索框宽度被内部按钮挤死,改不动; 固定条宽度、字号都可控,拇指也够得到 */ #zh-search { display: flex; width: 100%; box-sizing: border-box; padding: calc(6px * var(--zh-k)) 10px; background: Canvas; border-bottom: 1px solid rgba(128, 128, 128, 0.3); } #zh-search input { flex: 1 1 auto; min-width: 0; box-sizing: border-box; font-size: calc(15px * var(--zh-k)) !important; line-height: 1.4; padding: calc(6px * var(--zh-k)) calc(12px * var(--zh-k)); border: 1px solid rgba(128, 128, 128, 0.35); border-radius: 999px; background: transparent; color: CanvasText; } /* 搜索条固定在最上方,知乎顶栏和正文各自下移让位 */ /* 首页顶部的「分享想法 / 提问题 / 写回答 / 写文章 / 发视频」创作入口 */ .WriteArea { display: none !important; } /* 右侧栏一律不要。问题页里它和主列中间还隔着别的节点, 兄弟选择器靠不住,直接按类名片段收掉 */ .Topstory-mainColumn + *, .Question-mainColumn + *, [class*="sideColumn"], [class*="SideBar"], .GlobalSideBar { display: none !important; } /* 主列吃满宽度。父级里还套着固定宽度的壳,百分比算不准, 所以同时给 min-width,让它把外层撑开 */ [class*="mainColumn"] { width: 100% !important; max-width: none !important; min-width: calc(100vw - 16px) !important; margin-right: 0 !important; } /* 问题页标题区:右侧关注卡换到标题下方 */ .QuestionHeader-content { display: flex !important; flex-wrap: wrap !important; } /* 卡片化:页面铺一层浅灰底,每条内容单独成白卡(照 v2ex 手机版的做法), 比一条细线分隔清楚得多;颜色用系统色,深浅主题自动跟随 */ html { background: Canvas !important; } body { background: rgba(128, 128, 128, 0.09) !important; } .Topstory-mainColumnCard, .Card { background: transparent !important; box-shadow: none !important; } /* 卡片容器自己留了一段右边距(给已隐藏的右栏),拉满 */ .Topstory-mainColumnCard { width: 100% !important; max-width: 100% !important; padding: 0 !important; } .TopstoryItem, .AnswerItem { background: Canvas !important; border: none !important; border-radius: calc(10px * var(--zh-k)) !important; margin: 0 0 calc(8px * var(--zh-k)) !important; padding: calc(12px * var(--zh-k)) !important; } /* 卡片里还套着知乎自己的问答卡,边框和底色都撤掉,别做出双层卡片 */ .TopstoryItem .Card, .AnswerItem .Card { background: transparent !important; border-color: transparent !important; box-shadow: none !important; } /* 标题与正文之间留一点气口 */ .ContentItem-title { margin-bottom: calc(6px * var(--zh-k)) !important; font-weight: 600 !important; } /* 摘要不再按固定高度裁行:字号放大后裁出来只有半截字, 而且 !important 会把知乎自己的「展开」覆盖掉,点了没反应 */ .RichContent-inner { max-height: none !important; overflow: visible !important; } /* 回答底部操作条:桌面版是绝对定位,窄屏会跑到屏幕外; 同时字太小、行太矮,手机上点不准,这里统一放大字号和点击区 */ .ContentItem-actions { left: 0 !important; right: 0 !important; margin: 0 !important; padding: calc(8px * var(--zh-k)) 0 calc(6px * var(--zh-k)) !important; min-height: calc(44px * var(--zh-k)) !important; display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; overflow-y: hidden !important; align-items: center !important; font-size: calc(15px * var(--zh-k)) !important; } .ContentItem-actions > * { margin: 0 calc(10px * var(--zh-k)) 0 0 !important; } .ContentItem-actions button, .ContentItem-actions a, .ContentItem-actions .Button { font-size: calc(15px * var(--zh-k)) !important; line-height: 1.4 !important; min-height: calc(36px * var(--zh-k)) !important; padding: calc(4px * var(--zh-k)) calc(8px * var(--zh-k)) !important; } .ContentItem-actions svg { flex-shrink: 0; } .ContentItem-actions.is-fixed { width: 100vw !important; padding: calc(4px * var(--zh-k)) calc(8px * var(--zh-k)) !important; } /* 「收起」常驻右侧,其余按钮在左边横向滑动(脚本会给它打 data-zh-pin) */ .ContentItem-actions > [data-zh-pin] { margin-left: auto !important; position: sticky !important; right: 0 !important; z-index: 2 !important; background: Canvas !important; box-shadow: calc(-12px * var(--zh-k)) 0 calc(10px * var(--zh-k)) Canvas !important; padding-left: calc(12px * var(--zh-k)) !important; } /* 头像和作者名:桌面版是 24px 头像、13px 名字,手机上太小 */ .Avatar, img.Avatar, .AuthorInfo-avatarWrapper { width: calc(36px * var(--zh-k)) !important; height: calc(36px * var(--zh-k)) !important; } .AuthorInfo-name, .UserLink-link { font-size: calc(15px * var(--zh-k)) !important; font-weight: 600 !important; } .ContentItem-meta, [class*="CommentItem"] button, [class*="CommentItem"] a { font-size: calc(14px * var(--zh-k)) !important; } /* 顶部「提问 / 写回答 / 写文章 / 发视频」一排 */ .Topstory-mainColumn .Button, .QuestionHeader .Button { font-size: calc(15px * var(--zh-k)) !important; } /* 富文本里的宽内容不撑破页面 */ .RichText img, .RichText video, .RichText iframe, .RichText pre, .Post-content img { max-width: 100% !important; height: auto !important; } .RichText table { display: block !important; max-width: 100% !important; overflow-x: auto !important; } .RichText pre { overflow-x: auto !important; } /* 弹窗与图片查看器不超出屏幕 */ .Modal-content { max-width: 100vw !important; } /* 手机上字号不能太小。--zh-k 是缩放补偿系数: 视口被浏览器放大到 980px 时它是 2.x,字号同比放大,屏幕上仍是 16px 的观感 */ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { font-size: calc(15px * var(--zh-k)) !important; } .RichText, .RichText p, .Post-RichText, .Post-content, .ContentItem-excerpt, .CommentContent, .CommentContent p { font-size: calc(16px * var(--zh-k)) !important; line-height: 1.7 !important; } .ContentItem-title, .QuestionHeader-title { font-size: calc(18px * var(--zh-k)) !important; line-height: 1.45 !important; } .ContentItem-time, .ContentItem-meta, .AnswerItem-time { font-size: calc(13px * var(--zh-k)) !important; } .RichText pre, .RichText code { font-size: calc(14px * var(--zh-k)) !important; } /* 补偿模式下字号被放大,按固定像素裁行的摘要会露出半截字,直接展开 */ html[data-zh-scaled] .RichText, html[data-zh-scaled] .ContentItem-content { max-height: none !important; -webkit-line-clamp: unset !important; } /* App 推广条 */ .OpenInAppButton, .PlaceHolder.List-item { display: none !important; } } `; const root = document.documentElement; root.style.setProperty('--zh-k', '1'); const style = document.createElement('style'); style.id = 'zhihu-desktop-on-mobile'; style.textContent = CSS; let mounted = false; function fixViewport() { const head = document.head; if (!head) return false; const want = viewportContent(); const exist = head.querySelector('meta[name="viewport"]'); if (exist) { if (exist.getAttribute('content') !== want) exist.setAttribute('content', want); return true; } const meta = document.createElement('meta'); meta.name = 'viewport'; meta.setAttribute('content', want); head.appendChild(meta); return true; } // 视口仍比屏幕宽 = 浏览器无视了 viewport,这时按比例放大字号,别让它缩成 6px function fixScale() { const w = root.clientWidth; const sw = window.screen.width || w; const k = Math.min(w / sw, 4); if (k > 1.15) { root.style.setProperty('--zh-k', k.toFixed(3)); root.setAttribute('data-zh-scaled', ''); } else { root.style.setProperty('--zh-k', '1'); root.removeAttribute('data-zh-scaled'); } } // 顶栏导航条目太多,全留着换行后会变成三四行; // 只保留前 5 个(关注 / 推荐 / 热榜 / 专栏 / 圈子),其余推广位热词藏掉 function trimNav() { const links = [...document.querySelectorAll('header.AppHeader a')].filter( (a) => (a.textContent || '').trim() ); links.forEach((a, i) => { if (i >= 5) a.style.setProperty('display', 'none', 'important'); else a.style.removeProperty('display'); }); } // 顶栏自绘搜索条:原生搜索框的宽度被内部按钮挤死,改不动,直接换掉 function addSearch() { if (document.getElementById('zh-search')) return; const hd = document.querySelector('header.AppHeader'); if (!hd) return; const bar = document.createElement('div'); bar.id = 'zh-search'; const input = document.createElement('input'); input.type = 'search'; input.placeholder = '搜索知乎'; input.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; const q = input.value.trim(); if (!q) return; const url = new URL('https://www.zhihu.com/search'); url.searchParams.set('type', 'content'); url.searchParams.set('q', q); location.href = url.toString(); }); bar.appendChild(input); hd.appendChild(bar); // 顶栏外层是个固定高度的壳,撑开它,否则第二行被裁 const shell = hd.parentElement; if (shell) shell.style.setProperty('height', 'auto', 'important'); const syncExtra = () => root.style.setProperty('--zh-extra', bar.offsetHeight + 'px'); syncExtra(); if (window.ResizeObserver) new ResizeObserver(syncExtra).observe(bar); } // 「收起」按钮排在操作条末尾,窄屏上得横滑才能点到,钉到右边常驻。 // 文案可能带箭头或后缀(「收起 ▾」「收起回答」),以「收起」开头、整串不超过 6 个字就算, // 这样「收起全部 12 条评论」这种长文案不会被误钉 const COLLAPSE_RE = /^收起\S{0,4}$/; function pinCollapse() { document.querySelectorAll('.ContentItem-actions').forEach((bar) => { [...bar.children].forEach((el) => { const t = (el.textContent || '').replace(/\s+/g, ''); if (COLLAPSE_RE.test(t)) el.setAttribute('data-zh-pin', '1'); else if (el.hasAttribute('data-zh-pin')) el.removeAttribute('data-zh-pin'); }); }); } // 原生搜索框外层还挂着「直答」等按钮,整块收掉,别在顶栏里留个空位 function hideNativeSearch() { document.querySelectorAll('header.AppHeader [class*="SearchBar"]').forEach((el) => { const p = el.parentElement; if (p) p.style.setProperty('display', 'none', 'important'); }); } // 消息/私信按钮图标在缩放后只剩几个像素,改成纯文字加数字 function tidyTopButtons() { document.querySelectorAll('header.AppHeader button').forEach((btn) => { const bell = btn.querySelector('svg[class*="BellFill"]'); const chat = btn.querySelector('svg[class*="ChatBubble"]'); if (!bell && !chat) return; const label = bell ? '消息' : '私信'; const n = ((btn.textContent || '').match(/\d+/) || [])[0]; const text = n ? label + '(' + n + ')' : label; if (btn.dataset.zhTop !== text) btn.dataset.zhTop = text; }); } function mount() { if (mounted) return true; if (!document.querySelector(DESKTOP_ANCHOR)) return false; mounted = true; (document.head || root).appendChild(style); fixScale(); trimNav(); hideNativeSearch(); tidyTopButtons(); addSearch(); pinCollapse(); return true; } function tick() { fixViewport(); if (mounted) { fixScale(); trimNav(); hideNativeSearch(); tidyTopButtons(); addSearch(); pinCollapse(); } else { mount(); } } tick(); // 知乎用 react-helmet 管 head,会把自己的 viewport 写回来:属性变化也要盯 new MutationObserver(tick).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['content'], }); window.addEventListener('resize', tick); window.addEventListener('DOMContentLoaded', tick); window.addEventListener('load', tick); // helmet 有时在首帧后才写 meta,补两道 setTimeout(tick, 800); setTimeout(tick, 2500); })();