// ==UserScript==
// @name HeyBox Up
// @namespace https://cyojkoy.github.io/Payment/
// @version 0.4.21
// @description 补全小黑盒网页收藏:网页收藏列表不显示“插眼”(CY),本脚本单独拉取 CY 列表,在收藏页(/app/user/favour/content)把 CY 条目补进收藏列表末尾,按帖子去重,结果缓存 10 分钟。社区首页(/app/bbs/home)隐藏右栏热门社区等冗余模块;收藏页、文章管理页与文章详情页隐藏右侧二维码、推广与备案栏;帖子详情页(/app/bbs/link/*)把站点原生评论区停靠到右栏,保留原生回复与点赞,并补充默认/时间/点赞/作者排序,可按接口 is_cy 标记屏蔽插眼评论,正文与评论各自独立滚动,窄屏自动改为上下布局。评论预览侧栏同样提供 CY 开关,面板展开后鼠标在卡片上停留即可切换预览,点评论数字始终可用。顶部导航改为首页、社区、收藏、文章四个「SVG + 文本」入口,图标默认播放轻量动效,可在齿轮设置菜单切换为静态;帖子页左右两侧纵轴中心提供 SVG 上一篇/下一篇箭头(快捷键 [ 与 ]),切换走站内路由,不整页刷新。社区页(/app/topic/link/*)提供只在本社区内生效的帖子搜索,支持时间筛选与加载更多。宽屏社区页改为左频道、中信息流、右热度三栏工作台,中间栏居中且宽度可在齿轮设置菜单按屏幕百分比调节,窄屏自动退回双栏或单栏。帖子正文内嵌的游戏、专辑、百科、比赛、模组等卡片,以及“我的文章”创作者页与频道页补齐深色适配。另提供跟随系统、夜航、纯黑、浅色、纸页、雾青六套主题预设,在导航栏一键切换。需要登录小黑盒:脚本以浏览器内已有登录态请求小黑盒自己的插眼收藏接口(含本地计算的请求签名),只读取当前账号的数据;偏好与缓存仅写入本机 localStorage,不上传、不修改账号数据、不改动站点收藏总数。
// @author Yoko
// @homepageURL https://github.com/CYoJkoY/Yoko-ScriptCat-Scripts/blob/main/docs/heybox-up.md
// @supportURL https://github.com/CYoJkoY/Yoko-ScriptCat-Scripts/issues
// @icon data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3E%3Crect%20width='32'%20height='32'%20rx='8'%20fill='%23232b36'/%3E%3Cpath%20d='M9%207.5h14a2.5%202.5%200%200%201%202.5%202.5v12a2.5%202.5%200%200%201-2.5%202.5H9A2.5%202.5%200%200%201%206.5%2022V10A2.5%202.5%200%200%201%209%207.5Z'%20fill='none'%20stroke='%238aa7c4'%20stroke-width='1.6'/%3E%3Cpath%20d='M10.5%2016c1.2-2.1%203.1-3.2%205.5-3.2s4.3%201.1%205.5%203.2c-1.2%202.1-3.1%203.2-5.5%203.2S11.7%2018.1%2010.5%2016Z'%20fill='%23a9c0d8'/%3E%3Ccircle%20cx='16'%20cy='16'%20r='1.7'%20fill='%23232b36'/%3E%3C/svg%3E
// @match https://www.xiaoheihe.cn/*
// @match https://xiaoheihe.cn/*
// @run-at document-start
// @grant none
// @noframes
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const CY_API_PATH = "/bbs/app/profile/bbs/comment/list";
const CY_PAGE_SIZE = 20;
const CY_MAX_ITEMS = 200;
const CACHE_TTL_MS = 10 * 60 * 1000;
const CACHE_KEY_PREFIX = "heybox-up.cy.v1:";
const LINK_HREF_PREFIX = "/app/bbs/link/";
const HIDE_CY_KEY = "heybox-up.hideCy.v1";
const HIDE_CY_EVENT = "heybox-up:hide-cy";
const HIDE_CY_STYLE_ID = "heybox-up-cy-style";
const HIDE_CY_CLASS = "heybox-up-hide-cy";
function readHideCyPreference() {
try {
return localStorage.getItem(HIDE_CY_KEY) === "1";
} catch (error) {
return false;
}
}
function ensureHideCyStyle() {
if (document.getElementById(HIDE_CY_STYLE_ID)) return;
const style = document.createElement("style");
style.id = HIDE_CY_STYLE_ID;
style.textContent = "body." + HIDE_CY_CLASS + " [data-heybox-up-cy=\"1\"] { display: none !important; }";
(document.head || document.documentElement).append(style);
}
function applyHideCyPreference(value) {
ensureHideCyStyle();
if (document.body) document.body.classList.toggle(HIDE_CY_CLASS, value);
document.dispatchEvent(new CustomEvent(HIDE_CY_EVENT, { detail: value }));
}
function setHideCyPreference(value) {
try {
localStorage.setItem(HIDE_CY_KEY, value ? "1" : "0");
} catch (error) {
// 隐私模式下 localStorage 可能不可写,本次页面仍按新状态生效。
}
applyHideCyPreference(value);
}
// 首页评论预览是可选能力:关掉后浮层完全隐藏,也不接管信息流里的评论数字。
const COMMENT_PREVIEW_KEY = "heybox-up.commentPreview.v1";
const COMMENT_PREVIEW_EVENT = "heybox-up:comment-preview";
function readCommentPreviewPreference() {
try {
return localStorage.getItem(COMMENT_PREVIEW_KEY) !== "0";
} catch (error) {
return true;
}
}
function setCommentPreviewPreference(value) {
try {
localStorage.setItem(COMMENT_PREVIEW_KEY, value ? "1" : "0");
} catch (error) {
console.warn("[HeyBox Up] 保存评论预览设置失败", error);
}
document.dispatchEvent(new CustomEvent(COMMENT_PREVIEW_EVENT, { detail: value }));
}
// 导航图标默认播放轻量动效,静态版本供不喜欢动画的用户关闭。
const NAV_MOTION_KEY = "heybox-up.navMotion.v1";
const NAV_MOTION_EVENT = "heybox-up:nav-motion";
function readNavMotionPreference() {
try {
return localStorage.getItem(NAV_MOTION_KEY) !== "0";
} catch (error) {
return true;
}
}
function setNavMotionPreference(value) {
try {
localStorage.setItem(NAV_MOTION_KEY, value ? "1" : "0");
} catch (error) {
console.warn("[HeyBox Up] 保存导航图标设置失败", error);
}
document.dispatchEvent(new CustomEvent(NAV_MOTION_EVENT, { detail: value }));
}
// 社区工作台的中间栏宽度:按视口百分比记,默认 56%,两侧等宽留白。
const FEED_WIDTH_KEY = "heybox-up.feedWidth.v1";
const FEED_WIDTH_DEFAULT = 56;
const FEED_WIDTH_MIN = 40;
const FEED_WIDTH_MAX = 76;
function normalizeFeedWidth(value) {
// localStorage 未设置时 getItem 返回 null;Number(null) 是 0,会被误夹到下限 40。
if (value === null || value === undefined || value === "") return FEED_WIDTH_DEFAULT;
const parsed = Math.round(Number(value));
if (!Number.isFinite(parsed)) return FEED_WIDTH_DEFAULT;
return Math.min(FEED_WIDTH_MAX, Math.max(FEED_WIDTH_MIN, parsed));
}
function readFeedWidth() {
try {
return normalizeFeedWidth(localStorage.getItem(FEED_WIDTH_KEY));
} catch (error) {
return FEED_WIDTH_DEFAULT;
}
}
function applyFeedWidth(value) {
const width = normalizeFeedWidth(value);
document.documentElement.style.setProperty("--heybox-up-feed-pct", String(width));
return width;
}
function setFeedWidthPreference(value) {
const width = applyFeedWidth(value);
try {
localStorage.setItem(FEED_WIDTH_KEY, String(width));
} catch (error) {
// 隐私模式下写不进去,本次页面仍按新宽度排版。
}
return width;
}
// 站点会在评论上带 is_cy,值可能是数字 1、字符串 "1" 或布尔 true。
function isCyComment(comment) {
return Boolean(comment) && (comment.is_cy === 1 || comment.is_cy === true || comment.is_cy === "1");
}
const THEME_CLASS = "heybox-up-theme";
const THEME_ATTR = "data-heybox-up-theme";
const THEME_STORAGE_KEY = "heybox-up.theme.v1";
const THEME_ENTRY_ID = "heybox-up-theme-entry";
const THEME_MEDIA = window.matchMedia("(prefers-color-scheme: dark)");
// ==theme-core:start==
const THEME_PRESETS = [
{ id: "system", label: "跟随系统", hint: "随系统深浅切换", dark: null, icon: "system", swatch: ["#f5f7f9", "#1b2430"] },
{ id: "dark", label: "夜航", hint: "冷灰深色,细边框", dark: true, icon: "dark", swatch: ["#0f1419", "#8fb0cf"] },
{ id: "oled", label: "纯黑", hint: "纯黑底,低圆角", dark: true, icon: "oled", swatch: ["#000000", "#a8c0d8"] },
{ id: "light", label: "浅色", hint: "站点原色", dark: false, icon: "light", swatch: ["#ffffff", "#eef1f4"] },
{ id: "paper", label: "纸页", hint: "暖纸底,衬线标题", dark: false, icon: "paper", swatch: ["#f4efe4", "#8a6f4e"] },
{ id: "mist", label: "雾青", hint: "冷雾底,大圆角", dark: false, icon: "mist", swatch: ["#eef3f5", "#4f7f8a"] },
];
const THEME_IDS = THEME_PRESETS.map((preset) => preset.id);
function getThemePreset(id) {
return THEME_PRESETS.find((preset) => preset.id === id) || THEME_PRESETS[0];
}
function normalizeThemeMode(value) {
return THEME_IDS.includes(value) ? value : "system";
}
function resolveTheme(mode, systemDark) {
return mode === "system" ? (systemDark ? "dark" : "light") : getThemePreset(mode).id;
}
// ==theme-core:end==
let currentThemeMode = "system";
let themeEntry = null;
let previewToggle = null;
let navMotionToggle = null;
let feedWidthChips = [];
function readThemeMode() {
try {
return normalizeThemeMode(localStorage.getItem(THEME_STORAGE_KEY));
} catch (error) {
console.warn("[HeyBox Up] 读取主题设置失败", error);
return "system";
}
}
function writeThemeMode(mode) {
try {
localStorage.setItem(THEME_STORAGE_KEY, mode);
} catch (error) {
console.warn("[HeyBox Up] 保存主题设置失败", error);
}
}
const THEME_CSS = `
:root.heybox-up-theme {
--heybox-up-bg: #fff;
--heybox-up-surface: #fff;
--heybox-up-subtle: rgba(20, 25, 30, .06);
--heybox-up-raised: #d7dce3;
--heybox-up-border: rgba(20, 25, 30, .1);
--heybox-up-text: #14191e;
--heybox-up-text-2: #3c4249;
--heybox-up-muted: #6b7280;
--heybox-up-on-accent: #fff;
--heybox-up-icon: #62676e;
--heybox-up-icon-hover: #14191e;
--heybox-up-icon-hover-bg: rgba(20, 25, 30, .06);
--heybox-up-accent: #6f8db3;
--hbu-menu-bg: #fff;
--hbu-menu-text: #14191e;
--hbu-menu-muted: #6b7280;
--hbu-menu-border: rgba(20, 25, 30, .12);
--hbu-menu-hover: rgba(20, 25, 30, .06);
color-scheme: light;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] {
color-scheme: dark;
--heybox-up-bg: #0f1419;
--heybox-up-surface: #171d24;
--heybox-up-subtle: #1e2630;
--heybox-up-raised: #26303b;
--heybox-up-border: #2d3845;
--heybox-up-text: #dce5ef;
--heybox-up-text-2: #b4c0ce;
--heybox-up-muted: #8593a3;
--heybox-up-accent: #8fb0cf;
--heybox-up-on-accent: #10161c;
--heybox-up-icon: #b4c0ce;
--heybox-up-icon-hover: #dce5ef;
--heybox-up-icon-hover-bg: rgba(220, 229, 239, .09);
--hbu-menu-bg: #171d24;
--hbu-menu-text: #dce5ef;
--hbu-menu-muted: #8593a3;
--hbu-menu-border: #2d3845;
--hbu-menu-hover: rgba(220, 229, 239, .09);
--hbu-success: #7fc3a2;
--hbu-danger: #e0858d;
--hbu-warning: #d8b77a;
--hb-general-color-bg-0: #0f1419;
--hb-general-color-bg-1: #171d24;
--hb-general-color-bg-2: #1e2630;
--hb-general-color-bg-3: #1e2630;
--hb-general-color-bg-4: #171d24;
--hb-general-color-bg-5: rgba(23, 29, 36, .9);
--hb-general-color-bg-6: rgba(220, 229, 239, .1);
--hb-general-color-text-1: #dce5ef;
--hb-general-color-text-2: #b4c0ce;
--hb-general-color-text-3: #8593a3;
--hb-general-color-text-4: #6d7a88;
--hb-general-color-text-5: #9dc0e2;
--hb-general-color-text-6: #10161c;
--hb-general-color-stroke-0: #2d3845;
--hb-general-color-stroke-1: #2d3845;
--hb-general-color-stroke-2: #26303b;
--hb-general-color-stroke-3: #1e2630;
--hb-general-color-primary: #8fb0cf;
--hb-general-color-success: #7fc3a2;
--hb-general-color-danger: #e0858d;
--hb-primary: #dce5ef;
--hb-primary--value: 220, 229, 239;
--hb-primary-dynamic: #dce5ef;
--hb-primary-dynamic--value: 220, 229, 239;
--hb-neutral-100: #1e2630;
--hb-neutral-100--value: 30, 38, 48;
--hb-neutral-200: #1e2630;
--hb-neutral-200--value: 30, 38, 48;
--hb-neutral-300: #171d24;
--hb-neutral-300--value: 23, 29, 36;
--hb-neutral-400: #26303b;
--hb-neutral-400--value: 38, 48, 59;
--hb-neutral-500: #2d3845;
--hb-neutral-500--value: 45, 56, 69;
--hb-neutral-600: #6d7a88;
--hb-neutral-600--value: 109, 122, 136;
--hb-neutral-700: #8593a3;
--hb-neutral-700--value: 133, 147, 163;
--hb-neutral-800: #b4c0ce;
--hb-neutral-800--value: 180, 192, 206;
--el-color-primary: #8fb0cf;
--el-color-primary-light-9: #1b2735;
--el-color-success: #7fc3a2;
--el-color-danger: #e0858d;
--el-color-warning: #d8b77a;
--el-bg-color: #171d24;
--el-bg-color-page: #0f1419;
--el-bg-color-overlay: #171d24;
--el-fill-color-blank: #171d24;
--el-fill-color: #1e2630;
--el-fill-color-light: #1e2630;
--el-fill-color-lighter: #1e2630;
--el-fill-color-extra-light: #1e2630;
--el-text-color-primary: #dce5ef;
--el-text-color-regular: #b4c0ce;
--el-text-color-secondary: #8593a3;
--el-text-color-placeholder: #8593a3;
--el-text-color-disabled: #6d7a88;
--el-border-color: #2d3845;
--el-border-color-light: #2d3845;
--el-border-color-lighter: #2d3845;
--el-border-color-extra-light: #26303b;
--el-border-color-dark: #3b4653;
--el-border-color-darker: #46515f;
--el-disabled-bg-color: #1e2630;
--el-disabled-text-color: #8593a3;
--el-mask-color: rgba(15, 20, 25, .8);
--el-overlay-color-lighter: rgba(0, 0, 0, .6);
--el-table-bg-color: #171d24;
--el-table-tr-bg-color: #171d24;
--el-table-header-bg-color: #1e2630;
--el-table-row-hover-bg-color: #1e2630;
--el-table-border-color: #2d3845;
--el-table-text-color: #dce5ef;
--el-table-header-text-color: #b4c0ce;
--el-box-shadow: 0 12px 32px rgba(0, 0, 0, .3);
--el-box-shadow-light: 0 0 12px rgba(0, 0, 0, .28);
--el-box-shadow-lighter: 0 0 6px rgba(0, 0, 0, .24);
scrollbar-color: #3b4653 #0f1419;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] body {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#app, .hb-website__app, .hb-page__app, .hb-layout__main, .hb-layout-main__container, .hb-layout__content) {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.nav, .nav-user .nav-user-popover, .el-popper, .el-dropdown-menu, .el-dialog, .el-drawer, .el-message-box, .el-card, .el-table, .el-pagination, .el-select-dropdown, .el-autocomplete-suggestion, .hb-cpt__selector .selector__pull-list, .page-header__container-bg, .page-header__back-btn, .tab-list--wrapper, .link-tab-list, .hb-cpt-page-header, .hb-cpt__pagination, .hb-cpt__slide-tab, .user-profile-wrapper, .bbs-info, .message-center__main, .message-center__aside-title, .message-center__search-box, .message-home__tabs, .message-content-preview, .message-content-preview__post, .search-result__tab-header, .search-result__tool, .hb-search-result, .hot-search, .price-box, .result-mini-app__item, .result-filter-tag__tag, .hb-bbs-link__header, .hb-bbs-link__content, .link-comment, .link-reply, .comment__comment-header, .topic-link__header, .post__container, .image-text__container, .header-image__item, .bbs-link__related-recommend, .hb-cpt__empty, .qr-section, .app-link-item) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-bbs-community, #page-bbs-link, #page-bbs-list, .page-bbs-community, .page-bbs-link) {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
/* Search page: page and list paint fixed white bands above and below the results. */
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list::after {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list .list::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list .list::after {
background-color: var(--heybox-up-bg) !important;
}
/* Result rows are separated by inline 4px light strips. */
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list .search-result__space {
background-color: transparent !important;
}
/* Horizontal result scroller fades to hard-coded white on the right edge. */
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-bbs-list .hb-cpt__pagination--right {
background-image: linear-gradient(to left, var(--heybox-up-surface), var(--heybox-up-surface), transparent) !important;
}
/* 我的文章(创作者中心):站点整页写死 #fff,深色下逐块补齐,避免只剩外框变黑。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-creator-content-management, #page-creator-content-management-detail) {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-creator-content-management .creator-content-management__header, #page-creator-content-management .creator-content-management__tabs, #page-creator-content-management-detail .content-detail__content-wrapper, #page-creator-content-management-detail .content-detail__section, .com-chart, #creator-preview__frame, .creator-preview-normal, .creator-draft) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-creator-content-management .creator-content-management__tabs::after, #page-creator-content-management .content-management-home__content + .content-management-home__content::after) {
background-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-creator-content-management .content-management-home__content:hover, #page-creator-content-management .creator-content-management__tabs .sorter:hover, .creator-content-item__operation-item:hover) {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .creator__data-indicator {
background-color: var(--heybox-up-subtle) !important;
box-shadow: 0 0 0 .5px var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .creator__data-indicator.active {
background-color: var(--heybox-up-surface) !important;
box-shadow: inset 0 0 0 1px var(--heybox-up-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-creator-content-management .creator-content-management__header, #page-creator-content-management-detail .content-detail__title, #page-creator-content-management-detail .content-detail__section-header, .data-indicator__data, .com-chart .nav, .com-chart .selection .text, .aside-miniprogram__header, .aside-miniprogram__item--name) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.data-indicator__title, #page-creator-content-management .creator-content-management__tabs .sorter, #page-topic-link .topic-link__sorter) {
color: var(--heybox-up-text-2) !important;
}
/* 频道页:整页浅灰底 + 三块白卡,连同上下固定色条一起换底。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-topic-link {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-topic-link::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-topic-link::after {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-topic-link .topic-link__panel, #page-topic-link .topic-link__header, #page-topic-link .topic-link__filter-row, #page-topic-link .topic-link__game-detail, .topic-aside__topic-indicator, .topic__aside-miniprogram) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-topic-link .topic-link__main::after {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .topic__aside-miniprogram .aside-miniprogram__item,
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-topic-link .topic-link__splitline::after {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .topic__aside-miniprogram .aside-miniprogram__item:hover {
background-color: var(--heybox-up-surface) !important;
}
/* 兜底扫描挂上的类:只改底色、描边与文字色,不动布局。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] .heybox-up-dark-flat {
background-color: var(--heybox-up-surface) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .heybox-up-dark-flat-subtle {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .heybox-up-dark-edge {
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .heybox-up-dark-ink {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.hb-bbs-home, .bbs-home__content-list, .bbs-home__content-item, .bbs-home__topic-item, .bbs-community-hot-topic, .bbs-community__search-module, .right-side-default, .hb-cpt__scroll-list, #page-bbs-link .list, .link-tags__tag-item, .hb-cpt__popover-menu, .hb-cpt__report, .route-to-app__qrcode, .scroll-list__top-btn, .hb-cpt__loading.circle, .search-history, .search__pull-list) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.hb-cpt__image--default, .hb-cpt__image--placeholder, .link-reply__input-wrapper) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .bbs-new-style-bottom__rich-stack {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .bbs-new-style-bottom__rich-node {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-user-favour > .content > .list {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-user-favour .user-favour__list,
:root.heybox-up-theme[data-heybox-up-dark="true"] #page-user-favour .user-favour__list > * {
background-color: transparent !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.user-favour__item, .favour-item, .favour-card, .list-content, .bbs-list-content, .bbs-link, .link-item, .hot-topic-item, .creator-content-item, .comment-message-item, .message-list-item.active) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.bbs-content__title, .bbs-content__content, .post__title, .post__content, .image-text__title, .image-text__content, .comment-item__content, .children-item__comment-content, .list-content__username, .comment-item__username, .hot-topic-item__name, .nav-item, .view-header__nav-item, .user-profile-wrapper .name, .bbs-info-item .value, .back-btn__text, .page-header__username, .link-user__username, .section-title__content, .content-tag-text, .slide-tab__tab-label, .slide-tab__tab-cnt, .info-box__username, .hb-editor, .message-list-item__title, .comment-message-item__username, .comment-message-item__text, .message-content-preview__reply-text, .message-content-preview__text, .creator-content-item__title, .editor__more-info-title, .creator-editor__form-label, .frame-top-title, .bbs-link-section-title-text, .user-favour__title) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.list-content__time, .bbs-list-content__time, .post__time, .image-text__time, .comment-item__time, .creator-content-item__create_at, .creator-content-item__type, .creator-content-item__data-item, .message-list-item__time, .user-favour__empty, .scroll-list__no-more-desc, .page-header__meta, .list-content__meta) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.bbs-home__topic-name, .hot-topic__header, .related-recommend__container--header, .link-item__title, .qr-section .title, .qr-section span, .app-link-item span, .search-pull__found-item, .search-pull__history-item, .hb-cpt__popover-menu-item.plain) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.link-item__desc, .bbs-new-style-bottom__comment, .search-pull__section-title, .search-history__item) {
color: var(--heybox-up-muted) !important;
}
/* Search suggestions: site fills history chips light and hot rank labels dark; retheme both. */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.search-pull__history-item, .search-history__item) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.search-pull__history-item:hover, .search-history__item:hover) {
background-color: var(--heybox-up-surface) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search-history__item::before {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search-pull__section-title {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search__hot-rank .hot-rank__tab-item {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search__hot-rank .hot-rank__tab-item.active {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search__hot-rank .hot-rank__list-item .hot-rank__list-item-game-name {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .search__hot-rank .hot-rank__list-item:hover {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.post__content, .image-text__content) :where(p, span, b, strong, em, i, li, h1, h2, h3, h4, h5, h6, blockquote, .text) {
color: inherit !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.post__content, .image-text__content) a {
color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(input:not([type="checkbox"]):not([type="radio"]), textarea, select) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(input, textarea)::placeholder {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] option {
background-color: var(--heybox-up-surface);
color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.el-input__wrapper, .el-textarea__inner, .el-select__wrapper, [contenteditable="true"]) {
background-color: var(--heybox-up-subtle) !important;
color: var(--heybox-up-text) !important;
box-shadow: 0 0 0 1px var(--heybox-up-border) inset !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.tab-item.active, .slide-tab__tab-item.active, .slide-tab__tab-item.active .slide-tab__tab-label, .nav-link.nav-link--active, .nav-link--active) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .result-filter-tag__tag.active {
background-color: var(--heybox-up-accent) !important;
border-color: var(--heybox-up-accent) !important;
color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.page-header__back-btn, .hb-cpt__pagination, .hb-cpt__slide-tab) {
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.el-table th.el-table__cell, .el-table tr) {
background-color: transparent !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.el-table, .el-table__inner-wrapper, .el-table__body-wrapper, .el-table__header-wrapper) {
background-color: var(--heybox-up-surface) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.tab-active-block, .slide-tab-tab__bar) {
background-color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.hb-cpt__button.primary, .hb-cpt__button--primary, .editor-publish__btn.main-btn) {
background-color: var(--heybox-up-accent) !important;
background-image: none !important;
border-color: var(--heybox-up-accent) !important;
color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.nav-user-popover-item:hover, .message-list-item:hover, .link-comment__comment-item:hover, .creator-content-item__operation-item:hover) {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.bbs-info-item, .message-center__aside, .message-center__content, .comment-message-item, .link-comment__comment-item, .creator-content-item, .editor-tabs, .cpt-qa__row, .cpt-qa__tabs, .hb-cpt__editor-title, .editor-image-text__main-content, .editor-image-wrapper__box, .creator-editor__form-item) {
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] [data-mask$="-bg"] {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] [data-mask$="-edge"] {
background-color: var(--heybox-up-surface) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .nav {
--nav-background: rgba(15, 20, 25, .94);
--nav-shadow: 0 6px 16px rgba(0, 0, 0, .28);
--nav-link-color: #8593a3;
--nav-link-hover-color: #dce5ef;
--nav-link-active-color: #dce5ef;
--nav-link-hover-bg: transparent;
--nav-link-active-bg: transparent;
--publish-color: #10161c;
--publish-bg: linear-gradient(46deg, #9db9d3, #8fb0cf);
--publish-hover-shadow: 0 8px 18px rgba(0, 0, 0, .3);
--nav-user-bg: rgba(220, 229, 239, .08);
--nav-user-bg-hover: rgba(220, 229, 239, .14);
--nav-user-divider-color: rgba(220, 229, 239, .12);
--nav-user-name-color: #dce5ef;
--nav-user-arrow-color: rgba(220, 229, 239, .72);
--nav-logo-color: #dce5ef;
--nav-language-color: #8593a3;
--nav-language-border-color: rgba(133, 147, 163, .5);
background: var(--nav-background) !important;
box-shadow: var(--nav-shadow) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .nav :is(.nav-link, .nav-icon) {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .nav button.nav-link.nav-link--active {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .nav-content-left-logo :is(path, polygon, rect) {
fill: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.nav-user-popover-name, .nav-user-popover-item-text) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.nav-user-popover-id, .nav-user-popover-item-icon) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .nav-user-popover-divider {
background-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(button, a, input, textarea, select, summary, [tabindex], [role="button"], [contenteditable="true"]):focus-visible {
outline: 2px solid var(--heybox-up-accent) !important;
outline-offset: 2px !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.comment-item__tag-line, .comment-item__writer-like, .children-item__reply-to, .children-item__create-time, .children-item__ip, .info-box__create-time, .info-box__ip, .like-box, .load-all__text, .link-reply__placeholder, .link-reply__operation-desc) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.comment-children__load-all, .link-reply__editor, .link-reply__operation-item, .hb-toast, .el-message) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .hb-loading-spinner {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.el-message, .el-message__content) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .el-image-viewer__wrapper {
background-color: rgba(0, 0, 0, .88) !important;
}
/* 登录表单保持官方浅色,避免第三方登录控件在深色下失去可读性。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] .website-login {
color-scheme: light;
background-color: #fff !important;
color: #14191e !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .website-login :is(input, textarea, select) {
background-color: transparent !important;
border-color: #e8eaed !important;
color: #14191e !important;
}
/* 文章编辑器:站点把底色写在多层容器里,深色、纸页与雾青预设需要整块覆盖。 */
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor > .content > .list,
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.editor-tabs, .editor-article__main-content, .hb-cpt__editor-title, .editor__menu, .editor__more-info, .creator-editor__form-wrapper, .cpt-editor__publish, .cpt-qa),
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor-article__main-content {
box-shadow: 0 0 0 1px var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu {
box-shadow: inset 0 0 0 1px var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor-article__main-content :is(.hb-editor, .ProseMirror) {
background-color: var(--heybox-up-subtle) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.hb-editor, .ProseMirror) :where(p, span, b, strong, em, i, u, s, li, h1, h2, h3, h4, h5, h6, blockquote, td, th) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.hb-editor, .ProseMirror) :is(a, .prose-link) {
color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor-title__cnt-indicator {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor-publish__btn.main-btn {
background-color: var(--heybox-up-accent) !important;
background-image: none !important;
border-color: var(--heybox-up-accent) !important;
color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) :is(.hb-cpt__button.primary, .hb-cpt__button--primary) {
background-color: var(--heybox-up-accent) !important;
background-image: none !important;
border-color: var(--heybox-up-accent) !important;
color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.editor__topic-item, .editor__hashtag-item, .editor__add-btn, .selector__box, .editor-publish__btn.sub-btn, .cpt-qa__tab, .cpt-qa__key) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.selector__pull-list, .editor-menu-more__list) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .selector__pull-list-item {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.selector__pull-list-item:hover, .selector__pull-list-item.active) {
background-color: var(--heybox-up-raised) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .selector__box {
box-shadow: 0 0 0 1px var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .switch__button {
background-color: var(--heybox-up-raised) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .switch__button.active {
background-color: var(--heybox-up-accent) !important;
border-color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .switch__button-indicator {
background-color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .switch__button.active .switch__button-indicator {
background-color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.editor-form__more-info-title, .creator-editor__form-header, .selector__active, .cpt-qa__head-title, .cpt-qa__row-name) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.topic-item__text, .hashtag-item__text, .editor-form__more-info-desc, .editor__more-info-desc, .cpt-qa__head-sub) {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.switch__true-label, .switch__false-label) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.switch__true-label.active, .switch__false-label.active) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .cpt-form-split-line {
background-color: var(--heybox-up-border) !important;
}
/* Editor dividers and toolbar icons: site paints light 4px / 1px strips and dims icon fills. */
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .creator-editor__form-wrapper::before {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .cpt-editor__publish::after,
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .hb-cpt__editor-title::after,
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor-menu__item-splitline {
background-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .hb-icon {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu__item--content-inactive .hb-icon svg {
opacity: .8 !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu__item.forbid {
opacity: .8 !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu-more__list-item {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu-more__list-item .hb-icon {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu-more__list-item svg {
opacity: .9 !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor .editor__menu .editor-menu-more__list-item:hover {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) [data-mask$="-bg"] {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) [data-mask$="-edge"] {
background-color: var(--heybox-up-surface) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #page-creator-editor :is(.hb-cpt__image--default, .hb-cpt__image--placeholder) {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.creator-preview-normal, .bbs-link) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.bbs-link-section-author, .bbs-link-section-tags) {
background-color: transparent !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-author-name {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-author-time {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-author-follow {
background-color: var(--heybox-up-accent) !important;
background-image: none !important;
border-color: var(--heybox-up-accent) !important;
color: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-author-follow :is(i, span, svg, use) {
color: var(--heybox-up-on-accent) !important;
fill: var(--heybox-up-on-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.bbs-link-article-content, .hb-article) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.bbs-link-article-content, .hb-article) :where(p, span, b, strong, em, i, li, h1, h2, h3, h4, h5, h6, blockquote, .text, .main-title) {
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.bbs-link-article-content, .hb-article) :is(a, .prose-link) {
color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .img-desc {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-tags-item {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame .bbs-link-section-tags-item-text {
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) #creator-preview__frame :is(.bbs-link-section-bottom-info, .bbs-link-section-bottom-info-source) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) .cpt-right-side :is(.footer-info, .footer-row, .report-text, .copyright-text) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme:not([data-heybox-up-theme="light"]) .cpt-right-side .footer-link {
color: var(--heybox-up-text-2) !important;
}
/* 纯黑预设:黑底、低圆角、无阴影,仍复用全部深色覆盖规则。 */
:root.heybox-up-theme[data-heybox-up-theme="oled"] {
--heybox-up-bg: #000;
--heybox-up-surface: #08090b;
--heybox-up-subtle: #101216;
--heybox-up-raised: #161a20;
--heybox-up-border: #1d2026;
--heybox-up-text: #e6ebf2;
--heybox-up-text-2: #b8c2cf;
--heybox-up-muted: #9aa4b2;
--heybox-up-accent: #a8c0d8;
--heybox-up-on-accent: #05070a;
--hbu-menu-bg: #08090b;
--hbu-menu-text: #e6ebf2;
--hbu-menu-muted: #9aa4b2;
--hbu-menu-border: #1d2026;
--hbu-menu-hover: rgba(230, 235, 242, .07);
--hb-general-color-bg-0: #000;
--hb-general-color-bg-1: #08090b;
--hb-general-color-bg-2: #101216;
--hb-general-color-bg-3: #101216;
--hb-general-color-bg-4: #08090b;
--hb-general-color-text-1: #e6ebf2;
--hb-general-color-text-2: #b8c2cf;
--hb-general-color-text-3: #9aa4b2;
--hb-general-color-stroke-0: #1d2026;
--hb-general-color-stroke-1: #1d2026;
--hb-general-color-stroke-2: #161a20;
--hb-general-color-primary: #a8c0d8;
--el-color-primary: #a8c0d8;
--el-color-primary-light-9: #141a22;
--el-bg-color: #08090b;
--el-bg-color-page: #000;
--el-bg-color-overlay: #08090b;
--el-fill-color-blank: #08090b;
--el-fill-color: #101216;
--el-fill-color-light: #101216;
--el-fill-color-lighter: #101216;
--el-fill-color-extra-light: #101216;
--el-border-color: #1d2026;
--el-border-color-light: #1d2026;
--el-border-color-lighter: #1d2026;
--el-table-bg-color: #08090b;
--el-table-tr-bg-color: #08090b;
--el-table-header-bg-color: #101216;
--el-table-row-hover-bg-color: #101216;
--el-table-border-color: #1d2026;
--el-mask-color: rgba(0, 0, 0, .86);
--el-box-shadow: none;
--el-box-shadow-light: none;
scrollbar-color: #2a2e35 #000;
}
:root.heybox-up-theme[data-heybox-up-theme="oled"] .nav {
--nav-background: #000;
--nav-shadow: none;
--publish-color: #05070a;
--publish-bg: #a8c0d8;
--nav-user-bg: rgba(230, 235, 242, .07);
--nav-user-bg-hover: rgba(230, 235, 242, .13);
}
:root.heybox-up-theme[data-heybox-up-theme="oled"] :is(.bbs-home__content-item, .bbs-home__topic-item, .list-content, .user-favour__item, .post__container, .image-text__container, .link-comment, .hot-search, .right-side-default, .hb-bbs-link__header, .hb-bbs-link__content, .search-result__tool, .result-mini-app__item, .price-box, .el-card, .el-dialog, .el-drawer, .el-message-box, .el-popper, .el-dropdown-menu) {
border-radius: 4px !important;
box-shadow: none !important;
}
/* 纸页预设:暖纸底、衬线标题、细边扁平卡片。 */
:root.heybox-up-theme[data-heybox-up-theme="paper"] {
--heybox-up-bg: #f4efe4;
--heybox-up-surface: #fffdf7;
--heybox-up-subtle: #efe8d9;
--heybox-up-raised: #e7ddc9;
--heybox-up-border: #ddd2bd;
--heybox-up-text: #2f2a22;
--heybox-up-text-2: #4a4237;
--heybox-up-muted: #7c7160;
--heybox-up-accent: #8a6f4e;
--heybox-up-icon: #7c7160;
--heybox-up-icon-hover: #2f2a22;
--heybox-up-icon-hover-bg: rgba(47, 42, 34, .07);
--hbu-menu-bg: #fffdf7;
--hbu-menu-text: #2f2a22;
--hbu-menu-muted: #7c7160;
--hbu-menu-border: #ddd2bd;
--hbu-menu-hover: rgba(47, 42, 34, .06);
--hb-general-color-bg-0: #f4efe4;
--hb-general-color-bg-1: #fffdf7;
--hb-general-color-bg-2: #efe8d9;
--hb-general-color-bg-4: #fffdf7;
--hb-general-color-text-1: #2f2a22;
--hb-general-color-text-2: #4a4237;
--hb-general-color-text-3: #7c7160;
--hb-general-color-stroke-0: #ddd2bd;
--hb-general-color-stroke-1: #ddd2bd;
--hb-general-color-primary: #8a6f4e;
--el-color-primary: #8a6f4e;
--el-bg-color: #fffdf7;
--el-bg-color-page: #f4efe4;
--el-bg-color-overlay: #fffdf7;
--el-fill-color-blank: #fffdf7;
--el-fill-color-light: #efe8d9;
--el-text-color-primary: #2f2a22;
--el-text-color-regular: #4a4237;
--el-text-color-secondary: #7c7160;
--el-border-color: #ddd2bd;
--el-border-color-light: #ddd2bd;
scrollbar-color: #cfc2aa #f4efe4;
}
/* 雾青预设:冷雾底、大圆角、柔和浮层。 */
:root.heybox-up-theme[data-heybox-up-theme="mist"] {
--heybox-up-bg: #eef3f5;
--heybox-up-surface: #fbfdfe;
--heybox-up-subtle: #e4ecef;
--heybox-up-raised: #dbe7eb;
--heybox-up-border: #d3e0e5;
--heybox-up-text: #1f2c33;
--heybox-up-text-2: #3e5560;
--heybox-up-muted: #6f8792;
--heybox-up-accent: #4f7f8a;
--heybox-up-icon: #6f8792;
--heybox-up-icon-hover: #1f2c33;
--heybox-up-icon-hover-bg: rgba(31, 44, 51, .07);
--hbu-menu-bg: #fbfdfe;
--hbu-menu-text: #1f2c33;
--hbu-menu-muted: #6f8792;
--hbu-menu-border: #d3e0e5;
--hbu-menu-hover: rgba(31, 44, 51, .06);
--hb-general-color-bg-0: #eef3f5;
--hb-general-color-bg-1: #fbfdfe;
--hb-general-color-bg-2: #e4ecef;
--hb-general-color-bg-4: #fbfdfe;
--hb-general-color-text-1: #1f2c33;
--hb-general-color-text-2: #3e5560;
--hb-general-color-text-3: #6f8792;
--hb-general-color-stroke-0: #d3e0e5;
--hb-general-color-stroke-1: #d3e0e5;
--hb-general-color-primary: #4f7f8a;
--el-color-primary: #4f7f8a;
--el-bg-color: #fbfdfe;
--el-bg-color-page: #eef3f5;
--el-bg-color-overlay: #fbfdfe;
--el-fill-color-blank: #fbfdfe;
--el-fill-color-light: #e4ecef;
--el-text-color-primary: #1f2c33;
--el-text-color-regular: #3e5560;
--el-text-color-secondary: #6f8792;
--el-border-color: #d3e0e5;
--el-border-color-light: #d3e0e5;
scrollbar-color: #b9ccd3 #eef3f5;
}
:root.heybox-up-theme:is([data-heybox-up-theme="paper"], [data-heybox-up-theme="mist"]) body {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme:is([data-heybox-up-theme="paper"], [data-heybox-up-theme="mist"]) :is(#app, main, .hb-website__app, .hb-page__app, .hb-layout__main, .hb-layout-main__container, .hb-layout__content) {
background-color: var(--heybox-up-bg) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme:is([data-heybox-up-theme="paper"], [data-heybox-up-theme="mist"]) :is(.nav, .nav-user .nav-user-popover, .el-popper, .el-dropdown-menu, .el-dialog, .el-drawer, .el-message-box, .hb-cpt__selector .selector__pull-list, .page-header__container-bg, .tab-list--wrapper, .link-tab-list, .hb-cpt-page-header, .user-profile-wrapper, .bbs-info, .message-center__main, .message-center__aside-title, .message-center__search-box, .message-home__tabs, .message-content-preview, .message-content-preview__post, .search-result__tab-header, .hb-search-result, .hb-bbs-link__header, .hb-bbs-link__content, .link-comment, .link-reply, .comment__comment-header, .topic-link__header, .post__container, .image-text__container, .bbs-link__related-recommend, .hb-cpt__empty, .qr-section, .app-link-item, .bbs-home__content-item, .bbs-home__topic-item, .list-content, .user-favour__item, #page-bbs-link .list, .hot-search, .right-side-default, .search-result__tool, .result-mini-app__item, .price-box, .result-filter-tag__tag) {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
:root.heybox-up-theme:is([data-heybox-up-theme="paper"], [data-heybox-up-theme="mist"]) .nav {
--nav-link-color: var(--heybox-up-muted);
--nav-link-hover-color: var(--heybox-up-text);
--nav-link-active-color: var(--heybox-up-text);
--nav-link-hover-bg: transparent;
--nav-link-active-bg: transparent;
--publish-color: #fff;
--publish-bg: var(--heybox-up-accent);
--nav-user-bg: var(--heybox-up-subtle);
--nav-user-bg-hover: var(--heybox-up-raised);
--nav-user-name-color: var(--heybox-up-text);
--nav-user-divider-color: var(--heybox-up-border);
--nav-logo-color: var(--heybox-up-text);
}
:root.heybox-up-theme[data-heybox-up-theme="paper"] .nav {
--nav-background: rgba(244, 239, 228, .95);
--nav-shadow: 0 1px 0 #ddd2bd;
}
:root.heybox-up-theme[data-heybox-up-theme="mist"] .nav {
--nav-background: rgba(251, 253, 254, .84);
--nav-shadow: 0 8px 24px rgba(47, 74, 84, .08);
backdrop-filter: blur(14px) saturate(1.1);
}
:root.heybox-up-theme[data-heybox-up-theme="paper"] :is(.bbs-home__content-item, .bbs-home__topic-item, .list-content, .user-favour__item, .post__container, .image-text__container, .link-comment, .hot-search, .right-side-default, .hb-bbs-link__header, .hb-bbs-link__content, .search-result__tool, .result-mini-app__item, .price-box, .el-card, .el-dialog, .el-drawer, .el-message-box, .el-popper, .el-dropdown-menu) {
border-radius: 3px !important;
box-shadow: none !important;
}
:root.heybox-up-theme[data-heybox-up-theme="mist"] :is(.bbs-home__content-item, .bbs-home__topic-item, .list-content, .user-favour__item, .post__container, .image-text__container, .link-comment, .hot-search, .right-side-default, .hb-bbs-link__header, .hb-bbs-link__content, .search-result__tool, .result-mini-app__item, .price-box, .el-card, .el-dialog, .el-drawer, .el-message-box, .el-popper, .el-dropdown-menu) {
border-radius: 16px !important;
box-shadow: 0 6px 20px rgba(47, 74, 84, .08) !important;
}
:root.heybox-up-theme[data-heybox-up-theme="paper"] :is(.post__title, .image-text__title, .bbs-content__title, .bbs-link-section-title-text, .section-title__content, .user-favour__title, .related-recommend__container--header) {
font-family: Georgia, "Songti SC", "Noto Serif CJK SC", serif;
letter-spacing: .01em;
}
:root.heybox-up-theme[data-heybox-up-theme="paper"] :is(.post__content, .image-text__content) a {
color: var(--heybox-up-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
/* 版式:定宽阅读栏 + 卡片化信息流。站点通栏列表每行超过 90 字,长读易疲劳。 */
:root {
--heybox-up-page-w: 1360px;
/* 中间栏占视口百分比,可在齿轮设置菜单里调;上限避免超宽屏上正文行过长。 */
--heybox-up-feed-pct: 56;
--heybox-up-feed-w: min(calc(var(--heybox-up-feed-pct) * 1vw), 1180px);
--heybox-up-radius-card: 14px;
--heybox-up-gap: 12px;
--heybox-up-card-pad: 20px;
--heybox-up-shadow-1: 0 1px 2px rgba(16, 24, 40, .05);
--heybox-up-shadow-2: 0 14px 34px rgba(16, 24, 40, .14);
}
:root.heybox-up-theme[data-heybox-up-theme="paper"] {
--heybox-up-radius-card: 4px;
--heybox-up-shadow-1: none;
--heybox-up-shadow-2: 0 10px 24px rgba(47, 42, 34, .12);
}
:root.heybox-up-theme[data-heybox-up-theme="mist"] {
--heybox-up-radius-card: 18px;
--heybox-up-shadow-1: 0 4px 14px rgba(47, 74, 84, .07);
--heybox-up-shadow-2: 0 16px 36px rgba(47, 74, 84, .14);
}
:root.heybox-up-theme[data-heybox-up-theme="oled"] {
--heybox-up-radius-card: 8px;
--heybox-up-shadow-1: none;
--heybox-up-shadow-2: 0 14px 34px rgba(0, 0, 0, .72);
}
@media (min-width: 1200px) {
#app > main {
max-width: var(--heybox-up-page-w) !important;
padding-left: 24px !important;
padding-right: 24px !important;
}
#app > main > section,
#app > main > .layout-normal {
max-width: 100% !important;
}
}
/* 社区首页:移除热门社区/二维码右栏,信息流量成一条阅读栏,宽度够放两张图也不拉长正文行。 */
#page-bbs-community .cpt-right-side.right {
display: none !important;
}
#page-bbs-community .content {
width: auto !important;
max-width: var(--heybox-up-page-w) !important;
margin: 0 auto !important;
justify-content: center !important;
}
#page-bbs-community main.list {
width: auto !important;
max-width: var(--heybox-up-feed-w) !important;
flex: 0 1 var(--heybox-up-feed-w) !important;
min-width: 0;
}
/* 搜索栏跟随阅读栏宽度居中,避免页头比信息流每侧多出 176px。 */
#page-bbs-community .bbs-community__search-module {
width: min(var(--heybox-up-feed-w), 100%) !important;
max-width: 100% !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* 搜索建议加载期间的空壳会留下 26px 浮层,截获首张帖子卡片的点击;无内容时直接收起。 */
#page-bbs-community .search__pull-list:has(> .search__suggestion:empty) {
display: none !important;
}
@media (min-width: 1200px) {
body.heybox-up-rail-open #page-bbs-community .content {
justify-content: flex-start !important;
}
body.heybox-up-rail-open #page-bbs-community main.list {
margin-left: 0 !important;
margin-right: auto !important;
}
}
#page-bbs-community .hb-bbs-home,
#page-bbs-community .bbs-home__content-list {
width: auto !important;
max-width: none !important;
flex: 1 1 auto !important;
min-width: 0;
}
/* 卡片化:帖子之间留间隙与细边,滚动时条目边界一眼可辨。 */
#page-bbs-community .bbs-home__content-list {
display: flex !important;
flex-direction: column;
gap: var(--heybox-up-gap);
padding: 4px 0 28px !important;
}
#page-bbs-community .bbs-home__content-item {
margin: 0 !important;
padding: 0 !important;
border: 1px solid var(--heybox-up-border) !important;
border-radius: var(--heybox-up-radius-card) !important;
background-color: var(--heybox-up-surface) !important;
box-shadow: var(--heybox-up-shadow-1);
overflow: hidden;
transition: border-color .16s ease, box-shadow .16s ease;
}
#page-bbs-community .bbs-home__content-item:hover {
border-color: var(--heybox-up-raised) !important;
box-shadow: var(--heybox-up-shadow-2);
}
#page-bbs-community .bbs-home__content-item > a.hb-cpt__bbs-list-content {
display: block;
padding: var(--heybox-up-card-pad) !important;
background: transparent !important;
}
#page-bbs-community :is(.hb-bbs-home__feed-splitline, .hb-bbs-home__splitline)::before,
#page-bbs-community :is(.hb-bbs-home__feed-splitline, .hb-bbs-home__splitline)::after {
background: transparent !important;
}
#page-bbs-community .bbs-home__topic-list-wrapper {
margin: 0 0 var(--heybox-up-gap) !important;
padding: 10px 14px !important;
border: 1px solid var(--heybox-up-border) !important;
border-radius: var(--heybox-up-radius-card) !important;
background-color: var(--heybox-up-surface) !important;
overflow: hidden;
}
/* 卡片内排版:标题 17/1.5,正文 15/1.72,元信息压到 13px,层级靠字号与颜色拉开。 */
#page-bbs-community .bbs-content__title {
margin: 10px 0 6px !important;
font-size: 17px !important;
line-height: 1.5 !important;
font-weight: 600;
letter-spacing: .005em;
}
#page-bbs-community .bbs-content__content {
font-size: 15px !important;
line-height: 1.72 !important;
color: var(--heybox-up-text-2) !important;
}
#page-bbs-community .bbs-list-content__header {
gap: 8px;
}
#page-bbs-community .list-content__username {
font-size: 13px !important;
font-weight: 500;
}
#page-bbs-community .bbs-content__imgs-wrapper {
margin-top: 10px !important;
}
#page-bbs-community .bbs-content__image {
border-radius: 10px !important;
}
#page-bbs-community .bbs-new-style-bottom {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--heybox-up-border);
}
/* 窄屏:卡片化会吃掉内边距,收回固定宽度与内边距,信息流改用整幅。 */
@media (max-width: 1100px) {
:root {
--heybox-up-feed-w: min(960px, 100%);
--heybox-up-card-pad: 16px;
}
}
/* 评论预览侧栏:展开时由桌面栅格为预览保留真实宽度,侧栏本身在 Shadow DOM 内自绘。 */
:root { --heybox-up-rail-w: 320px; }
@media (max-width: 1399px) {
:root { --heybox-up-rail-w: 288px; }
}
#page-bbs-community :is(.bbs-new-style-bottom__comment, .content-list__comment) {
cursor: pointer;
}
/* 评论排序只改 order,不动 DOM,避免打断站点自身的列表更新。 */
#page-bbs-link .link-comment__list {
display: flex !important;
flex-direction: column;
}
/* 帖子详情:正文纸张 + 原生评论侧栏,两块各自成卡片,中间留 24px 空隙。 */
@media (min-width: 1200px) {
#page-bbs-link .content {
width: auto !important;
max-width: var(--heybox-up-page-w) !important;
margin: 0 auto !important;
gap: 24px;
align-items: flex-start;
justify-content: center;
}
#page-bbs-link .list {
max-width: none !important;
flex: 1 1 auto !important;
min-width: 0;
}
#page-bbs-link .cpt-right-side.right {
display: none !important;
}
#page-bbs-link .hb-cpt__scroll-list.hb-bbs-link__container {
width: auto !important;
max-width: none !important;
}
#page-bbs-link .hb-bbs-link__content {
display: grid !important;
grid-template-columns: minmax(0, 1fr) 424px;
gap: 24px;
align-items: start;
width: auto !important;
max-width: none !important;
}
#page-bbs-link .hb-bbs-image-text {
grid-column: 1;
min-width: 0;
padding: 28px 32px !important;
border: 1px solid var(--heybox-up-border) !important;
border-radius: var(--heybox-up-radius-card) !important;
background-color: var(--heybox-up-surface) !important;
box-shadow: var(--heybox-up-shadow-1);
/* 正文与评论各自滚动:正文列吸顶并限高,滚到底不带走整页。 */
position: sticky;
top: 88px;
max-height: calc(100vh - 112px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-gutter: stable;
}
#page-bbs-link .link-comment {
grid-column: 2;
position: sticky;
top: 88px;
height: calc(100vh - 112px);
max-height: calc(100vh - 112px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
width: 100% !important;
margin: 0 !important;
border: 1px solid var(--heybox-up-border) !important;
border-radius: var(--heybox-up-radius-card) !important;
background-color: var(--heybox-up-surface) !important;
box-shadow: var(--heybox-up-shadow-1);
}
#page-bbs-link .link-comment .comment__comment-header {
top: 0 !important;
background-color: var(--heybox-up-surface) !important;
}
}
/* 正文:16px/1.85 行高,段落限 46em,连续阅读不串行。 */
#page-bbs-link :is(.post__title, .image-text__title, .section-title__content) {
font-size: 25px !important;
line-height: 1.42 !important;
font-weight: 700;
letter-spacing: -.01em;
}
#page-bbs-link :is(.post__content, .image-text__content) {
font-size: 16px !important;
line-height: 1.85 !important;
}
#page-bbs-link :is(.post__content, .image-text__content) :is(p, blockquote, h1, h2, h3, h4) {
max-width: 46em;
}
#page-bbs-link :is(.post__content, .image-text__content) p {
margin: 0 0 .9em !important;
}
#page-bbs-link :is(.post__content, .image-text__content) h2 {
margin: 1.4em 0 .6em !important;
font-size: 20px !important;
line-height: 1.5 !important;
}
#page-bbs-link :is(.post__content, .image-text__content) h3 {
margin: 1.2em 0 .5em !important;
font-size: 17px !important;
line-height: 1.55 !important;
}
#page-bbs-link :is(.post__content, .image-text__content) blockquote {
margin: 1em 0 !important;
padding: 2px 0 2px 14px !important;
border-left: 3px solid var(--heybox-up-accent) !important;
color: var(--heybox-up-muted) !important;
}
#page-bbs-link :is(.post__content, .image-text__content) :is(img, .hb-cpt__image) {
border-radius: 10px !important;
}
/* 评论区:14px 正文,楼层用细线分隔,嵌套回复保留站点结构只改圆角与间距。 */
#page-bbs-link .link-comment__comment-item {
padding: 14px 16px !important;
border-top: 1px solid var(--heybox-up-border) !important;
}
#page-bbs-link :is(.comment-item__content, .children-item__comment-content) {
font-size: 14px !important;
line-height: 1.7 !important;
}
#page-bbs-link .link-comment__comment-children {
margin-top: 8px !important;
border-radius: 10px !important;
}
@media (max-width: 1199px) {
#page-bbs-link .content {
flex-direction: column;
}
#page-bbs-link .list {
width: 100% !important;
max-width: none !important;
}
#page-bbs-link .cpt-right-side.right {
display: none !important;
}
#page-bbs-link .hb-cpt__scroll-list.hb-bbs-link__container,
#page-bbs-link .hb-bbs-link__content {
width: auto !important;
max-width: none !important;
}
#page-bbs-link .hb-bbs-link__content {
display: flex;
flex-direction: column;
}
#page-bbs-link .link-comment {
width: 100% !important;
margin: 16px 0 0 !important;
}
}
/* 站点未覆盖的容器补齐主题底色:嵌套回复块、个人主页列表。 */
:root.heybox-up-theme .link-comment__comment-children {
background-color: var(--heybox-up-subtle) !important;
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme #page-user-profile > .content > .list {
background-color: var(--heybox-up-surface) !important;
border-color: var(--heybox-up-border) !important;
color: var(--heybox-up-text);
}
/* 站点用伪元素画页面底色与帖子分隔条,颜色不随变量走,需单独覆盖。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#app > main > section, #page-bbs-community .hb-bbs-home__feed-splitline, #page-bbs-community .hb-bbs-home__splitline)::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#app > main > section, #page-bbs-community .hb-bbs-home__feed-splitline, #page-bbs-community .hb-bbs-home__splitline)::after {
background-color: var(--heybox-up-bg) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-bbs-community main.list, #page-bbs-link main.list)::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(#page-bbs-community main.list, #page-bbs-link main.list)::after {
background-color: var(--heybox-up-surface) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .hb-cpt__moment-list-content::after {
background-color: var(--heybox-up-subtle) !important;
}
/* 站点用伪元素画卡片底部 4px 色带,取色固定为近白,任何主题下都会形成白条。 */
:root.heybox-up-theme .hb-bbs-image-text::after,
:root.heybox-up-theme .hb-bbs-post::after,
:root.heybox-up-theme .hb-bbs__video::after {
background-color: transparent !important;
background-image: none !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .link-reply::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] .link-comment__comment-item + .link-comment__comment-item:not(:hover)::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] .link-comment__comment-children .comment-children__load-all::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] .hb-cpt__slide-tab.underline::after,
:root.heybox-up-theme[data-heybox-up-dark="true"] .hb-cpt__slide-tab.line::after,
:root.heybox-up-theme[data-heybox-up-dark="true"] .related-recommend__link-item--content::before,
:root.heybox-up-theme[data-heybox-up-dark="true"] .related-recommend__link-item--topic::before {
background-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .link-comment__comment-children .comment-children-item:hover::after {
background-color: var(--heybox-up-subtle) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .link-reply .link-reply__main-box .link-reply__input-wrapper::after {
border-color: var(--heybox-up-border) !important;
}
/* 正文内嵌的 com-* 卡片族(游戏/专辑/百科/比赛/模组/角标/等)底色写死为浅色,
深色下会整块发白。统一按容器、描边、主次文字、药丸底、占位块五组覆盖。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.com-game-card, .com-album-card, .com-award-table-card, .com-chatroom-card, .com-league-card, .com-match-card, .com-mod-card, .com-sku-card, .com-wiki-card) {
background-color: var(--heybox-up-subtle) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.com-game-card, .com-album-card, .com-award-table-card, .com-chatroom-card, .com-league-card, .com-match-card, .com-mod-card, .com-sku-card, .com-wiki-card).is-editor-mode,
:root.heybox-up-theme[data-heybox-up-dark="true"] .com-card-placeholder {
background-color: var(--heybox-up-surface) !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .com-card-placeholder svg path {
fill: var(--heybox-up-muted) !important;
}
/* 卡片描边用 1px 边框乘 0.5 缩放绘制,颜色写死,深色下成亮边。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.com-game-card, .com-album-card, .com-award-table-card, .com-chatroom-card, .com-league-card, .com-match-card, .com-mod-card, .com-sku-card, .com-wiki-card)::after {
border-color: var(--heybox-up-border) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.com-award-table-card .award-table-row, .com-award-table-card .award-table-row.title, .com-award-table-card .award-table-row:last-of-type) {
border-color: var(--heybox-up-border) !important;
}
/* 主文字:写死 #14191e。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(
.com-game-card .game-info .line-1,
.com-game-card .game-info .line-2 .price-box .price-content-box,
.com-game-card .game-info .line-2 .price-box .price-content-box .coupon-tip,
.com-game-card .game-info .line-2 .follow-box .follow-num,
.com-album-card .album-info .line-1 .album-name,
.com-award-table-card .award-table-row,
.com-chatroom-card .info-section .chatroom-title,
.com-league-card .league-info-box .line-1 .league-title,
.com-match-card .team-1 .team-name,
.com-match-card .team-2 .team-name,
.com-match-card .match-info-wrapper .line-2 .aftermatch .team-score.weight,
.com-match-card .match-info-wrapper .line-2 .watched,
.com-match-card .match-info-wrapper .line-2 .commingsoon,
.com-mod-card .mod-card-info .mod-card-title,
.com-sku-card .sku-info-box .line-1,
.com-sku-card .sku-info-box .line-2 .price-box .price-content-box,
.com-wiki-card .info-section .info-line .words-num
) {
color: var(--heybox-up-text) !important;
}
/* 次要文字:写死 #8c9196 与 #64696e。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(
.com-game-card .game-info .line-2 .price-box .price-content-box .release-desc,
.com-game-card .game-info .line-2 .price-box .price-content-box .release-time,
.com-game-card .game-info .line-2 .price-box .price-content-box .original-price,
.com-game-card .game-info .line-2 .follow-box .had-tag,
.com-game-card .game-info .line-2 .follow-box i,
.com-album-card .delete-desc,
.com-album-card .album-info .line-2 .game-num,
.com-award-table-card .award-table-row.unfold,
.com-chatroom-card .info-section .line-2 .default-desc,
.com-chatroom-card .info-section .line-2 .online-count,
.com-chatroom-card .info-section .line-2 .room-count,
.com-league-card .league-info-box .line-2 .league-date,
.com-match-card .match-info-wrapper .line-1,
.com-match-card .match-info-wrapper .line-2,
.com-match-card .match-info-wrapper .line-3,
.com-match-card .match-info-wrapper .line-2 .aftermatch .colon,
.com-mod-card .mod-card-info .mod-card-summary,
.com-sku-card .sku-info-box .line-2 .price-box .price-content-box .original-price,
.com-wiki-card .info-section .info-line .words-num-desc,
.com-wiki-card .info-section .info-line .enter-wiki,
.com-wiki-card .info-section .info-line i
) {
color: var(--heybox-up-muted) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .com-game-card .game-info .line-2 .price-box .price-content-box .original-price::after {
background-color: var(--heybox-up-muted) !important;
}
/* 药丸/标签底:写死 #f3f4f5,配深色文字,深色下变成亮块加暗字。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(
.com-game-card .game-info .line-2 .price-box,
.com-game-card .game-info .line-2 .follow-box,
.com-album-card .delete-album,
.com-album-card .album-info .line-2 .favour-btn.favoured,
.com-award-table-card .award-table-row.title,
.com-award-table-card .award-table-row.unfold,
.com-league-card .league-info-box .line-2 .league-date,
.com-league-card .league-info-box .line-2 .price-pool,
.com-match-card .match-info-wrapper .line-2 .aftermatch .team-score,
.com-match-card .match-info-wrapper .line-2 .watched,
.com-match-card .match-info-wrapper .line-2 .commingsoon,
.com-match-card .match-info-wrapper .line-2 .aftermatch .colon,
.com-sku-card .sku-info-box .line-2 .price-box
) {
background-color: var(--heybox-up-raised) !important;
}
/* 深色实心按钮:写死 #14191e 或同色渐变,深色下与卡片底几乎融为一体。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(
.com-game-card .game-info .line-2 .follow-btn,
.com-game-card .game-info .line-2 .wishlist-box,
.com-game-card .game-info .line-2 .follow-box i,
.com-game-card .game-info .line-2 .follow-box .follow-unit,
.com-game-card .game-info .line-2 .wishlist-box i,
.com-game-card .game-info .line-2 .wishlist-box .add-wishlist-text,
.com-album-card .album-info .line-1 .album-label,
.com-album-card .album-info .line-2 .favour-btn,
.com-match-card .match-info-wrapper .line-2 .unwatched,
.com-mod-card .mod-card-download,
.com-mod-card .mod-card-download .mod-card-download-text,
.com-sku-card .sku-info-box .line-2 .follow-btn
) {
background-color: var(--heybox-up-raised) !important;
background-image: none !important;
color: var(--heybox-up-text) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .com-album-card .album-info .line-2 .favour-btn.favoured {
color: var(--heybox-up-text-2) !important;
}
/* 图片占位底与未加载图片的灰块。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] :is(.com-album-card .game-img-album, .com-mod-card .mod-card-image-box) {
background-color: var(--heybox-up-raised) !important;
}
/* 评论区与列表底部的写死浅色小件。 */
:root.heybox-up-theme[data-heybox-up-dark="true"] .comment-item__writer-like span {
background-color: var(--heybox-up-raised) !important;
color: var(--heybox-up-text-2) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .children-item__comment-creator {
color: var(--heybox-up-accent) !important;
}
:root.heybox-up-theme[data-heybox-up-dark="true"] .scroll-list__no-more-desc::before {
background-color: var(--heybox-up-border) !important;
}
/* 文章管理页:列表末尾的“暂无更多内容”会打断卡片流,直接隐藏。 */
#page-creator-content-management .scroll-list__no-more-desc {
display: none !important;
}
/* 收藏与文章页:右栏只剩二维码、推广与备案信息,移除后让主列表占满内容区。 */
:is(#page-user-favour, #page-creator-content-management, #page-creator-content-management-detail)
.cpt-right-side.right {
display: none !important;
}
:is(#page-user-favour, #page-creator-content-management, #page-creator-content-management-detail)
.content > .list {
flex: 1 1 auto !important;
width: auto !important;
max-width: none !important;
}
/* Global search page: right rail holds only hot searches, a QR code, app links and ICP text. */
#page-bbs-list .cpt-right-side.right {
display: none !important;
}
#page-bbs-list .content > .list {
flex: 1 1 0 !important;
width: auto !important;
min-width: 0 !important;
max-width: none !important;
}
/* The native control anchors to the content box, so it drifts under rails and off the viewport. */
.scroll-list__button-group {
right: 20px !important;
bottom: 24px !important;
left: auto !important;
}
#page-bbs-community .scroll-list__button-group,
#page-topic-link .scroll-list__button-group,
#page-bbs-link .scroll-list__button-group {
display: none !important;
}
`;
const COMMENT_SORTS = [
{ id: "default", label: "默认" },
{ id: "time", label: "时间" },
{ id: "likes", label: "点赞" },
{ id: "author", label: "作者" },
];
const COMMENT_SORT_HOST_ID = "heybox-up-comment-sort";
const COMMENT_SORT_CSS = `
:host {
display: block;
margin: 0 !important;
padding: 9px 12px !important;
border-bottom: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1)) !important;
background: var(--heybox-up-surface, #fff) !important;
font: 12px/1.4 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif !important;
}
.bar { display: flex; align-items: center; gap: 8px; }
.group {
display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px;
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
}
.spacer { flex: 1 1 auto; }
button {
padding: 4px 10px; border: 0; border-radius: 999px;
background: transparent; color: var(--heybox-up-muted, #6b7280);
font: inherit; cursor: pointer;
transition: color .12s ease, background-color .12s ease;
}
button:hover { color: var(--heybox-up-text, #14191e); }
button:focus-visible { outline: 2px solid var(--heybox-up-accent, #6f8db3); outline-offset: 1px; }
.group button[aria-pressed="true"] {
background: var(--heybox-up-surface, #fff);
color: var(--heybox-up-text, #14191e);
font-weight: 600;
box-shadow: 0 1px 2px rgba(16, 24, 40, .14);
}
.cy {
padding: 4px 9px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
}
.cy[aria-pressed="true"] {
border-color: transparent;
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
color: var(--heybox-up-accent, #6f8db3);
font-weight: 600;
}
`;
function parseCommentAge(text) {
const value = text.trim();
if (value === "" || value === "刚刚") return value === "刚刚" ? 0 : Number.POSITIVE_INFINITY;
if (value === "昨天") return 86400;
const relative = /^(\d+)\s*(秒|分钟|小时|天)前$/.exec(value);
if (relative) {
const unit = { 秒: 1, 分钟: 60, 小时: 3600, 天: 86400 }[relative[2]];
return Number(relative[1]) * unit;
}
const parsed = Date.parse(value.replace(/-/g, "/"));
return Number.isNaN(parsed) ? Number.POSITIVE_INFINITY : (Date.now() - parsed) / 1000;
}
function applyCommentSort(tree, mode) {
const list = tree.querySelector(".link-comment__list");
if (!list) return;
const items = Array.from(list.children).filter((node) => node.classList.contains("link-comment__comment-item"));
if (mode === "default") {
for (const item of items) item.style.order = "";
return;
}
const authorName = (document.querySelector("#page-bbs-link .link-user__username")?.textContent || "").trim();
const keyed = items.map((item, index) => {
const likes = Number((item.querySelector(".like-box__cnt")?.textContent || "0").trim()) || 0;
const age = parseCommentAge(item.querySelector(".info-box__create-time")?.textContent || "");
const name = (item.querySelector(".info-box__username")?.textContent || "").trim();
return { item, index, likes, age, isAuthor: authorName !== "" && name === authorName };
});
keyed.sort((a, b) => {
if (mode === "likes") return b.likes - a.likes || a.index - b.index;
if (mode === "time") return a.age - b.age || a.index - b.index;
if (mode === "author") return Number(b.isAuthor) - Number(a.isAuthor) || a.index - b.index;
return a.index - b.index;
});
keyed.forEach((entry, index) => { entry.item.style.order = String(index); });
}
function createCommentSort(tree, mode = "default", isHideCy = () => false, onToggleCy = () => {}) {
const host = document.createElement("div");
host.id = COMMENT_SORT_HOST_ID;
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = COMMENT_SORT_CSS;
const bar = document.createElement("div");
bar.className = "bar";
const group = document.createElement("div");
group.className = "group";
group.setAttribute("role", "group");
group.setAttribute("aria-label", "评论排序");
bar.append(group);
let current = mode;
const buttons = COMMENT_SORTS.map((sort) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = sort.label;
button.dataset.sort = sort.id;
button.setAttribute("aria-pressed", String(sort.id === current));
button.addEventListener("click", () => {
current = sort.id;
for (const node of buttons) node.setAttribute("aria-pressed", String(node.dataset.sort === current));
applyCommentSort(tree, current);
});
group.append(button);
return button;
});
const spacer = document.createElement("span");
spacer.className = "spacer";
const cyButton = document.createElement("button");
cyButton.type = "button";
cyButton.className = "cy";
cyButton.textContent = "屏蔽 CY";
cyButton.setAttribute("aria-pressed", String(isHideCy()));
cyButton.title = isHideCy() ? "显示 CY 评论" : "屏蔽 CY 评论";
cyButton.addEventListener("click", () => onToggleCy());
bar.append(spacer, cyButton);
shadow.append(style, bar);
tree.prepend(host);
return host;
}
function watchCommentSort() {
if (!document.body) return;
let host = null;
let mode = "default";
let scheduled = false;
let cyLinkId = "";
let cyPage = 0;
let cyDone = false;
let cyLoading = false;
let cyIds = new Set();
let cyScanned = new Set();
const getLinkId = () => {
const match = /link\/(\d+)/.exec(location.pathname);
return match ? match[1] : "";
};
const toggleCy = () => {
setHideCyPreference(!readHideCyPreference());
};
const updateCyButton = () => {
const button = host?.shadowRoot?.querySelector("button.cy");
if (!button) return;
const hideCy = readHideCyPreference();
button.setAttribute("aria-pressed", String(hideCy));
button.title = hideCy ? "显示 CY 评论" : "屏蔽 CY 评论";
};
const markCy = () => {
const tree = document.querySelector("#page-bbs-link .link-comment");
if (!tree) return;
let count = 0;
const counted = new Set();
for (const node of tree.querySelectorAll("[data-comment-id]")) {
const id = node.getAttribute("data-comment-id") || "";
if (cyIds.has(id)) {
if (node.getAttribute("data-heybox-up-cy") !== "1") node.setAttribute("data-heybox-up-cy", "1");
if (!counted.has(id)) {
counted.add(id);
count += 1;
}
} else if (node.hasAttribute("data-heybox-up-cy")) {
node.removeAttribute("data-heybox-up-cy");
}
}
const button = host?.shadowRoot?.querySelector("button.cy");
if (!button) return;
const hidden = readHideCyPreference() ? count : 0;
button.textContent = hidden > 0 ? "屏蔽 CY (" + hidden + ")" : "屏蔽 CY";
};
const pullCyIds = async () => {
if (cyLoading || cyDone) return;
const linkId = getLinkId();
const tree = document.querySelector("#page-bbs-link .link-comment");
if (linkId === "" || !tree) return;
const hasUnknown = () => Array.from(tree.querySelectorAll("[data-comment-id]"))
.some((node) => {
const id = node.getAttribute("data-comment-id") || "";
return id !== "" && !cyScanned.has(id);
});
if (!hasUnknown()) return;
cyLoading = true;
try {
// 只补未识别的楼层,最多 10 页,避免长楼反复翻页触发接口风控。
while (!cyDone && cyPage < 10 && getLinkId() === linkId) {
const data = await fetchRailPage(linkId, cyPage + 1);
cyPage += 1;
let added = 0;
for (const group of data.groups) {
const list = Array.isArray(group.comment) ? group.comment : [];
for (const raw of list) {
const id = String((raw && (raw.commentid || raw.comment_id)) || "");
if (id !== "" && !cyScanned.has(id)) {
cyScanned.add(id);
added += 1;
}
if (id !== "" && isCyComment(raw)) cyIds.add(id);
}
}
if (added === 0) cyDone = true;
if (!hasUnknown()) break;
}
if (cyPage >= 10) cyDone = true;
} catch (error) {
// 接口失败时保持原生评论可见,不误伤正常楼层。
cyDone = true;
} finally {
cyLoading = false;
markCy();
}
};
const sync = () => {
if (!location.pathname.startsWith(LINK_HREF_PREFIX)) {
host = null;
mode = "default";
return;
}
const tree = document.querySelector("#page-bbs-link .link-comment");
if (!tree) return;
const linkId = getLinkId();
if (linkId !== cyLinkId) {
cyLinkId = linkId;
cyPage = 0;
cyDone = false;
cyIds = new Set();
cyScanned = new Set();
}
const active = host?.shadowRoot?.querySelector('button[data-sort][aria-pressed="true"]');
if (active) mode = active.dataset.sort || "default";
if (!host || !host.isConnected || host.parentElement !== tree) {
host = createCommentSort(tree, mode, readHideCyPreference, toggleCy);
}
applyCommentSort(tree, mode);
updateCyButton();
markCy();
void pullCyIds();
};
const schedule = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
};
new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true });
document.addEventListener(HIDE_CY_EVENT, () => {
updateCyButton();
markCy();
});
sync();
}
const THEME_ENTRY_CSS = `
:host { display: inline-flex; flex: none; position: relative; gap: 2px; }
.button {
display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
border: 0; border-radius: 8px; background: transparent;
color: var(--heybox-up-icon, #62676e); cursor: pointer;
}
.button:hover {
background: var(--heybox-up-icon-hover-bg, rgba(20, 25, 30, .06));
color: var(--heybox-up-icon-hover, #14191e);
}
.button:focus-visible {
outline: 2px solid var(--heybox-up-accent, #6f8db3);
outline-offset: 2px;
}
svg { display: block; width: 18px; height: 18px; }
.menu {
position: absolute; top: 38px; right: 0; z-index: 1000; width: 236px; padding: 6px;
border: 1px solid var(--hbu-menu-border, rgba(20, 25, 30, .12)); border-radius: 12px;
background: var(--hbu-menu-bg, #fff); color: var(--hbu-menu-text, #14191e);
box-shadow: 0 14px 34px rgba(0, 0, 0, .18);
}
.theme-menu { right: 34px; }
.menu[hidden] { display: none; }
.menu-title {
padding: 6px 10px 4px; font-size: 11px; letter-spacing: .08em;
color: var(--hbu-menu-muted, #6b7280);
}
.item {
display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 8px;
border: 0; border-radius: 8px; background: transparent; color: inherit;
font: inherit; text-align: left; cursor: pointer;
}
.item:hover, .item:focus-visible { background: var(--hbu-menu-hover, rgba(20, 25, 30, .06)); outline: none; }
.item[aria-checked="true"] { background: var(--hbu-menu-hover, rgba(20, 25, 30, .06)); }
.swatch {
flex: none; width: 28px; height: 28px; border-radius: 7px;
border: 1px solid var(--hbu-menu-border, rgba(20, 25, 30, .12));
background: linear-gradient(135deg, var(--a) 0 55%, var(--b) 55%);
}
.copy { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.name { font-size: 13px; line-height: 1.3; }
.hint { font-size: 11px; line-height: 1.3; color: var(--hbu-menu-muted, #6b7280); }
.check { margin-left: auto; font-size: 12px; color: var(--heybox-up-accent, #6f8db3); opacity: 0; }
.item[aria-checked="true"] .check { opacity: 1; }
.sep { height: 1px; margin: 6px 4px; background: var(--hbu-menu-border, rgba(20, 25, 30, .12)); }
.field { padding: 2px 8px 6px; }
.field-label { padding: 0 2px 6px; font-size: 11px; letter-spacing: .08em; color: var(--hbu-menu-muted, #6b7280); }
.chips { display: flex; gap: 4px; }
.chip {
flex: 1 1 0; min-width: 0; padding: 5px 0; border: 1px solid var(--hbu-menu-border, rgba(20, 25, 30, .12));
border-radius: 7px; background: transparent; color: inherit; font: inherit; font-size: 12px;
font-variant-numeric: tabular-nums; text-align: center; cursor: pointer;
transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.chip:hover { background: var(--hbu-menu-hover, rgba(20, 25, 30, .06)); }
.chip:focus-visible { outline: 2px solid var(--heybox-up-accent, #6f8db3); outline-offset: 1px; }
.chip[aria-checked="true"] {
border-color: var(--heybox-up-accent, #6f8db3);
color: var(--heybox-up-accent, #6f8db3);
background: var(--hbu-menu-hover, rgba(20, 25, 30, .06));
}
.item.toggle[aria-checked="true"] { background: transparent; }
.switch {
position: relative; flex: none; margin-left: auto; width: 30px; height: 16px; border-radius: 999px;
border: 1px solid var(--hbu-menu-border, rgba(20, 25, 30, .12));
background: var(--hbu-menu-hover, rgba(20, 25, 30, .06));
transition: border-color .15s ease;
}
.switch::after {
content: ""; position: absolute; top: 1px; left: 2px; width: 12px; height: 12px; border-radius: 50%;
background: var(--hbu-menu-muted, #6b7280);
transition: transform .15s ease, background .15s ease;
}
.item[aria-checked="true"] .switch { border-color: var(--heybox-up-accent, #6f8db3); }
.item[aria-checked="true"] .switch::after { transform: translateX(13px); background: var(--heybox-up-accent, #6f8db3); }
`;
const THEME_ICONS = {
system: '',
dark: '',
oled: '',
light: '',
paper: '',
mist: '',
};
const SETTINGS_ICON = '';
function ensureThemeStyle() {
let style = document.getElementById("heybox-up-theme-style");
if (!style) {
style = document.createElement("style");
style.id = "heybox-up-theme-style";
style.textContent = THEME_CSS;
} else if (style.textContent !== THEME_CSS) {
/* 脚本热更新后复用旧节点会导致旧样式残留,按当前版本重写。 */
style.textContent = THEME_CSS;
}
const parent = document.head || document.documentElement;
if (!parent) return;
if (style.parentNode !== parent || parent === document.head && document.head.lastElementChild !== style) {
parent.append(style);
}
}
function updateThemeEntry() {
if (!themeEntry) return;
const preset = getThemePreset(currentThemeMode);
themeEntry.button.innerHTML = THEME_ICONS[currentThemeMode];
themeEntry.button.title = "主题:" + preset.label + "(点击选择)";
themeEntry.button.setAttribute("aria-label", "主题:" + preset.label + ",点击选择");
themeEntry.button.setAttribute("aria-expanded", String(!themeEntry.menu.hidden));
themeEntry.settingsButton.setAttribute("aria-expanded", String(!themeEntry.settingsMenu.hidden));
for (const item of themeEntry.items) {
item.node.setAttribute("aria-checked", String(item.id === currentThemeMode));
}
if (previewToggle) previewToggle.setAttribute("aria-checked", String(readCommentPreviewPreference()));
if (navMotionToggle) navMotionToggle.setAttribute("aria-checked", String(readNavMotionPreference()));
syncFeedWidthChips(readFeedWidth());
}
function applyTheme(mode, persist) {
currentThemeMode = normalizeThemeMode(mode);
const effective = resolveTheme(currentThemeMode, THEME_MEDIA.matches);
const preset = getThemePreset(effective);
const root = document.documentElement;
root.classList.add(THEME_CLASS);
root.setAttribute(THEME_ATTR, effective);
root.setAttribute("data-heybox-up-dark", String(preset.dark === true));
root.style.colorScheme = preset.dark === true ? "dark" : "light";
ensureThemeStyle();
if (persist) writeThemeMode(currentThemeMode);
updateThemeEntry();
if (preset.dark === true) scheduleDarkSweep();
else clearDarkSweep();
}
function createThemeEntry() {
const host = document.createElement("span");
host.id = THEME_ENTRY_ID;
host.className = "heybox-up-theme-entry";
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = THEME_ENTRY_CSS;
const button = document.createElement("button");
button.type = "button";
button.className = "button";
button.setAttribute("aria-haspopup", "menu");
button.setAttribute("aria-expanded", "false");
const settingsButton = document.createElement("button");
settingsButton.type = "button";
settingsButton.className = "button";
settingsButton.setAttribute("aria-haspopup", "menu");
settingsButton.setAttribute("aria-expanded", "false");
settingsButton.setAttribute("aria-label", "打开设置");
settingsButton.title = "设置";
settingsButton.innerHTML = SETTINGS_ICON;
const menu = document.createElement("div");
menu.className = "menu theme-menu";
menu.hidden = true;
menu.setAttribute("role", "menu");
menu.setAttribute("aria-label", "主题预设");
menu.append(createThemeMenuTitle());
const settingsMenu = document.createElement("div");
settingsMenu.className = "menu settings-menu";
settingsMenu.hidden = true;
settingsMenu.setAttribute("role", "menu");
settingsMenu.setAttribute("aria-label", "设置");
settingsMenu.append(createSettingsMenuTitle());
const items = THEME_PRESETS.map((preset) => {
const node = document.createElement("button");
node.type = "button";
node.className = "item";
node.dataset.theme = preset.id;
node.setAttribute("role", "menuitemradio");
node.setAttribute("aria-checked", "false");
const swatch = document.createElement("span");
swatch.className = "swatch";
swatch.style.setProperty("--a", preset.swatch[0]);
swatch.style.setProperty("--b", preset.swatch[1]);
const copy = document.createElement("span");
copy.className = "copy";
const name = document.createElement("span");
name.className = "name";
name.textContent = preset.label;
const hint = document.createElement("span");
hint.className = "hint";
hint.textContent = preset.hint;
copy.append(name, hint);
const check = document.createElement("span");
check.className = "check";
check.textContent = "✓";
node.append(swatch, copy, check);
node.addEventListener("click", () => { applyTheme(preset.id, true); closeThemeMenu(); button.focus(); });
return { id: preset.id, node };
});
menu.append(...items.map((item) => item.node));
settingsMenu.append(
createCommentPreviewToggle(),
createNavMotionToggle(),
createThemeMenuSeparator(),
createFeedWidthControl()
);
const closeThemeMenu = () => {
if (menu.hidden) return;
menu.hidden = true;
button.setAttribute("aria-expanded", "false");
updateThemeEntry();
};
const closeSettingsMenu = () => {
if (settingsMenu.hidden) return;
settingsMenu.hidden = true;
settingsButton.setAttribute("aria-expanded", "false");
updateThemeEntry();
};
button.addEventListener("click", () => {
closeSettingsMenu();
menu.hidden = !menu.hidden;
button.setAttribute("aria-expanded", String(!menu.hidden));
updateThemeEntry();
});
settingsButton.addEventListener("click", () => {
closeThemeMenu();
settingsMenu.hidden = !settingsMenu.hidden;
settingsButton.setAttribute("aria-expanded", String(!settingsMenu.hidden));
updateThemeEntry();
});
document.addEventListener("click", (event) => {
if ((menu.hidden && settingsMenu.hidden) || event.composedPath().includes(host)) return;
closeThemeMenu();
closeSettingsMenu();
}, true);
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || (menu.hidden && settingsMenu.hidden)) return;
event.preventDefault();
const returnTarget = settingsMenu.hidden ? button : settingsButton;
closeThemeMenu();
closeSettingsMenu();
returnTarget.focus();
});
shadow.append(style, button, settingsButton, menu, settingsMenu);
themeEntry = { host, button, menu, items, settingsButton, settingsMenu };
updateThemeEntry();
}
function createThemeMenuTitle() {
const title = document.createElement("div");
title.className = "menu-title";
title.textContent = "主题预设";
return title;
}
function createSettingsMenuTitle() {
const title = document.createElement("div");
title.className = "menu-title";
title.textContent = "设置";
return title;
}
function createThemeMenuSeparator() {
const line = document.createElement("div");
line.className = "sep";
line.setAttribute("role", "separator");
return line;
}
// 首页评论预览开关:放在齿轮设置菜单里,偏好只写本机 localStorage。
function createCommentPreviewToggle() {
const node = document.createElement("button");
node.type = "button";
node.className = "item toggle";
node.setAttribute("role", "menuitemcheckbox");
node.setAttribute("aria-checked", String(readCommentPreviewPreference()));
const copy = document.createElement("span");
copy.className = "copy";
const name = document.createElement("span");
name.className = "name";
name.textContent = "首页评论预览";
const hint = document.createElement("span");
hint.className = "hint";
hint.textContent = "信息流右侧直接看评论";
copy.append(name, hint);
const toggle = document.createElement("span");
toggle.className = "switch";
node.append(copy, toggle);
node.addEventListener("click", () => {
const next = !(node.getAttribute("aria-checked") === "true");
node.setAttribute("aria-checked", String(next));
setCommentPreviewPreference(next);
});
previewToggle = node;
return node;
}
// 动态导航图标开关:关闭后图标保持静态,悬停与激活都不再播放动画。
function createNavMotionToggle() {
const node = document.createElement("button");
node.type = "button";
node.className = "item toggle";
node.setAttribute("role", "menuitemcheckbox");
node.setAttribute("aria-checked", String(readNavMotionPreference()));
const copy = document.createElement("span");
copy.className = "copy";
const name = document.createElement("span");
name.className = "name";
name.textContent = "动态导航图标";
const hint = document.createElement("span");
hint.className = "hint";
hint.textContent = "悬停时播放轻量动画";
copy.append(name, hint);
const toggle = document.createElement("span");
toggle.className = "switch";
node.append(copy, toggle);
node.addEventListener("click", () => {
const next = !(node.getAttribute("aria-checked") === "true");
node.setAttribute("aria-checked", String(next));
setNavMotionPreference(next);
});
navMotionToggle = node;
return node;
}
// 中栏宽度:按视口百分比分段,只改 CSS 变量,不重排 DOM。
const FEED_WIDTH_STEPS = [40, 48, 56, 64, 72];
function syncFeedWidthChips(value) {
for (const chip of feedWidthChips) {
chip.setAttribute("aria-checked", String(Number(chip.dataset.width) === value));
}
}
function createFeedWidthControl() {
const wrap = document.createElement("div");
wrap.className = "field";
wrap.setAttribute("role", "group");
wrap.setAttribute("aria-label", "中栏宽度");
const label = document.createElement("div");
label.className = "field-label";
label.textContent = "中栏宽度(占屏幕宽度)";
const chips = document.createElement("div");
chips.className = "chips";
for (const step of FEED_WIDTH_STEPS) {
const chip = document.createElement("button");
chip.type = "button";
chip.className = "chip";
chip.dataset.width = String(step);
chip.textContent = step + "%";
chip.title = "中栏占屏幕宽度 " + step + "%";
chip.setAttribute("role", "menuitemradio");
chip.setAttribute("aria-checked", "false");
chip.addEventListener("click", () => {
setFeedWidthPreference(step);
syncFeedWidthChips(step);
});
chips.append(chip);
}
wrap.append(label, chips);
feedWidthChips = Array.from(chips.children);
syncFeedWidthChips(readFeedWidth());
return wrap;
}
function mountThemeEntry() {
if (!document.body) return;
const actions = document.querySelector(".nav-actions");
if (!actions) return;
if (!themeEntry) createThemeEntry();
if (themeEntry.host.parentNode !== actions) {
const anchor = actions.querySelector(".publish-btn, .login-btn");
actions.insertBefore(themeEntry.host, anchor);
}
}
function watchTheme() {
if (!document.body) return;
let scheduled = false;
const sync = () => {
ensureThemeStyle();
mountThemeEntry();
};
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
scheduleDarkSweep();
});
}).observe(document.body, { childList: true, subtree: true });
sync();
scheduleDarkSweep();
}
// 站点把不少底色直接写在组件里,选择器一变就漏出白块。深色主题下对这些页面做一次兜底扫描:
// 只给近白底色、近白描边与过暗文字挂类,不改 DOM 结构;类一旦挂上就保留,避免与 hover 反复拉锯。
const DARK_SWEEP_ROOT_SELECTOR =
"#page-creator-content-management, #page-creator-content-management-detail, #page-topic-link";
const DARK_FLAT_CLASS = "heybox-up-dark-flat";
const DARK_FLAT_SUBTLE_CLASS = "heybox-up-dark-flat-subtle";
const DARK_EDGE_CLASS = "heybox-up-dark-edge";
const DARK_INK_CLASS = "heybox-up-dark-ink";
const DARK_SWEEP_LIMIT = 3000;
const DARK_SWEEP_SKIP = new Set([
"IMG", "VIDEO", "CANVAS", "SVG", "PATH", "USE", "IFRAME",
"INPUT", "TEXTAREA", "SELECT", "OPTION", "SCRIPT", "STYLE", "LINK",
]);
const RGB_COLOR = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/;
let darkSweepTimer = 0;
function parseRgbColor(value) {
const match = RGB_COLOR.exec(value || "");
if (match === null) return null;
return {
r: Number(match[1]),
g: Number(match[2]),
b: Number(match[3]),
a: match[4] === undefined ? 1 : Number(match[4]),
};
}
function isNearWhite(color, min) {
return color !== null && color.a >= 0.6 && (color.r + color.g + color.b) / 3 >= min;
}
function hasOwnText(node) {
for (const child of node.childNodes) {
if (child.nodeType === 3 && child.nodeValue.trim() !== "") return true;
}
return false;
}
// 找到自己的第一层不透明背景祖先(含自身),用来判断文字是否落在被兜底改暗的底色上。
function flatBackgroundAncestor(node) {
for (let el = node; el; el = el.parentElement) {
if (el.classList.contains(DARK_FLAT_CLASS) || el.classList.contains(DARK_FLAT_SUBTLE_CLASS)) return true;
const color = parseRgbColor(getComputedStyle(el).backgroundColor);
if (color !== null && color.a >= 0.6) return false;
}
return false;
}
function clearDarkSweep() {
const selector = "." + DARK_FLAT_CLASS + ", ." + DARK_FLAT_SUBTLE_CLASS + ", ." + DARK_EDGE_CLASS + ", ." + DARK_INK_CLASS;
for (const node of document.querySelectorAll(selector)) {
node.classList.remove(DARK_FLAT_CLASS, DARK_FLAT_SUBTLE_CLASS, DARK_EDGE_CLASS, DARK_INK_CLASS);
}
}
function sweepDarkSurfaces() {
if (document.documentElement.getAttribute("data-heybox-up-dark") !== "true") {
clearDarkSweep();
return;
}
const roots = document.querySelectorAll(DARK_SWEEP_ROOT_SELECTOR);
if (roots.length === 0) return;
for (const root of roots) {
const nodes = root.querySelectorAll("*");
const total = Math.min(nodes.length, DARK_SWEEP_LIMIT);
for (let index = 0; index < total; index += 1) {
const node = nodes[index];
if (DARK_SWEEP_SKIP.has(node.tagName) || node.id.startsWith("heybox-up")) continue;
const style = getComputedStyle(node);
if (!node.classList.contains(DARK_FLAT_CLASS) && !node.classList.contains(DARK_FLAT_SUBTLE_CLASS)) {
if (style.backgroundImage === "none") {
const bg = parseRgbColor(style.backgroundColor);
if (isNearWhite(bg, 240)) {
node.classList.add(isNearWhite(bg, 250) ? DARK_FLAT_CLASS : DARK_FLAT_SUBTLE_CLASS);
}
}
}
if (!node.classList.contains(DARK_EDGE_CLASS) && styleHasBrightEdge(style)) {
node.classList.add(DARK_EDGE_CLASS);
}
if (!node.classList.contains(DARK_INK_CLASS) && hasOwnText(node)) {
const ink = parseRgbColor(style.color);
if (ink !== null && (ink.r + ink.g + ink.b) / 3 < 120 && flatBackgroundAncestor(node)) {
node.classList.add(DARK_INK_CLASS);
}
}
}
}
}
// 只处理真正画出来的描边:宽 0 或 style 为 none 时,border-color 只是 color 的副本,改它没有意义。
function styleHasBrightEdge(style) {
if (style.borderStyle === "none" || style.borderStyle === "") return false;
if (style.borderTopWidth === "0px" && style.borderRightWidth === "0px"
&& style.borderBottomWidth === "0px" && style.borderLeftWidth === "0px") return false;
return isNearWhite(parseRgbColor(style.borderTopColor), 240)
|| isNearWhite(parseRgbColor(style.borderRightColor), 240)
|| isNearWhite(parseRgbColor(style.borderBottomColor), 240)
|| isNearWhite(parseRgbColor(style.borderLeftColor), 240);
}
function scheduleDarkSweep() {
if (darkSweepTimer !== 0) return;
darkSweepTimer = window.setTimeout(() => {
darkSweepTimer = 0;
sweepDarkSurfaces();
}, 350);
}
// 顶部导航只保留四个高频入口,用「SVG + 文本」替换站点原有的七个文字按钮。
const NAV_ID = "heybox-up-nav";
const NAV_STYLE_ID = "heybox-up-nav-style";
const NAV_STATIC_CLASS = "heybox-up-nav-static";
const CY_HASH = "#heybox-up-cy";
const NAV_ITEMS = [
{ id: "home", label: "首页", href: "/" },
{ id: "community", label: "社区", href: "/app/bbs/home" },
{ id: "favour", label: "收藏", href: "/app/user/favour/content" },
{ id: "article", label: "文章", href: "/creator/content_management/home?article_type=all" },
];
const NAV_ICONS = {
home: '',
community: '',
favour: '',
article: '',
};
const NAV_CSS = `
.nav-links > .nav-link { display: none !important; }
#${NAV_ID} {
display: flex;
align-items: stretch;
height: 64px;
gap: 2px;
margin-left: 2px;
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
}
.hbu-nav-link {
position: relative;
display: inline-flex;
align-items: center;
gap: 7px;
height: 100%;
padding: 0 12px;
border-radius: 8px;
color: var(--nav-link-color, var(--heybox-up-muted, #8c9198));
text-decoration: none;
white-space: nowrap;
transition: color .18s ease;
}
.hbu-nav-link::after {
content: "";
position: absolute;
left: 12px;
right: 12px;
bottom: 11px;
height: 2px;
border-radius: 2px;
background: var(--heybox-up-accent, #6f8db3);
transform: scaleX(0);
transform-origin: 50% 50%;
transition: transform .2s ease;
}
.hbu-nav-link:hover { color: var(--nav-link-active-color, var(--heybox-up-text, #14191e)); }
.hbu-nav-link:hover::after, .hbu-nav-link.is-active::after { transform: scaleX(1); }
.hbu-nav-link.is-active {
color: var(--nav-link-active-color, var(--heybox-up-text, #14191e));
font-weight: 600;
}
.hbu-nav-link:focus-visible {
outline: 2px solid var(--heybox-up-accent, #6f8db3);
outline-offset: -2px;
}
.hbu-nav-link svg { width: 18px; height: 18px; flex: none; }
.hbu-nav-link .hbu-ico-anim { transform-box: fill-box; transform-origin: 50% 60%; }
.hbu-nav-link:hover .hbu-ico-home, .hbu-nav-link:focus-visible .hbu-ico-home, .hbu-nav-link.is-active .hbu-ico-home { animation: hbu-nav-home .55s cubic-bezier(.3, .7, .3, 1); }
.hbu-nav-link:hover .hbu-ico-community, .hbu-nav-link:focus-visible .hbu-ico-community, .hbu-nav-link.is-active .hbu-ico-community { animation: hbu-nav-community .55s cubic-bezier(.3, .7, .3, 1); }
.hbu-nav-link:hover .hbu-ico-favour, .hbu-nav-link:focus-visible .hbu-ico-favour, .hbu-nav-link.is-active .hbu-ico-favour { animation: hbu-nav-favour .55s cubic-bezier(.3, .7, .3, 1); }
.hbu-nav-link:hover .hbu-ico-article, .hbu-nav-link:focus-visible .hbu-ico-article, .hbu-nav-link.is-active .hbu-ico-article { animation: hbu-nav-article .55s cubic-bezier(.3, .7, .3, 1); }
@keyframes hbu-nav-home {
0%, 100% { transform: translateY(0); }
38% { transform: translateY(-1.8px); }
72% { transform: translateY(.8px); }
}
@keyframes hbu-nav-community {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-2px); }
76% { transform: translateY(.6px); }
}
@keyframes hbu-nav-favour {
0%, 100% { transform: scaleY(1); }
46% { transform: scaleY(.8) translateY(-1px); }
}
@keyframes hbu-nav-article {
0%, 100% { transform: rotate(0); }
38% { transform: rotate(-4deg); }
72% { transform: rotate(1.5deg); }
}
:root.${NAV_STATIC_CLASS} .hbu-nav-link svg * { animation: none !important; }
@media (prefers-reduced-motion: reduce) {
.hbu-nav-link svg * { animation: none !important; }
.hbu-nav-link::after { transition: none; }
}
@media (max-width: 1180px) {
.hbu-nav-link { padding: 0 9px; gap: 5px; font-size: 13px; }
.hbu-nav-link::after { left: 9px; right: 9px; }
}
@media (max-width: 940px) {
.hbu-nav-link span { display: none; }
.hbu-nav-link { padding: 0 10px; }
}
`;
function applyNavMotionPreference(value) {
document.documentElement.classList.toggle(NAV_STATIC_CLASS, value === false);
}
function ensureNavStyle() {
let style = document.getElementById(NAV_STYLE_ID);
if (!style) {
style = document.createElement("style");
style.id = NAV_STYLE_ID;
style.textContent = NAV_CSS;
} else if (style.textContent !== NAV_CSS) {
style.textContent = NAV_CSS;
}
const parent = document.head || document.documentElement;
if (!parent) return;
if (style.parentNode !== parent) parent.append(style);
}
function readNavActiveId() {
const path = location.pathname;
if (path === "/app/user/favour/content") return "favour";
if (path === "/") return "home";
if (path.startsWith("/creator/")) return "article";
if (path.startsWith("/app/bbs/")) return "community";
return "";
}
function syncNavActive() {
const host = document.getElementById(NAV_ID);
if (!host) return;
const activeId = readNavActiveId();
for (const node of host.querySelectorAll(".hbu-nav-link")) {
const active = node.dataset.nav === activeId;
node.classList.toggle("is-active", active);
if (active) node.setAttribute("aria-current", "page");
else node.removeAttribute("aria-current");
}
}
function scrollToCySection() {
const host = document.getElementById("heybox-up-favour");
if (!host) return false;
host.scrollIntoView({ behavior: "smooth", block: "start" });
return true;
}
function readSiteRouter() {
const root = document.querySelector("#app");
const app = root ? root.__vue_app__ : null;
const properties = app ? app.config.globalProperties : null;
const router = properties ? properties.$router : null;
return router && typeof router.push === "function" ? router : null;
}
// 站点是 Vue 单页应用:脚本合成点击的 带 isTrusted=false,路由会忽略它并整页刷新。
// 这里优先调用站点的 router.push,取不到时退回 点击,最坏情况只是整页加载。
function navigateToPath(path) {
const router = readSiteRouter();
if (router) {
try {
const pending = router.push(path);
if (pending && typeof pending.catch === "function") pending.catch(() => {});
return;
} catch (error) {
// 路由内部状态异常时退回原生跳转。
}
}
const anchor = document.createElement("a");
anchor.href = path;
anchor.style.display = "none";
document.body.append(anchor);
anchor.click();
anchor.remove();
}
function handleNavClick(event) {
const target = event.target;
if (!(target instanceof Element)) return;
const link = target.closest("a.hbu-nav-link");
if (!link) return;
// 组合键或中键交给浏览器,保留新标签页打开的能力。
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const id = link.dataset.nav;
// 收藏页内再点收藏时清掉旧锚点,回到列表顶部。
if (id === "favour" && location.pathname === "/app/user/favour/content" && location.hash !== "") {
event.preventDefault();
history.pushState(null, "", "/app/user/favour/content");
window.scrollTo({ top: 0, behavior: "smooth" });
syncNavActive();
return;
}
// 导航栏挂在站点根节点之外,原生 点击会整页刷新;统一交给 navigateToPath 走站内路由。
event.preventDefault();
const href = link.getAttribute("href");
if (href !== null && href !== "") navigateToPath(href);
}
function mountTopNav() {
if (!document.body) return;
const links = document.querySelector(".nav .nav-links") || document.querySelector(".nav-links");
if (!links) return;
ensureNavStyle();
let host = document.getElementById(NAV_ID);
if (!host) {
host = document.createElement("div");
host.id = NAV_ID;
host.setAttribute("role", "navigation");
host.setAttribute("aria-label", "站内导航");
host.innerHTML = NAV_ITEMS.map((item) =>
'' + NAV_ICONS[item.id] + "" + item.label + ""
).join("");
host.addEventListener("click", handleNavClick);
}
if (host.parentNode !== links || links.firstElementChild !== host) links.prepend(host);
syncNavActive();
}
function watchTopNav() {
if (!document.body) return;
let scheduled = false;
const sync = () => {
mountTopNav();
syncNavActive();
};
window.addEventListener("hashchange", syncNavActive);
window.addEventListener("popstate", syncNavActive);
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
}).observe(document.body, { childList: true, subtree: true });
sync();
}
THEME_MEDIA.addEventListener("change", () => {
if (currentThemeMode === "system") applyTheme("system", false);
});
applyTheme(readThemeMode(), false);
applyFeedWidth(readFeedWidth());
document.addEventListener(NAV_MOTION_EVENT, (event) => applyNavMotionPreference(event.detail === true));
applyNavMotionPreference(readNavMotionPreference());
// ==sign-core:start==
const SIG_ALPHABET = "AB45STUVWZEFGJ6CH01D237IXYPQRKLMN89";
const MD5_SHIFTS = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
const MD5_TABLE = Array.from({ length: 64 }, (_, index) =>
Math.floor(Math.abs(Math.sin(index + 1)) * 0x100000000) >>> 0
);
function toUtf8Bytes(text) {
const bytes = [];
for (const char of text) {
const point = char.codePointAt(0);
if (point < 0x80) {
bytes.push(point);
} else if (point < 0x800) {
bytes.push(0xc0 | (point >> 6), 0x80 | (point & 0x3f));
} else if (point < 0x10000) {
bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
} else {
bytes.push(
0xf0 | (point >> 18),
0x80 | ((point >> 12) & 0x3f),
0x80 | ((point >> 6) & 0x3f),
0x80 | (point & 0x3f)
);
}
}
return bytes;
}
function rotateLeft(value, shift) {
return ((value << shift) | (value >>> (32 - shift))) >>> 0;
}
function md5Hex(text) {
const bytes = toUtf8Bytes(text);
const bitLength = bytes.length * 8;
bytes.push(0x80);
while (bytes.length % 64 !== 56) bytes.push(0);
const high = Math.floor(bitLength / 0x100000000) >>> 0;
const low = bitLength >>> 0;
for (const word of [low, high]) {
bytes.push(word & 0xff, (word >>> 8) & 0xff, (word >>> 16) & 0xff, (word >>> 24) & 0xff);
}
let stateA = 0x67452301;
let stateB = 0xefcdab89;
let stateC = 0x98badcfe;
let stateD = 0x10325476;
for (let offset = 0; offset < bytes.length; offset += 64) {
const words = [];
for (let index = 0; index < 16; index += 1) {
const base = offset + index * 4;
words.push(
(bytes[base] | (bytes[base + 1] << 8) | (bytes[base + 2] << 16) | (bytes[base + 3] << 24)) >>> 0
);
}
let a = stateA;
let b = stateB;
let c = stateC;
let d = stateD;
for (let index = 0; index < 64; index += 1) {
let mix;
let slot;
if (index < 16) {
mix = (b & c) | (~b & d);
slot = index;
} else if (index < 32) {
mix = (d & b) | (~d & c);
slot = (5 * index + 1) % 16;
} else if (index < 48) {
mix = b ^ c ^ d;
slot = (3 * index + 5) % 16;
} else {
mix = c ^ (b | ~d);
slot = (7 * index) % 16;
}
const sum = (a + mix + MD5_TABLE[index] + words[slot]) >>> 0;
a = d;
d = c;
c = b;
b = (b + rotateLeft(sum, MD5_SHIFTS[index])) >>> 0;
}
stateA = (stateA + a) >>> 0;
stateB = (stateB + b) >>> 0;
stateC = (stateC + c) >>> 0;
stateD = (stateD + d) >>> 0;
}
let hex = "";
for (const word of [stateA, stateB, stateC, stateD]) {
for (let index = 0; index < 4; index += 1) {
hex += ((word >>> (index * 8)) & 0xff).toString(16).padStart(2, "0");
}
}
return hex;
}
// endIndex < 0 时取字母表末尾若干位,其余情况用整表。
function mapByAlphabet(value, endIndex) {
const source = endIndex < 0 ? SIG_ALPHABET.slice(0, endIndex) : SIG_ALPHABET;
let mapped = "";
for (let index = 0; index < value.length; index += 1) {
mapped += source[value.charCodeAt(index) % source.length];
}
return mapped;
}
function interleave(parts) {
const longest = Math.max(...parts.map((part) => part.length));
let merged = "";
for (let index = 0; index < longest; index += 1) {
for (const part of parts) {
if (index < part.length) merged += part[index];
}
}
return merged;
}
function xtime(value) {
// 不能对左移结果截断:服务端算法保留了高位,截断会算出不同校验位。
return value & 0x80 ? ((value << 1) ^ 0x1b) & 0xff : value << 1;
}
// 就地改写前 4 字节并返回整个数组:服务端求和时把末尾 2 字节也算进去。
function mixColumns(values) {
const quartic = (value) => xtime(value) ^ value;
const r = (value) => quartic(xtime(value));
const y = (value) => r(quartic(xtime(value)));
const g = (value) => y(value) ^ r(value) ^ quartic(value);
const [v0, v1, v2, v3] = values;
values[0] = g(v0) ^ y(v1) ^ r(v2) ^ quartic(v3);
values[1] = quartic(v0) ^ g(v1) ^ y(v2) ^ r(v3);
values[2] = r(v0) ^ quartic(v1) ^ g(v2) ^ y(v3);
values[3] = y(v0) ^ r(v1) ^ quartic(v2) ^ g(v3);
return values;
}
function createSignature(path, time, nonce) {
const normalizedPath = "/" + path.split("/").filter(Boolean).join("/") + "/";
const seed = interleave([
mapByAlphabet(String(time + 1), -2),
mapByAlphabet(normalizedPath, 0),
mapByAlphabet(nonce, 0),
]).slice(0, 20);
const digest = md5Hex(seed);
const tail = digest.slice(-6).split("").map((char) => char.charCodeAt(0));
const checksum = String(mixColumns(tail).reduce((total, value) => total + value, 0) % 100).padStart(2, "0");
return mapByAlphabet(digest.slice(0, 5), -4) + checksum;
}
// ==sign-core:end==
function readCookie(name) {
const prefix = name + "=";
for (const part of document.cookie.split(";")) {
const entry = part.trim();
if (entry.startsWith(prefix)) return decodeURIComponent(entry.slice(prefix.length));
}
return "";
}
function getHeyboxId() {
return readCookie("heybox_id") || readCookie("user_heybox_id");
}
function buildCyApiUrl(userId, offset) {
const time = Math.floor(Date.now() / 1000);
const nonce = md5Hex(String(time) + String(Math.random())).toUpperCase();
const hkey = createSignature(CY_API_PATH, time, nonce);
const query = new URLSearchParams({
os_type: "web",
app: "heybox",
client_type: "web",
version: "999.0.4",
web_version: "2.5",
x_client_type: "web",
x_app: "heybox_website",
heybox_id: userId,
x_os_type: "Windows",
device_info: "Chrome",
hkey: hkey,
_time: String(time),
nonce: nonce,
userid: userId,
only_cy: "1",
filter_type: "0",
enable_new_style_collect: "1",
limit: String(CY_PAGE_SIZE),
offset: String(offset),
no_more: "false",
});
return "https://api.xiaoheihe.cn" + CY_API_PATH + "?" + query.toString();
}
async function fetchCyPage(userId, offset) {
const response = await fetch(buildCyApiUrl(userId, offset), {
credentials: "include",
headers: { accept: "application/json" },
});
if (!response.ok) throw new Error("接口返回 HTTP " + response.status);
const data = await response.json();
if (data.status !== "ok") throw new Error(data.msg || "接口返回失败状态");
const list = Array.isArray(data.result) ? data.result : [];
return { list, noMore: data.no_more === true || list.length === 0 };
}
function normalizeCyItem(raw) {
const link = raw && raw.link;
if (!link || link.id === undefined || link.id === null) return null;
const thumbs = Array.isArray(link.thumbs) ? link.thumbs : [];
return {
id: String(link.id),
title: typeof link.title === "string" && link.title.trim() !== "" ? link.title.trim() : "无标题帖子",
text: typeof raw.text === "string" ? raw.text.trim() : "",
createAt: Number(raw.create_at) || 0,
thumb: typeof thumbs[0] === "string" ? thumbs[0] : "",
kind: raw.comment_type === 1 ? "回复" : "评论",
};
}
async function loadCyFavorites(userId, onProgress) {
const seen = new Set();
const items = [];
let offset = 0;
for (;;) {
const page = await fetchCyPage(userId, offset);
for (const raw of page.list) {
const item = normalizeCyItem(raw);
if (item && !seen.has(item.id)) {
seen.add(item.id);
items.push(item);
}
}
onProgress(items.length);
if (page.noMore || items.length >= CY_MAX_ITEMS) break;
offset += CY_PAGE_SIZE;
if (offset >= CY_MAX_ITEMS + CY_PAGE_SIZE) break;
}
return items.slice(0, CY_MAX_ITEMS);
}
function readCache(userId) {
try {
const raw = localStorage.getItem(CACHE_KEY_PREFIX + userId);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.items)) return null;
if (Date.now() - Number(parsed.savedAt) > CACHE_TTL_MS) return null;
return parsed.items;
} catch (error) {
console.warn("[HeyBox Up] 读取缓存失败", error);
return null;
}
}
function writeCache(userId, items) {
try {
localStorage.setItem(CACHE_KEY_PREFIX + userId, JSON.stringify({ savedAt: Date.now(), items: items }));
} catch (error) {
console.warn("[HeyBox Up] 写入缓存失败", error);
}
}
function formatTime(value) {
if (!value) return "";
const milliseconds = value > 1e12 ? value : value * 1000;
const date = new Date(milliseconds);
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleString("zh-CN", { hour12: false });
}
function createCard(item) {
const card = document.createElement("a");
card.className = "card";
card.href = LINK_HREF_PREFIX + item.id;
card.target = "_blank";
card.rel = "noopener noreferrer";
if (item.thumb !== "") {
const thumb = document.createElement("img");
thumb.className = "thumb";
thumb.src = item.thumb;
thumb.alt = "";
thumb.loading = "lazy";
thumb.referrerPolicy = "no-referrer";
card.append(thumb);
}
const body = document.createElement("div");
body.className = "body";
const cardTitle = document.createElement("div");
cardTitle.className = "card-title";
cardTitle.textContent = item.title;
const cardText = document.createElement("div");
cardText.className = item.text !== "" ? "card-text" : "card-text is-empty";
if (item.text === "") {
cardText.textContent = "(无文字内容)";
} else {
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("class", "card-text-icon");
icon.setAttribute("viewBox", "0 0 16 16");
icon.setAttribute("aria-hidden", "true");
const iconPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
iconPath.setAttribute("fill", "currentColor");
iconPath.setAttribute("d", "M8 3C4.5 3 1.8 5.5 1 8c.8 2.5 3.5 5 7 5s6.2-2.5 7-5c-.8-2.5-3.5-5-7-5Zm0 8.2A3.2 3.2 0 1 1 8 4.8a3.2 3.2 0 0 1 0 6.4Zm0-1.7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
icon.append(iconPath);
const content = document.createElement("span");
content.className = "card-text-body";
content.textContent = item.text;
cardText.append(icon, content);
cardText.title = item.text;
}
const foot = document.createElement("div");
foot.className = "card-foot";
const kind = document.createElement("span");
kind.textContent = "插眼" + item.kind;
const time = document.createElement("span");
time.textContent = formatTime(item.createAt);
foot.append(kind, time);
body.append(cardTitle, cardText, foot);
card.append(body);
return card;
}
const FAVOUR_PATH = "/app/user/favour/content";
const FAVOUR_HOST_ID = "heybox-up-favour";
// shortcut: 依赖站点类名 .user-favour__list 及其父级 .list,站点改版时需升级。
const FAVOUR_LIST_SELECTOR = ".user-favour__list";
const FAVOUR_CSS = `
:host { display: block; scroll-margin-top: 80px; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
.section { padding: 14px 8px 4px; }
.head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.heading { font-size: 14px; font-weight: 700; }
.refresh, .retry {
font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 8px;
border: 1px solid rgba(127,127,127,.32); background: transparent; color: inherit; cursor: pointer;
}
.refresh:hover:not(:disabled), .retry:hover { border-color: rgba(127,127,127,.55); }
.refresh:disabled { opacity: .5; cursor: default; }
.retry { margin: -2px 0 10px; }
.note { font-size: 12px; opacity: .62; margin-bottom: 10px; }
.search {
width: 100%; box-sizing: border-box; margin-bottom: 10px; padding: 7px 10px;
border-radius: 8px; border: 1px solid rgba(127,127,127,.28);
background: rgba(127,127,127,.08); color: inherit; font: inherit; font-size: 13px;
}
.search:focus { outline: none; border-color: #6f8db3; }
.card {
display: flex; gap: 10px; padding: 10px; margin-bottom: 8px;
border-radius: 12px; text-decoration: none; color: inherit;
border: 1px solid rgba(127,127,127,.22);
}
.card:hover { border-color: rgba(127,127,127,.45); }
.thumb { width: 52px; height: 52px; border-radius: 9px; object-fit: cover; flex: none; background: rgba(127,127,127,.16); }
.body { min-width: 0; flex: 1; }
.card-title {
font-weight: 600; font-size: 13px; margin-bottom: 3px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card-text {
display: flex; gap: 6px; align-items: flex-start; margin-bottom: 6px;
font-size: 14px; line-height: 1.5; color: inherit;
}
.card-text-icon { width: 15px; height: 15px; flex: none; margin-top: 2px; color: #6f8db3; }
.card-text-body { min-width: 0; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.card-text.is-empty { font-size: 12px; opacity: .55; }
.card-foot { margin-top: 5px; font-size: 11px; opacity: .6; display: flex; gap: 10px; }
`;
function createFavourSection() {
const host = document.createElement("div");
host.id = FAVOUR_HOST_ID;
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = FAVOUR_CSS;
shadow.append(style);
const section = document.createElement("section");
section.className = "section";
const head = document.createElement("div");
head.className = "head";
const heading = document.createElement("div");
heading.className = "heading";
heading.textContent = "插眼收藏";
const refresh = document.createElement("button");
refresh.type = "button";
refresh.className = "refresh";
refresh.textContent = "刷新";
refresh.setAttribute("aria-label", "刷新插眼收藏");
head.append(heading, refresh);
const note = document.createElement("div");
note.className = "note";
note.textContent = "正在读取插眼收藏…";
const retry = document.createElement("button");
retry.type = "button";
retry.className = "retry";
retry.textContent = "重试";
retry.hidden = true;
const search = document.createElement("input");
search.type = "search";
search.className = "search";
search.placeholder = "搜索标题或插眼内容";
search.setAttribute("aria-label", "搜索插眼收藏");
search.hidden = true;
const list = document.createElement("div");
list.className = "list";
section.append(head, note, retry, search, list);
shadow.append(section);
const state = { items: [], loading: false, error: "", notice: "", progress: 0, query: "", signature: "" };
const readPageIds = () => {
const ids = new Set();
for (const node of document.querySelectorAll(FAVOUR_LIST_SELECTOR + ' a[href*="/bbs/link/"]')) {
const match = /\/bbs\/link\/(\d+)/.exec(node.getAttribute("href") || "");
if (match) ids.add(match[1]);
}
return ids;
};
const render = () => {
const pageIds = readPageIds();
state.signature = Array.from(pageIds).sort().join(",");
const visible = state.items.filter((item) => !pageIds.has(item.id));
const query = state.query.trim();
const needle = query.toLowerCase();
const matched = needle === ""
? visible
: visible.filter((item) => item.title.toLowerCase().includes(needle) || item.text.toLowerCase().includes(needle));
list.replaceChildren();
refresh.disabled = state.loading;
refresh.textContent = state.loading ? "读取中…" : "刷新";
retry.hidden = state.error === "";
search.hidden = state.items.length === 0;
if (state.error !== "") {
note.textContent = "读取失败:" + state.error + (state.items.length > 0 ? "(显示上次结果)" : "");
} else if (state.notice !== "") {
note.textContent = state.notice;
} else if (state.loading) {
note.textContent = "正在读取插眼收藏…" + (state.progress > 0 ? " 已获取 " + state.progress + " 条" : "");
} else if (state.items.length === 0) {
note.textContent = "没有插眼收藏。";
} else if (visible.length === 0) {
note.textContent = "插眼条目已全部出现在列表中。";
} else if (matched.length === 0) {
note.textContent = "没有匹配“" + query + "”的插眼条目。";
} else {
note.textContent = "共 " + visible.length + " 条插眼收藏" + (query !== "" ? ",匹配 " + matched.length + " 条" : "") + ",网页收藏列表默认不显示。";
}
if (matched.length === 0) return;
const fragment = document.createDocumentFragment();
for (const item of matched) fragment.append(createCard(item));
list.append(fragment);
};
const place = () => {
const listNode = document.querySelector(FAVOUR_LIST_SELECTOR);
const parent = listNode ? listNode.parentElement : null;
if (!listNode || !parent) return;
if (host.parentNode !== parent || host.previousElementSibling !== listNode) {
parent.insertBefore(host, listNode.nextSibling);
}
if (Array.from(readPageIds()).sort().join(",") !== state.signature) render();
};
const load = async (force) => {
if (state.loading) return;
const userId = getHeyboxId();
if (userId === "") {
state.notice = "未登录小黑盒,无法读取插眼收藏。";
render();
return;
}
state.loading = true;
state.error = "";
state.notice = "";
state.progress = 0;
render();
try {
const cached = force ? null : readCache(userId);
if (cached) {
state.items = cached;
return;
}
state.items = await loadCyFavorites(userId, (count) => {
state.progress = count;
note.textContent = "正在读取插眼收藏… 已获取 " + count + " 条";
});
writeCache(userId, state.items);
} catch (error) {
state.error = error instanceof Error ? error.message : String(error);
} finally {
state.loading = false;
render();
}
};
refresh.addEventListener("click", () => void load(true));
retry.addEventListener("click", () => void load(true));
search.addEventListener("input", () => {
state.query = search.value;
render();
});
place();
void load(false);
return { host, place };
}
function watchFavourPage() {
if (!document.body) return;
let section = null;
let scheduled = false;
let cyJumpDone = false;
const sync = () => {
if (location.pathname !== FAVOUR_PATH || getHeyboxId() === "") {
cyJumpDone = false;
if (section) {
section.host.remove();
section = null;
}
return;
}
if (!section) section = createFavourSection();
section.place();
if (!cyJumpDone && location.hash === CY_HASH) {
requestAnimationFrame(() => {
if (!scrollToCySection()) return;
cyJumpDone = true;
});
}
};
const schedule = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
};
new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true });
sync();
}
const RAIL_ID = "heybox-up-comment-rail";
const COMMENT_TREE_API_PATH = "/bbs/app/link/tree";
const RAIL_PAGE_SIZE = 20;
// 预览只读:回复、点赞与楼中楼展开仍走帖子页的原生评论区。
const RAIL_TRIGGER_SELECTOR = ".bbs-new-style-bottom__comment, .content-list__comment";
const RAIL_ICONS = {
comment: '',
external: '',
collapse: '',
};
const RAIL_CSS = `
[hidden] { display: none !important; }
:host {
--rail-bg: var(--heybox-up-surface, #fff);
--rail-text: var(--heybox-up-text, #14191e);
--rail-text-2: var(--heybox-up-text-2, #3c4249);
--rail-muted: var(--heybox-up-muted, #6b7280);
--rail-subtle: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
--rail-line: var(--heybox-up-border, rgba(20, 25, 30, .1));
--rail-accent: var(--heybox-up-accent, #6f8db3);
--rail-radius: var(--heybox-up-radius-card, 14px);
position: fixed;
top: 80px;
right: 20px;
width: var(--heybox-up-rail-w, 340px);
display: block;
/* 低于站点导航(5000)与全屏 loading(2002),高于正文与编辑器控件。 */
z-index: 1500;
font: 13px/1.6 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--rail-text);
}
:host([data-state="hidden"]) { display: none; }
@media (max-width: 1199px) { :host { display: none; } }
.panel {
display: flex;
flex-direction: column;
max-height: calc(100vh - 168px);
background: var(--rail-bg);
border: 1px solid var(--rail-line);
border-radius: var(--rail-radius);
box-shadow: var(--heybox-up-shadow-2, 0 18px 44px rgba(8, 12, 16, .24));
overflow: hidden;
}
:host([data-state="collapsed"]) { top: 50%; right: 0; width: auto; transform: translateY(-50%); }
:host([data-state="collapsed"]) .panel { border-radius: var(--rail-radius) 0 0 var(--rail-radius); }
:host([data-state="collapsed"]) .dock { display: none; }
:host([data-state="open"]) .tab { display: none; }
.tab {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
padding: 12px 7px;
border: 0;
background: transparent;
color: var(--rail-muted);
font: inherit;
writing-mode: vertical-rl;
cursor: pointer;
}
.tab:hover { background: var(--rail-subtle); color: var(--rail-text); }
.tab:focus-visible { outline: 2px solid var(--rail-accent); outline-offset: -2px; }
.tab svg { width: 15px; height: 15px; }
.dock { display: flex; flex-direction: column; min-height: 0; }
.head { display: flex; gap: 8px; padding: 12px 14px 10px; border-bottom: 1px solid var(--rail-line); }
.head-text { flex: 1 1 auto; min-width: 0; }
.kicker { font-size: 10px; letter-spacing: .12em; color: var(--rail-muted); }
.title {
margin-top: 3px;
font-size: 13px;
font-weight: 600;
line-height: 1.45;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.tools { display: flex; flex: none; gap: 2px; }
.icon {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border: 0;
border-radius: 7px;
background: transparent;
color: var(--rail-muted);
text-decoration: none;
cursor: pointer;
}
.icon:hover { background: var(--rail-subtle); color: var(--rail-text); }
.icon:focus-visible { outline: 2px solid var(--rail-accent); outline-offset: 1px; }
.icon svg { width: 16px; height: 16px; }
.icon.cy {
width: auto;
min-width: 26px;
padding: 0 5px;
font-size: 10px;
font-weight: 600;
letter-spacing: .04em;
}
.icon.cy[aria-pressed="true"] { background: var(--rail-subtle); color: var(--rail-accent); }
.meta { padding: 8px 14px 0; font-size: 11px; color: var(--rail-muted); }
.list { margin: 0; padding: 8px 0 0; list-style: none; overflow-y: auto; overscroll-behavior: contain; min-height: 0; }
.item { display: flex; gap: 10px; padding: 11px 14px; border-top: 1px solid var(--rail-subtle); }
.item:first-child { border-top: 0; }
.avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; flex: none; background: var(--rail-subtle); }
.body { flex: 1 1 auto; min-width: 0; }
.who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.name { font-size: 12px; font-weight: 600; color: var(--rail-text); }
.time, .likes, .replies { font-size: 11px; color: var(--rail-muted); }
.text { margin: 3px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--rail-text); }
.text .emoji, .quote .emoji {
display: inline-block;
width: 17px;
height: 17px;
margin: 0 1px;
vertical-align: -3px;
background-repeat: no-repeat;
background-size: 102px 306px;
}
.text .pic, .quote .pic {
max-width: 100%;
max-height: 160px;
border-radius: 6px;
object-fit: contain;
vertical-align: top;
}
.foot { display: flex; gap: 10px; margin-top: 4px; }
.sub { display: grid; gap: 4px; margin-top: 7px; padding-left: 9px; border-left: 2px solid var(--rail-subtle); }
.quote {
font-size: 12px;
color: var(--rail-text-2);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.quote .quote-name { color: var(--rail-muted); }
.status { padding: 26px 18px; text-align: center; color: var(--rail-muted); }
.foot-bar { display: flex; gap: 8px; padding: 11px 12px; border-top: 1px solid var(--rail-line); }
.action {
flex: 1 1 auto;
height: 30px;
border: 1px solid var(--rail-line);
border-radius: 8px;
background: transparent;
color: var(--rail-text);
font: inherit;
cursor: pointer;
}
.action:hover { background: var(--rail-subtle); }
.action:focus-visible { outline: 2px solid var(--rail-accent); outline-offset: 1px; }
.action.primary { border-color: transparent; background: var(--rail-accent); color: var(--heybox-up-on-accent, #fff); }
`;
function createApiParams(path) {
const time = Math.floor(Date.now() / 1000);
const nonce = md5Hex(String(time) + String(Math.random())).toUpperCase();
return {
os_type: "web",
app: "heybox",
client_type: "web",
version: "999.0.4",
web_version: "2.5",
x_client_type: "web",
x_app: "heybox_website",
x_os_type: "Windows",
device_info: "Chrome",
hkey: createSignature(path, time, nonce),
_time: String(time),
nonce: nonce,
};
}
function buildLinkCommentUrl(linkId, page) {
const query = new URLSearchParams({
...createApiParams(COMMENT_TREE_API_PATH),
heybox_id: getHeyboxId(),
link_id: String(linkId),
is_first: page === 1 ? "1" : "0",
page: String(page),
index: "1",
limit: String(RAIL_PAGE_SIZE),
owner_only: "0",
});
// h_src 是站点网页端自带的空参数,缺省时接口更容易触发风控。
return "https://api.xiaoheihe.cn" + COMMENT_TREE_API_PATH + "?" + query.toString().replace("&link_id=", "&h_src&link_id=");
}
function normalizeRailText(value) {
return String(value === undefined || value === null ? "" : value)
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 400);
}
// 站点表情是 CSS sprite(),不能当普通标签剥掉,
// 否则纯表情评论在预览里会变成空白。这里把评论正文拆成文本、表情、图片三类片段,
// 渲染时只创建节点,不注入原始 HTML。
const RAIL_BODY_MAX = 4000;
const RAIL_EMOJI_SPRITE = "https://static.max-c.com/heybox_web/emoji/cube/cube_emoji_v33.png";
const RAIL_EMOJI_CELL = 17;
const RAIL_EMOJI_TOKEN = /\[cube_([^\]]{1,64})\]/g;
// 评论接口返回的是 [cube_表情名] 文本,而不是 DOM 里的编号 class。
// 以下顺序与站点 cube 表情清单及 6 列精灵图一致,用来把名称还原成编号。
const RAIL_EMOJI_NAMES = [
"哭泣", "酷", "doge", "喜欢", "黑人问号", "惊讶",
"开心", "捂脸哭", "晕", "感动", "委屈", "并不简单",
"乖", "笑cry", "怒", "滑稽", "沧桑", "凄凉",
"赞", "学习", "叹气", "加油", "摊手", "喷水",
"打脸", "H币", "生气", "困", "闭嘴", "吐",
"咕咕", "微笑", "哇", "汗", "吓", "睡觉",
"2023", "圣诞树", "庆祝", "庆祝-圣诞", "你懂我", "我懂你",
"阳", "比心", "wota", "鹅", "握草", "这是什么鸟",
"上学-乐", "上学-丧", "打咩", "超人", "僵尸", "小鸡",
"窝囊", "摸摸头", "电牛", "摘墨镜", "悟空", "2024",
"良民", "嬉水女王", "2025", "浪人砍一刀", "吃鸡啦", "鸡毙你",
"机智", "石化", "吹口哨", "思考", "太酷啦", "蛋糕",
"毁灭战士", "P的谎言", "剑星伊芙", "欧润吉", "剑星渡鸦", "猛男微笑",
"时间旅者", "来财", "我方了", "喜+1", "2026", "害羞",
"吃瓜", "菜doge", "柠檬", "比耶", "爱心", "玫瑰",
"+1", "-1", "点赞", "盒十", "耶", "鼓掌",
"碰拳", "马年吉祥", "红包", "炒菜", "山姆无奈", "洛的点赞",
"灵光一闪", "露莉亚", "傲娇助手", "认真记录", "鳄鱼", "星乐蒂",
];
const RAIL_EMOJI_NAME_IDS = new Map(
RAIL_EMOJI_NAMES.map((name, index) => [name, String((Math.floor(index / 6) + 1) * 10 + (index % 6) + 1)])
);
// 站点精灵图是 6 列 × 18 行,合法编号的个位是 1–6、十位段是 1–18;
// 编号最大到 186,只认 11–66 会漏掉 7x–18x 这一大批表情。
function isRailEmojiId(value) {
const id = Number(value);
if (!Number.isInteger(id) || id < 11 || id > 186) return false;
const unit = id % 10;
const row = Math.floor(id / 10);
return unit >= 1 && unit <= 6 && row >= 1 && row <= 18;
}
function resolveRailEmojiId(value) {
const raw = String(value === undefined || value === null ? "" : value).trim();
const candidate = raw.startsWith("cube_") ? raw.slice(5) : raw;
if (isRailEmojiId(candidate)) return String(Number(candidate));
return RAIL_EMOJI_NAME_IDS.get(candidate) || "";
}
function parseRailBody(value) {
const source = String(value === undefined || value === null ? "" : value).slice(0, RAIL_BODY_MAX);
if (source === "") return [];
const template = document.createElement("template");
template.innerHTML = source;
const segments = [];
const pushText = (value) => {
const parts = String(value).split(RAIL_EMOJI_TOKEN);
for (let index = 0; index < parts.length; index += 1) {
if (index % 2 === 1) {
const emojiId = resolveRailEmojiId(parts[index]);
if (emojiId !== "") segments.push({ type: "emoji", id: emojiId });
else segments.push({ type: "text", text: "[cube_" + parts[index] + "]" });
continue;
}
const collapsed = parts[index].replace(/\s+/g, " ");
if (collapsed.trim() === "") continue;
segments.push({ type: "text", text: collapsed });
}
};
const walk = (node) => {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
pushText(child.nodeValue);
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) continue;
const element = child;
const emoji = element.getAttribute("data-emoji");
if (emoji !== null && emoji !== "") {
const match = /(?:^|\s)hb-emoji-cube_([^\s]+)/.exec(String(element.className || ""));
const candidates = [match === null ? "" : match[1], emoji];
const id = candidates.map(resolveRailEmojiId).find((item) => item !== "") || "";
if (id !== "") segments.push({ type: "emoji", id: id });
else segments.push({ type: "text", text: "[" + emoji + "]" });
continue;
}
if (element.tagName === "IMG") {
const src = element.getAttribute("src");
if (src !== null && /^https?:\/\//i.test(src)) segments.push({ type: "image", src });
continue;
}
if (element.tagName === "BR") {
pushText(" ");
continue;
}
walk(element);
}
};
walk(template.content);
if (segments.length > 0) {
const first = segments[0];
if (first.type === "text") first.text = first.text.replace(/^\s+/, "");
const last = segments[segments.length - 1];
if (last.type === "text") last.text = last.text.replace(/\s+$/, "");
}
return segments.filter((segment) => segment.type !== "text" || segment.text !== "").slice(0, 80);
}
function railBodyText(segments) {
return segments.filter((segment) => segment.type === "text").map((segment) => segment.text).join(" ").trim();
}
function normalizeRailComment(comment) {
const raw = comment && typeof comment === "object" ? comment : {};
const user = raw.user && typeof raw.user === "object" ? raw.user : {};
const avatar = user.avatar || user.avartar || "";
const body = parseRailBody(raw.text);
return {
id: String(raw.comment_id || raw.commentid || raw.id || ""),
name: String(user.username || user.nickname || "匿名用户").trim(),
avatar: typeof avatar === "string" ? avatar : "",
text: railBodyText(body),
body: body,
createAt: Number(raw.create_at) || 0,
likes: Number(raw.support_num || raw.support_count) || 0,
replies: Number(raw.reply_count || raw.sub_comment_num) || 0,
isCy: isCyComment(raw),
};
}
function normalizeRailGroup(group) {
const list = group && Array.isArray(group.comment) ? group.comment : [];
const root = list[0];
if (!root) return null;
const item = normalizeRailComment(root);
if (item.id === "" && item.body.length === 0 && item.name === "匿名用户") return null;
item.quotes = list.slice(1, 3).map(normalizeRailComment).filter((quote) => quote.body.length > 0);
return item;
}
async function fetchRailPage(linkId, page) {
const response = await fetch(buildLinkCommentUrl(linkId, page), {
credentials: "include",
headers: { accept: "application/json" },
});
if (!response.ok) throw new Error("接口返回 HTTP " + response.status);
const data = await response.json();
if (data.status !== "ok") throw new Error(data.msg || "接口返回 " + data.status);
const result = data.result && typeof data.result === "object" ? data.result : {};
const groups = Array.isArray(result.comments) ? result.comments : [];
const link = result.link && typeof result.link === "object" ? result.link : {};
return {
items: groups.map(normalizeRailGroup).filter((item) => item !== null),
total: Number(link.comment_num || result.total_floor_num) || 0,
title: typeof link.title === "string" ? link.title.trim() : "",
groups,
};
}
function formatRailAge(value) {
const seconds = Math.floor(Date.now() / 1000) - Number(value || 0);
if (!Number.isFinite(seconds) || seconds < 60) return "刚刚";
if (seconds < 3600) return Math.floor(seconds / 60) + " 分钟前";
if (seconds < 86400) return Math.floor(seconds / 3600) + " 小时前";
if (seconds < 86400 * 30) return Math.floor(seconds / 86400) + " 天前";
return formatTime(value).slice(0, 10);
}
function createCommentRail() {
const host = document.createElement("div");
host.id = RAIL_ID;
host.dataset.state = "hidden";
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = RAIL_CSS;
const panel = document.createElement("div");
panel.className = "panel";
const tab = document.createElement("button");
tab.type = "button";
tab.className = "tab";
tab.setAttribute("aria-label", "展开评论预览");
tab.setAttribute("aria-expanded", "false");
tab.innerHTML = RAIL_ICONS.comment + "评论预览";
const dock = document.createElement("div");
dock.className = "dock";
const head = document.createElement("div");
head.className = "head";
const headText = document.createElement("div");
headText.className = "head-text";
const kicker = document.createElement("div");
kicker.className = "kicker";
kicker.textContent = "评论预览";
const titleNode = document.createElement("div");
titleNode.className = "title";
titleNode.textContent = "评论";
headText.append(kicker, titleNode);
const tools = document.createElement("div");
tools.className = "tools";
const cyButton = document.createElement("button");
cyButton.type = "button";
cyButton.className = "icon cy";
cyButton.textContent = "CY";
const openLinkNode = document.createElement("a");
openLinkNode.className = "icon";
openLinkNode.target = "_blank";
openLinkNode.rel = "noopener noreferrer";
openLinkNode.title = "打开帖子";
openLinkNode.setAttribute("aria-label", "打开帖子");
openLinkNode.innerHTML = RAIL_ICONS.external;
const collapseButton = document.createElement("button");
collapseButton.type = "button";
collapseButton.className = "icon";
collapseButton.title = "收起";
collapseButton.setAttribute("aria-label", "收起评论预览");
collapseButton.innerHTML = RAIL_ICONS.collapse;
tools.append(cyButton, openLinkNode, collapseButton);
head.append(headText, tools);
const meta = document.createElement("div");
meta.className = "meta";
const list = document.createElement("ol");
list.className = "list";
const status = document.createElement("div");
status.className = "status";
const footBar = document.createElement("div");
footBar.className = "foot-bar";
const moreButton = document.createElement("button");
moreButton.type = "button";
moreButton.className = "action";
moreButton.textContent = "加载更多";
footBar.append(moreButton);
dock.append(head, meta, list, status, footBar);
panel.append(tab, dock);
shadow.append(style, panel);
const state = {
mode: "hidden",
linkId: "",
title: "",
total: 0,
page: 0,
items: [],
loading: false,
error: "",
hasMore: false,
seq: 0,
};
let hideCy = readHideCyPreference();
const updateCyButton = () => {
cyButton.setAttribute("aria-pressed", String(hideCy));
cyButton.title = hideCy ? "显示 CY 评论" : "屏蔽 CY 评论";
cyButton.setAttribute("aria-label", cyButton.title);
};
cyButton.addEventListener("click", () => {
hideCy = !hideCy;
setHideCyPreference(hideCy);
updateCyButton();
render();
});
document.addEventListener(HIDE_CY_EVENT, () => {
const next = readHideCyPreference();
if (next === hideCy) return;
hideCy = next;
updateCyButton();
render();
});
const applyMode = () => {
host.dataset.state = state.mode;
tab.setAttribute("aria-expanded", String(state.mode === "open"));
if (document.body) document.body.classList.toggle("heybox-up-rail-open", state.mode === "open");
};
const createEmoji = (id) => {
const node = document.createElement("span");
node.className = "emoji";
node.setAttribute("role", "img");
node.setAttribute("aria-label", "表情");
// 编号的十位是精灵图行、个位是列;这里用负坐标裁切,
// 不能照抄站点依赖 background-repeat 回卷的正坐标写法。
const column = (Number(id) % 10) - 1;
const row = Math.floor(Number(id) / 10) - 1;
node.style.backgroundImage = 'url("' + RAIL_EMOJI_SPRITE + '")';
node.style.backgroundPosition = "-" + column * RAIL_EMOJI_CELL + "px -" + row * RAIL_EMOJI_CELL + "px";
return node;
};
const createImage = (src) => {
const node = document.createElement("img");
node.className = "pic";
node.alt = "";
node.loading = "lazy";
node.referrerPolicy = "no-referrer";
node.src = src;
return node;
};
const fillBody = (node, segments) => {
if (segments.length === 0) {
node.textContent = "(图片或表情评论)";
return;
}
for (const segment of segments) {
if (segment.type === "text") node.append(document.createTextNode(segment.text));
else if (segment.type === "emoji") node.append(createEmoji(segment.id));
else node.append(createImage(segment.src));
}
};
const createQuote = (quote) => {
const node = document.createElement("div");
node.className = "quote";
const name = document.createElement("span");
name.className = "quote-name";
name.textContent = quote.name + ":";
node.append(name);
fillBody(node, quote.body);
return node;
};
const createItem = (comment) => {
const item = document.createElement("li");
item.className = "item";
item.setAttribute("role", "listitem");
const avatar = document.createElement("img");
avatar.className = "avatar";
avatar.alt = "";
avatar.loading = "lazy";
avatar.referrerPolicy = "no-referrer";
if (comment.avatar !== "") avatar.src = comment.avatar;
const body = document.createElement("div");
body.className = "body";
const who = document.createElement("div");
who.className = "who";
const name = document.createElement("span");
name.className = "name";
name.textContent = comment.name;
who.append(name);
if (comment.createAt > 0) {
const time = document.createElement("span");
time.className = "time";
time.textContent = formatRailAge(comment.createAt);
who.append(time);
}
const text = document.createElement("p");
text.className = "text";
fillBody(text, comment.body);
const foot = document.createElement("div");
foot.className = "foot";
if (comment.likes > 0) {
const likes = document.createElement("span");
likes.className = "likes";
likes.textContent = "赞 " + comment.likes;
foot.append(likes);
}
if (comment.replies > 0) {
const replies = document.createElement("span");
replies.className = "replies";
replies.textContent = comment.replies + " 条回复";
foot.append(replies);
}
body.append(who, text);
if (foot.childElementCount > 0) body.append(foot);
const quotes = hideCy ? comment.quotes.filter((quote) => quote.isCy !== true) : comment.quotes;
if (quotes.length > 0) {
const sub = document.createElement("div");
sub.className = "sub";
sub.append(...quotes.map(createQuote));
body.append(sub);
}
item.append(avatar, body);
return item;
};
const render = () => {
const visible = hideCy ? state.items.filter((item) => item.isCy !== true) : state.items;
const hiddenCy = state.items.length - visible.length;
titleNode.textContent = state.title !== "" ? state.title : "评论";
openLinkNode.href = state.linkId !== "" ? LINK_HREF_PREFIX + state.linkId : "#";
openLinkNode.hidden = state.linkId === "";
const loadedText = visible.length > 0 ? " · 已加载 " + visible.length + " 条" : "";
const cyText = hiddenCy > 0 ? " · 已屏蔽 " + hiddenCy + " 条 CY" : "";
if (state.error !== "") {
meta.textContent = "";
} else if (state.total > 0) {
meta.textContent = "共 " + state.total + " 条评论" + loadedText + cyText;
} else {
meta.textContent = (visible.length > 0 ? "已加载 " + visible.length + " 条" : "") + cyText;
}
list.replaceChildren(...visible.map(createItem));
list.hidden = visible.length === 0;
if (state.error !== "") {
status.textContent = state.error;
status.hidden = false;
} else if (state.loading && state.items.length === 0) {
status.textContent = "正在加载评论…";
status.hidden = false;
} else if (state.linkId === "") {
status.textContent = "这里会直接显示信息流首条帖子的评论;点其他帖子的评论数字可切换。";
status.hidden = false;
} else if (visible.length === 0) {
status.textContent = hiddenCy > 0 ? "已屏蔽全部 " + hiddenCy + " 条 CY 评论" : "这条帖子还没有评论";
status.hidden = false;
} else {
status.hidden = true;
}
moreButton.hidden = state.linkId === "" || !state.hasMore;
moreButton.textContent = state.loading ? "加载中…" : "加载更多";
footBar.hidden = moreButton.hidden;
};
const load = async (page) => {
const seq = ++state.seq;
state.loading = true;
state.error = "";
render();
try {
const data = await fetchRailPage(state.linkId, page);
if (seq !== state.seq) return;
state.items = page === 1 ? data.items : state.items.concat(data.items);
state.page = page;
state.hasMore = data.items.length > 0;
if (data.total > 0) state.total = data.total;
if (data.title !== "" && state.title === "") state.title = data.title;
} catch (error) {
if (seq !== state.seq) return;
state.error = "评论加载失败:" + (error && error.message ? error.message : "未知错误") + ",可打开帖子查看。";
state.hasMore = false;
} finally {
if (seq === state.seq) {
state.loading = false;
render();
}
}
};
const open = (linkId) => {
const next = String(linkId === undefined || linkId === null ? "" : linkId);
if (next === "") return;
state.mode = "open";
applyMode();
if (next === state.linkId) return;
state.linkId = next;
state.title = "";
state.total = 0;
state.page = 0;
state.items = [];
state.hasMore = false;
state.error = "";
render();
void load(1);
};
const collapse = () => {
state.mode = state.linkId === "" ? "hidden" : "collapsed";
applyMode();
};
const setActive = (active) => {
if (!active) {
state.seq += 1;
state.loading = false;
state.mode = "hidden";
applyMode();
return;
}
if (state.mode !== "hidden") return;
state.mode = "collapsed";
applyMode();
};
tab.addEventListener("click", () => {
state.mode = "open";
applyMode();
render();
});
collapseButton.addEventListener("click", collapse);
moreButton.addEventListener("click", () => {
if (state.loading || state.linkId === "") return;
void load(state.page + 1);
});
updateCyButton();
applyMode();
render();
return {
host,
open,
collapse,
setActive,
isOpen: () => state.mode === "open",
getLinkId: () => state.linkId,
};
}
function watchCommentRail() {
if (!document.body) return;
const rail = createCommentRail();
document.body.append(rail.host);
const isFeedPage = () => !location.pathname.startsWith(LINK_HREF_PREFIX)
&& document.querySelector("#page-bbs-community .bbs-home__content-list") !== null;
const readLinkId = (href) => {
const match = /link\/(\d+)/.exec(href || "");
return match ? match[1] : "";
};
const readFirstFeedLinkId = () => {
const anchor = document.querySelector("#page-bbs-community .bbs-home__content-list a[href*='/app/bbs/link/']");
if (!anchor) return "";
return readLinkId(anchor.getAttribute("href"));
};
// 评论按钮在视频/图集等卡片模板里不一定放在帖子锚点内,退回到整张卡片里找链接。
const readCardLinkId = (element) => {
const anchor = element.closest('a[href*="/app/bbs/link/"]');
if (anchor) return readLinkId(anchor.getAttribute("href"));
const card = element.closest(".bbs-home__content-item, .content-list__item, li");
if (!card) return "";
const inner = card.querySelector('a[href*="/app/bbs/link/"]');
return inner ? readLinkId(inner.getAttribute("href")) : "";
};
const isEnabled = () => readCommentPreviewPreference();
document.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const trigger = target.closest(RAIL_TRIGGER_SELECTOR);
if (!trigger) return;
const linkId = readCardLinkId(trigger);
if (linkId === "") return;
if (!isEnabled()) return;
if (!isFeedPage()) return;
event.preventDefault();
event.stopPropagation();
rail.open(linkId);
}, true);
// 面板展开时,鼠标在信息流卡片上停留即切换预览,不必精确点中评论数字。
let hoverTimer = 0;
let hoverId = "";
const cancelHover = () => {
if (hoverTimer !== 0) window.clearTimeout(hoverTimer);
hoverTimer = 0;
hoverId = "";
};
document.addEventListener("pointerover", (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const card = target.closest("#page-bbs-community .bbs-home__content-item");
if (!card) return;
if (!isEnabled() || !isFeedPage() || !rail.isOpen()) return;
const linkId = readCardLinkId(card);
if (linkId === "" || linkId === hoverId) return;
cancelHover();
hoverId = linkId;
hoverTimer = window.setTimeout(() => {
hoverTimer = 0;
if (!rail.isOpen() || rail.getLinkId() === linkId) return;
rail.open(linkId);
}, 600);
}, true);
document.addEventListener("pointerdown", cancelHover, true);
document.addEventListener("scroll", cancelHover, true);
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !rail.isOpen()) return;
event.preventDefault();
rail.collapse();
});
let scheduled = false;
let autoOpened = false;
const sync = () => {
if (rail.host.parentNode !== document.body) document.body.append(rail.host);
if (!isEnabled()) {
autoOpened = false;
rail.setActive(false);
return;
}
const active = isFeedPage();
rail.setActive(active);
if (!active) {
autoOpened = false;
return;
}
// 开启预览后,进入首页直接带出首条帖子的评论,不必先点评论数字。
if (!autoOpened && !rail.isOpen()) {
const linkId = readFirstFeedLinkId();
if (linkId !== "") {
autoOpened = true;
rail.open(linkId);
}
}
};
document.addEventListener(COMMENT_PREVIEW_EVENT, sync);
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
}).observe(document.body, { childList: true, subtree: true });
sync();
}
// 帖子连读:在帖子页纵轴中心左右两侧提供上一篇/下一篇箭头,切换走站内路由,不整页刷新。
const PAGER_ID = "heybox-up-post-pager";
const FEED_ORDER_KEY = "heybox-up.feedOrder.v1";
const POST_TRAIL_KEY = "heybox-up.postTrail.v1";
const POST_TRAIL_MAX = 40;
const PAGER_CSS = `
:host { position: fixed; inset: 0; display: block; pointer-events: none; z-index: 2147482990; font: 12px/1.5 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; color: var(--heybox-up-text, #14191e); }
:host([hidden]) { display: none; }
.pager { position: absolute; inset: 0; pointer-events: none; }
.btn {
position: absolute;
top: 50%;
display: grid;
place-items: center;
width: 42px;
height: 64px;
padding: 0;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .12));
border-radius: 999px;
background: var(--heybox-up-surface, #fff);
box-shadow: var(--heybox-up-shadow-2, 0 10px 28px rgba(8, 12, 16, .18));
color: var(--heybox-up-muted, #6b7280);
font: inherit;
cursor: pointer;
opacity: .78;
transform: translateY(-50%);
pointer-events: auto;
transition: opacity .16s ease, color .16s ease, background-color .16s ease, border-color .16s ease;
}
.btn--prev { left: max(10px, calc((100vw - var(--heybox-up-page-w, 1360px)) / 2 - 54px)); }
.btn--next { right: max(10px, calc((100vw - var(--heybox-up-page-w, 1360px)) / 2 - 54px)); }
.btn:hover:not(:disabled) { opacity: 1; border-color: var(--heybox-up-accent, #6f8db3); background: var(--heybox-up-subtle, rgba(20, 25, 30, .06)); color: var(--heybox-up-text, #14191e); }
.btn:active:not(:disabled) { background: var(--heybox-up-raised, #d7dce3); }
.btn:disabled { opacity: .3; cursor: default; }
.btn:focus-visible { outline: 2px solid var(--heybox-up-accent, #6f8db3); outline-offset: 2px; }
.btn svg { width: 20px; height: 20px; flex: none; transition: transform .16s ease; }
.btn--prev:hover:not(:disabled) svg { transform: translateX(-1px); }
.btn--next:hover:not(:disabled) svg { transform: translateX(1px); }
@media (max-width: 1199px) { .btn { width: 38px; height: 56px; opacity: .64; } .btn--prev { left: 8px; } .btn--next { right: 8px; } }
@media (max-width: 1080px) { :host { display: none; } }
@media (prefers-reduced-motion: reduce) { .btn, .btn svg { transition: none; } }
`;
const PAGER_ICONS = {
prev: '',
next: '',
};
function readPostLinkId() {
const match = /^\/app\/bbs\/link\/(\d+)/.exec(location.pathname);
return match ? match[1] : "";
}
function readSessionArray(key) {
try {
const value = JSON.parse(sessionStorage.getItem(key) || "[]");
return Array.isArray(value) ? value : [];
} catch (error) {
return [];
}
}
function writeSessionValue(key, value) {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch (error) {
// 隐私模式下连读仍可按推荐列表工作,只是记不住顺序。
}
}
function readFeedOrder() {
return readSessionArray(FEED_ORDER_KEY).filter((item) => item && typeof item.id === "string" && /^\d+$/.test(item.id));
}
function recordFeedOrder() {
if (readPostLinkId() !== "") return;
if (!document.querySelector("#page-bbs-community .bbs-home__content-list")) return;
const seen = new Set();
const list = [];
for (const anchor of document.querySelectorAll("#page-bbs-community .bbs-home__content-list a[href*='/app/bbs/link/']")) {
const match = /link\/(\d+)/.exec(anchor.getAttribute("href") || "");
if (!match || seen.has(match[1])) continue;
seen.add(match[1]);
const titleNode = anchor.querySelector(".bbs-home__content-title, .link-item__title, .bbs-home__topic-name");
const title = (titleNode ? titleNode.textContent : anchor.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
list.push({ id: match[1], title: title });
}
if (list.length > 0) writeSessionValue(FEED_ORDER_KEY, list);
}
function readRelatedPostLinks() {
const seen = new Set();
const list = [];
const nodes = document.querySelectorAll(".bbs-link__related-recommend a[href*='/app/bbs/link/'], .related-recommend__container a[href*='/app/bbs/link/']");
for (const anchor of nodes) {
const match = /link\/(\d+)/.exec(anchor.getAttribute("href") || "");
if (!match || seen.has(match[1])) continue;
seen.add(match[1]);
const title = (anchor.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
list.push({ id: match[1], title: title });
}
return list;
}
function readPostTrail() {
return readSessionArray(POST_TRAIL_KEY).filter((id) => typeof id === "string" && /^\d+$/.test(id));
}
function pushPostTrail(id) {
const trail = readPostTrail();
if (trail[trail.length - 1] === id) return;
const seenAt = trail.lastIndexOf(id);
if (seenAt !== -1) trail.splice(seenAt, 1);
trail.push(id);
while (trail.length > POST_TRAIL_MAX) trail.shift();
writeSessionValue(POST_TRAIL_KEY, trail);
}
function resolvePostNeighbors(id) {
const order = readFeedOrder();
const index = order.findIndex((item) => item.id === id);
let prev = index > 0 ? order[index - 1] : null;
let next = index !== -1 && index < order.length - 1 ? order[index + 1] : null;
if (!next) {
const related = readRelatedPostLinks().filter((item) => item.id !== id);
if (related.length > 0) next = related[0];
}
if (!prev) {
const trail = readPostTrail();
const at = trail.lastIndexOf(id);
const prevId = at > 0 ? trail[at - 1] : at === -1 && trail.length > 0 ? trail[trail.length - 1] : "";
if (prevId !== "" && prevId !== id) prev = { id: prevId, title: "" };
}
return { prev: prev, next: next, index: index === -1 ? null : index + 1, total: order.length };
}
function isTypingTarget(target) {
if (!(target instanceof Element)) return false;
if (target.isContentEditable) return true;
return target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
}
function watchPostPager() {
if (!document.body) return;
const host = document.createElement("div");
host.id = PAGER_ID;
host.hidden = true;
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = PAGER_CSS;
const pager = document.createElement("nav");
pager.className = "pager";
pager.setAttribute("aria-label", "文章连读");
const prevButton = document.createElement("button");
prevButton.type = "button";
prevButton.className = "btn btn--prev";
prevButton.setAttribute("aria-label", "上一篇");
prevButton.title = "上一篇";
prevButton.innerHTML = PAGER_ICONS.prev;
const nextButton = document.createElement("button");
nextButton.type = "button";
nextButton.className = "btn btn--next";
nextButton.setAttribute("aria-label", "下一篇");
nextButton.title = "下一篇";
nextButton.innerHTML = PAGER_ICONS.next;
pager.append(prevButton, nextButton);
shadow.append(style, pager);
document.body.append(host);
let currentId = "";
let prevId = "";
let nextId = "";
let navigating = false;
const go = (id) => {
if (id === "" || navigating) return;
navigating = true;
navigateToPath(LINK_HREF_PREFIX + id);
const toTop = () => window.scrollTo({ top: 0, behavior: "auto" });
requestAnimationFrame(toTop);
window.setTimeout(toTop, 120);
window.setTimeout(() => { navigating = false; }, 900);
};
prevButton.addEventListener("click", () => go(prevId));
nextButton.addEventListener("click", () => go(nextId));
document.addEventListener("keydown", (event) => {
if (host.hidden || event.defaultPrevented) return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
if (isTypingTarget(event.target)) return;
if (event.key === "[" && prevId !== "") {
event.preventDefault();
go(prevId);
} else if (event.key === "]" && nextId !== "") {
event.preventDefault();
go(nextId);
}
});
let scheduled = false;
const sync = () => {
recordFeedOrder();
const id = readPostLinkId();
if (id === "") {
host.hidden = true;
currentId = "";
return;
}
if (id !== currentId) {
currentId = id;
pushPostTrail(id);
host.hidden = false;
}
const neighbors = resolvePostNeighbors(id);
prevId = neighbors.prev ? neighbors.prev.id : "";
nextId = neighbors.next ? neighbors.next.id : "";
prevButton.disabled = prevId === "";
nextButton.disabled = nextId === "";
prevButton.title = neighbors.prev ? "上一篇" + (neighbors.prev.title !== "" ? ":" + neighbors.prev.title : "") : "没有上一篇";
nextButton.title = neighbors.next ? "下一篇" + (neighbors.next.title !== "" ? ":" + neighbors.next.title : "") : "没有下一篇";
prevButton.setAttribute("aria-label", prevButton.title);
nextButton.setAttribute("aria-label", nextButton.title);
};
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
}).observe(document.body, { childList: true, subtree: true });
sync();
}
// 社区内搜索:站点搜索页(/app/search/list)只做全局检索,它的 queryFn 不读 topic_id,
// 所以脚本在社区页直接带 topic_id 请求同一个搜索接口,把结果渲染在自建面板里。
const COMMUNITY_SEARCH_API_PATH = "/bbs/app/api/general/search/v1";
const COMMUNITY_SEARCH_ID = "heybox-up-community-search";
const COMMUNITY_SEARCH_STYLE_ID = "heybox-up-community-search-style";
const COMMUNITY_SEARCH_ACTIVE_CLASS = "heybox-up-cs-active";
const COMMUNITY_SEARCH_PAGE_SIZE = 30;
const COMMUNITY_SEARCH_ICONS = {
search: '',
};
const COMMUNITY_SEARCH_CSS = `
#heybox-up-community-search {
display: block;
margin: 14px 0 12px;
font: 13px/1.55 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--heybox-up-text, #14191e);
-webkit-font-smoothing: antialiased;
}
#heybox-up-community-search *,
#heybox-up-community-search *::before,
#heybox-up-community-search *::after { box-sizing: border-box; }
#heybox-up-community-search .bar {
display: flex;
align-items: center;
gap: 10px;
height: 42px;
margin: 0;
padding: 0 6px 0 13px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: var(--heybox-up-radius-card, 14px);
background: var(--heybox-up-surface, #fff);
box-shadow: var(--heybox-up-shadow-1, none);
transition: border-color .16s ease, box-shadow .16s ease;
}
#heybox-up-community-search .bar:focus-within {
border-color: var(--heybox-up-accent, #6f8db3);
box-shadow: 0 0 0 3px rgba(111, 141, 179, .16);
}
#heybox-up-community-search .icon { display: inline-flex; flex: none; color: var(--heybox-up-muted, #6b7280); }
#heybox-up-community-search .icon svg { width: 16px; height: 16px; }
#heybox-up-community-search .scope {
flex: none;
max-width: 34%;
height: 22px;
padding: 0 9px;
border-radius: 999px;
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
color: var(--heybox-up-muted, #6b7280);
font-size: 12px;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#heybox-up-community-search input.q {
flex: 1 1 auto;
min-width: 0;
height: 30px;
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 14px;
line-height: 30px;
outline: none;
box-shadow: none;
appearance: none;
}
#heybox-up-community-search input.q::placeholder { color: var(--heybox-up-muted, #6b7280); }
#heybox-up-community-search button.go {
flex: none;
height: 30px;
margin: 0;
padding: 0 15px;
border: 0;
border-radius: 999px;
background: var(--heybox-up-accent, #6f8db3);
color: var(--heybox-up-on-accent, #fff);
font: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: filter .16s ease;
}
#heybox-up-community-search button.go:hover { filter: brightness(1.06); }
#heybox-up-community-search button.go:focus-visible,
#heybox-up-community-search .chip:focus-visible,
#heybox-up-community-search .more:focus-visible,
#heybox-up-community-search .close:focus-visible {
outline: 2px solid var(--heybox-up-accent, #6f8db3);
outline-offset: 2px;
}
#heybox-up-community-search .panel {
margin-top: 10px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: var(--heybox-up-radius-card, 14px);
background: var(--heybox-up-surface, #fff);
box-shadow: var(--heybox-up-shadow-1, none);
overflow: hidden;
}
#heybox-up-community-search .panel[hidden] { display: none !important; }
#heybox-up-community-search .head {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 16px;
border-bottom: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
}
#heybox-up-community-search .head-text { flex: 1 1 auto; min-width: 0; }
#heybox-up-community-search .head-title {
margin: 0;
font-size: 14px;
font-weight: 600;
line-height: 1.5;
color: var(--heybox-up-text, #14191e);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#heybox-up-community-search .head-meta {
margin: 2px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--heybox-up-muted, #6b7280);
}
#heybox-up-community-search .head-actions { flex: none; display: flex; align-items: center; gap: 6px; }
#heybox-up-community-search .ghost,
#heybox-up-community-search .close {
height: 28px;
padding: 0 11px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: 999px;
background: transparent;
color: var(--heybox-up-text-2, #3c4249);
font: inherit;
font-size: 12px;
line-height: 26px;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: background .16s ease, border-color .16s ease, color .16s ease;
}
#heybox-up-community-search .ghost:hover,
#heybox-up-community-search .close:hover {
border-color: var(--heybox-up-accent, #6f8db3);
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
color: var(--heybox-up-text, #14191e);
}
#heybox-up-community-search .filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px 16px;
border-bottom: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
}
#heybox-up-community-search .filters[hidden] { display: none !important; }
#heybox-up-community-search .chip {
height: 26px;
margin: 0;
padding: 0 11px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: 999px;
background: transparent;
color: var(--heybox-up-text-2, #3c4249);
font: inherit;
font-size: 12px;
line-height: 24px;
cursor: pointer;
transition: color .16s ease, border-color .16s ease, background .16s ease;
}
#heybox-up-community-search .chip:hover { border-color: var(--heybox-up-accent, #6f8db3); }
#heybox-up-community-search .chip[aria-pressed="true"] {
border-color: var(--heybox-up-accent, #6f8db3);
background: rgba(111, 141, 179, .14);
color: var(--heybox-up-accent, #6f8db3);
font-weight: 600;
}
#heybox-up-community-search .item {
display: block;
padding: 15px 16px;
border-bottom: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
color: inherit;
text-decoration: none;
cursor: pointer;
transition: background .14s ease;
}
#heybox-up-community-search .item:last-child { border-bottom: 0; }
#heybox-up-community-search .item:hover { background: var(--heybox-up-subtle, rgba(20, 25, 30, .06)); }
#heybox-up-community-search .item-thumb {
float: right;
width: 88px;
height: 66px;
margin: 2px 0 6px 14px;
border-radius: 8px;
object-fit: cover;
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
}
#heybox-up-community-search .item-title {
margin: 0 0 4px;
font-size: 15px;
font-weight: 600;
line-height: 1.45;
color: var(--heybox-up-text, #14191e);
}
#heybox-up-community-search .item-desc {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.62;
color: var(--heybox-up-text-2, #3c4249);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
#heybox-up-community-search .item-meta {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--heybox-up-muted, #6b7280);
}
#heybox-up-community-search .empty {
margin: 0;
padding: 30px 18px;
text-align: center;
font-size: 13px;
line-height: 1.7;
color: var(--heybox-up-muted, #6b7280);
}
#heybox-up-community-search .foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 16px;
border-top: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
}
#heybox-up-community-search .foot[hidden] { display: none !important; }
#heybox-up-community-search .more {
flex: none;
height: 30px;
margin: 0;
padding: 0 14px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: 999px;
background: transparent;
color: var(--heybox-up-text-2, #3c4249);
font: inherit;
font-size: 12px;
cursor: pointer;
}
#heybox-up-community-search .more[hidden] { display: none !important; }
#heybox-up-community-search .more:hover:not(:disabled) {
border-color: var(--heybox-up-accent, #6f8db3);
color: var(--heybox-up-text, #14191e);
}
#heybox-up-community-search .more:disabled { opacity: .45; cursor: default; }
#heybox-up-community-search .status {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--heybox-up-muted, #6b7280);
text-align: right;
}
/* 结果面板打开时,站点自己的筛选行与信息流藏起来,避免两套列表叠在一起。 */
#page-topic-link.heybox-up-cs-active .topic-link__filter-row,
#page-topic-link.heybox-up-cs-active .topic-link__list { display: none !important; }
@media (max-width: 720px) {
#heybox-up-community-search { margin: 10px 0; }
#heybox-up-community-search .scope { display: none; }
#heybox-up-community-search .head { flex-wrap: wrap; }
#heybox-up-community-search .item-thumb { width: 72px; height: 54px; }
}
`;
function ensureCommunitySearchStyle() {
let style = document.getElementById(COMMUNITY_SEARCH_STYLE_ID);
if (!style) {
style = document.createElement("style");
style.id = COMMUNITY_SEARCH_STYLE_ID;
style.textContent = COMMUNITY_SEARCH_CSS;
} else if (style.textContent !== COMMUNITY_SEARCH_CSS) {
style.textContent = COMMUNITY_SEARCH_CSS;
}
const parent = document.head || document.documentElement;
if (parent && style.parentNode !== parent) parent.append(style);
}
// 社区页地址形如 /app/topic/link/,社区名从页头读,读不到时退回标题。
function readTopicContext() {
const match = /^\/app\/topic\/link\/(\d+)/.exec(location.pathname);
if (match === null) return null;
const header = document.querySelector("#page-topic-link .topic-link__header");
let name = "";
if (header !== null) {
const heading = header.querySelector("h1, h2, [class*='name'], [class*='title']");
if (heading !== null) name = normalizeRailText(heading.textContent).slice(0, 24);
}
if (name === "") name = normalizeRailText((document.title || "").replace(/[-_|].*$/, "")).slice(0, 24);
return { topicId: match[1], name: name === "" ? "本社区" : name };
}
function buildCommunitySearchUrl(topicId, keyword, offset, timeRange, signed) {
const query = new URLSearchParams({
os_type: "web",
app: "heybox",
client_type: "web",
version: "999.0.4",
web_version: "2.5",
x_client_type: "web",
x_app: "heybox_website",
x_os_type: "Windows",
device_info: "Chrome",
q: keyword,
search_type: "link",
is_pull_down: "0",
offset: String(offset),
limit: String(COMMUNITY_SEARCH_PAGE_SIZE),
dw: "660",
topic_id: String(topicId),
});
if (timeRange !== "") query.set("time_range", timeRange);
if (signed === true) {
const time = Math.floor(Date.now() / 1000);
const nonce = md5Hex(String(time) + String(Math.random())).toUpperCase();
query.set("hkey", createSignature(COMMUNITY_SEARCH_API_PATH, time, nonce));
query.set("_time", String(time));
query.set("nonce", nonce);
}
return "https://api.xiaoheihe.cn" + COMMUNITY_SEARCH_API_PATH + "?" + query.toString();
}
async function fetchCommunitySearchPage(url) {
let response;
try {
response = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
} catch (error) {
return { ok: false, message: "网络请求失败,请稍后重试" };
}
if (response.ok !== true) return { ok: false, message: "接口返回 HTTP " + response.status };
let data;
try {
data = await response.json();
} catch (error) {
return { ok: false, message: "接口返回非 JSON 内容,可能触发站点风控" };
}
if (!data || data.status !== "ok") {
const status = data && typeof data.status === "string" ? data.status : "";
const message = data && typeof data.msg === "string" && data.msg !== "" ? data.msg : status;
return { ok: false, message: message === "" ? "接口返回失败状态" : message };
}
const result = data.result && typeof data.result === "object" ? data.result : {};
const items = Array.isArray(result.items) ? result.items : [];
const rawRanges = Array.isArray(result.time_range_list) ? result.time_range_list : [];
const timeRanges = rawRanges
.filter((entry) => entry && typeof entry === "object" && typeof entry.value === "string" && typeof entry.name === "string")
.map((entry) => ({ value: entry.value, name: entry.name }));
const scopedTip = typeof data.search_not_result_tips === "string" ? data.search_not_result_tips.trim() : "";
const resultTip = typeof result.search_not_result_tips === "string" ? result.search_not_result_tips.trim() : "";
return { ok: true, items, timeRanges, tip: scopedTip !== "" ? scopedTip : resultTip };
}
async function requestCommunitySearch(topicId, keyword, offset, timeRange) {
// 站点搜索接口接受带签名的请求;签名被风控拒绝时退回不带签名的请求,两者都失败才报错。
const signed = await fetchCommunitySearchPage(buildCommunitySearchUrl(topicId, keyword, offset, timeRange, true));
if (signed.ok === true) return signed;
const plain = await fetchCommunitySearchPage(buildCommunitySearchUrl(topicId, keyword, offset, timeRange, false));
return plain.ok === true ? plain : signed;
}
function pickSearchText(values) {
for (const value of values) {
if (typeof value === "string" && value.trim() !== "") return value.trim();
if (typeof value === "number" && Number.isFinite(value)) return String(value);
}
return "";
}
function pickSearchNumber(values) {
for (const value of values) {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
return 0;
}
// 搜索结果的 link 条目与信息流 link 对象同构,但不同接口会缺字段,这里逐个兜底。
function normalizeSearchResult(entry) {
if (!entry || typeof entry !== "object") return null;
if (typeof entry.type === "string" && entry.type !== "" && entry.type !== "link") return null;
const info = entry.info && typeof entry.info === "object" ? entry.info : entry;
const id = pickSearchText([info.linkid, info.link_id, info.id]);
if (id === "") return null;
const user = info.user && typeof info.user === "object" ? info.user : {};
const thumbs = Array.isArray(info.thumbs) ? info.thumbs : [];
return {
id: id,
title: pickSearchText([info.title, info.name]) || "无标题帖子",
desc: normalizeRailText(pickSearchText([info.description, info.desc, info.text, info.content, info.summary])).slice(0, 160),
author: pickSearchText([user.username, user.name, user.nickname]),
thumb: pickSearchText([thumbs[0], info.thumb, info.cover, info.image]),
createAt: pickSearchNumber([info.create_at, info.create_time, info.timestamp]),
like: pickSearchNumber([info.like_num, info.like_count, info.up, info.likes]),
comment: pickSearchNumber([info.comment_num, info.link_comment_num, info.comment_count, info.comments]),
};
}
function createCommunitySearchItem(item) {
const node = document.createElement("a");
node.className = "item";
node.href = LINK_HREF_PREFIX + item.id;
node.addEventListener("click", (event) => {
// 组合键与中键交给浏览器,保留新标签页打开。
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigateToPath(LINK_HREF_PREFIX + item.id);
});
if (item.thumb !== "") {
const thumb = document.createElement("img");
thumb.className = "item-thumb";
thumb.src = item.thumb;
thumb.alt = "";
thumb.loading = "lazy";
thumb.referrerPolicy = "no-referrer";
node.append(thumb);
}
const title = document.createElement("h3");
title.className = "item-title";
title.textContent = item.title;
node.append(title);
if (item.desc !== "") {
const desc = document.createElement("p");
desc.className = "item-desc";
desc.textContent = item.desc;
node.append(desc);
}
const parts = [];
if (item.author !== "") parts.push(item.author);
if (item.createAt > 0) parts.push(formatRailAge(item.createAt));
if (item.like > 0) parts.push("赞 " + item.like);
if (item.comment > 0) parts.push("评论 " + item.comment);
if (parts.length > 0) {
const meta = document.createElement("p");
meta.className = "item-meta";
meta.textContent = parts.join(" · ");
node.append(meta);
}
return node;
}
function createCommunitySearchPanel() {
const root = document.createElement("section");
root.id = COMMUNITY_SEARCH_ID;
root.setAttribute("aria-label", "社区内搜索");
const bar = document.createElement("form");
bar.className = "bar";
bar.setAttribute("role", "search");
const icon = document.createElement("span");
icon.className = "icon";
icon.innerHTML = COMMUNITY_SEARCH_ICONS.search;
const scope = document.createElement("span");
scope.className = "scope";
const input = document.createElement("input");
input.className = "q";
input.type = "search";
input.name = "q";
input.autocomplete = "off";
input.placeholder = "搜索本社区的帖子";
input.setAttribute("aria-label", "搜索本社区的帖子");
const go = document.createElement("button");
go.type = "submit";
go.className = "go";
go.textContent = "搜索";
bar.append(icon, scope, input, go);
const panel = document.createElement("div");
panel.className = "panel";
panel.hidden = true;
const head = document.createElement("div");
head.className = "head";
const headText = document.createElement("div");
headText.className = "head-text";
const headTitle = document.createElement("p");
headTitle.className = "head-title";
const headMeta = document.createElement("p");
headMeta.className = "head-meta";
headText.append(headTitle, headMeta);
const headActions = document.createElement("div");
headActions.className = "head-actions";
const globalLink = document.createElement("a");
globalLink.className = "ghost";
globalLink.href = "/app/search/list";
globalLink.textContent = "全站搜索";
globalLink.addEventListener("click", (event) => {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigateToPath(globalLink.getAttribute("href") || "/app/search/list");
});
const close = document.createElement("button");
close.type = "button";
close.className = "close";
close.textContent = "返回社区";
headActions.append(globalLink, close);
head.append(headText, headActions);
const filters = document.createElement("div");
filters.className = "filters";
filters.hidden = true;
const list = document.createElement("div");
list.className = "list";
const foot = document.createElement("div");
foot.className = "foot";
foot.hidden = true;
const more = document.createElement("button");
more.type = "button";
more.className = "more";
more.textContent = "加载更多";
const status = document.createElement("p");
status.className = "status";
foot.append(more, status);
panel.append(head, filters, list, foot);
root.append(bar, panel);
return { root, bar, input, scope, panel, headTitle, headMeta, globalLink, filters, list, foot, more, status, close };
}
function watchCommunitySearch() {
if (!document.body) return;
const ui = createCommunitySearchPanel();
let state = null;
const pageNode = () => document.querySelector("#page-topic-link");
const mainNode = () => document.querySelector("#page-topic-link .topic-link__main");
const setActive = (active) => {
const page = pageNode();
if (page !== null) page.classList.toggle(COMMUNITY_SEARCH_ACTIVE_CLASS, active === true);
ui.panel.hidden = active !== true;
};
const renderChips = () => {
ui.filters.textContent = "";
const ranges = state === null ? [] : state.timeRanges;
if (ranges.length === 0) {
ui.filters.hidden = true;
return;
}
ui.filters.hidden = false;
const build = (label, value) => {
const chip = document.createElement("button");
chip.type = "button";
chip.className = "chip";
chip.textContent = label;
chip.setAttribute("aria-pressed", String(state !== null && state.timeRange === value));
chip.addEventListener("click", () => {
const current = state;
if (current === null || current.loading === true || current.timeRange === value) return;
current.timeRange = value;
current.offset = 0;
current.items = [];
current.noMore = false;
void run(true);
});
ui.filters.append(chip);
};
build("不限时间", "");
for (const range of ranges) build(range.name, range.value);
};
const renderItems = () => {
ui.list.textContent = "";
const items = state === null ? [] : state.items;
if (items.length > 0) {
for (const item of items) ui.list.append(createCommunitySearchItem(item));
return;
}
const empty = document.createElement("p");
empty.className = "empty";
if (state === null) empty.textContent = "";
else if (state.loading === true) empty.textContent = "正在搜索…";
else if (state.error !== "") empty.textContent = state.error;
else if (state.tip !== "") empty.textContent = state.tip;
else empty.textContent = "这个社区里没有匹配的帖子,换个关键词试试。";
ui.list.append(empty);
};
const render = () => {
if (state === null) return;
ui.headTitle.textContent = "在「" + state.topicName + "」中搜索 “" + state.keyword + "”";
const loaded = state.items.length;
if (state.loading === true) {
ui.headMeta.textContent = loaded === 0 ? "搜索中…" : "已加载 " + loaded + " 条,正在继续加载…";
} else if (loaded === 0) {
ui.headMeta.textContent = state.error === "" ? "" : "共 0 条";
} else {
ui.headMeta.textContent = "本社区已加载 " + loaded + " 条" + (state.noMore === true ? "" : "(还有更多)");
}
ui.globalLink.setAttribute("href", "/app/search/list?q=" + encodeURIComponent(state.keyword));
renderChips();
renderItems();
ui.foot.hidden = loaded > 0 || state.loading === true ? false : true;
ui.more.hidden = state.noMore === true;
ui.more.disabled = state.loading === true;
if (state.status !== "") ui.status.textContent = state.status;
else if (state.noMore === true && loaded > 0) ui.status.textContent = "已到最后一页";
else ui.status.textContent = "";
};
const run = async (reset) => {
const current = state;
if (current === null || current.loading === true) return;
current.loading = true;
current.error = "";
current.status = reset === true ? "" : "加载中…";
render();
const offset = reset === true ? 0 : current.offset;
const response = await requestCommunitySearch(current.topicId, current.keyword, offset, current.timeRange);
if (state !== current) return;
current.loading = false;
current.status = "";
if (response.ok !== true) {
if (reset === true) {
current.items = [];
current.error = "搜索失败:" + response.message;
} else {
current.status = "加载失败:" + response.message;
}
render();
return;
}
const known = new Set(current.items.map((item) => item.id));
const incoming = response.items
.map(normalizeSearchResult)
.filter((item) => item !== null && !known.has(item.id));
current.items = reset === true ? incoming : current.items.concat(incoming);
current.offset = offset + response.items.length;
current.noMore = response.items.length < COMMUNITY_SEARCH_PAGE_SIZE;
current.tip = response.tip;
if (current.timeRanges.length === 0 && response.timeRanges.length > 0) current.timeRanges = response.timeRanges;
render();
};
const clearPanel = () => {
ui.list.textContent = "";
ui.filters.textContent = "";
ui.filters.hidden = true;
ui.foot.hidden = true;
ui.headMeta.textContent = "";
};
const close = () => {
state = null;
clearPanel();
setActive(false);
};
const submit = () => {
const context = readTopicContext();
if (context === null) return;
const keyword = ui.input.value.trim();
if (keyword === "") {
ui.input.focus();
return;
}
state = {
topicId: context.topicId,
topicName: context.name,
keyword: keyword,
timeRange: "",
items: [],
offset: 0,
noMore: false,
timeRanges: [],
tip: "",
error: "",
status: "",
loading: false,
};
setActive(true);
render();
void run(true);
};
ui.bar.addEventListener("submit", (event) => {
event.preventDefault();
submit();
});
ui.input.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || state === null) return;
event.preventDefault();
close();
});
ui.close.addEventListener("click", close);
ui.more.addEventListener("click", () => {
void run(false);
});
const sync = () => {
ensureCommunitySearchStyle();
const context = readTopicContext();
const main = mainNode();
if (context === null || main === null) {
if (ui.root.parentNode !== null) ui.root.remove();
if (state !== null) {
state = null;
clearPanel();
}
setActive(false);
return;
}
const anchor = main.querySelector(".topic-link__filter-row");
const misplaced = ui.root.parentNode !== main || (anchor !== null && ui.root.nextElementSibling !== anchor);
if (misplaced === true) {
if (anchor !== null) main.insertBefore(ui.root, anchor);
else main.append(ui.root);
}
// 写入前先比对,避免 MutationObserver 被自身改动反复唤醒。
if (ui.scope.textContent !== context.name) ui.scope.textContent = context.name;
const placeholder = "在「" + context.name + "」中搜索帖子";
if (ui.input.placeholder !== placeholder) ui.input.placeholder = placeholder;
if (ui.input.getAttribute("aria-label") !== placeholder) ui.input.setAttribute("aria-label", placeholder);
// 换到另一个社区时旧结果不再适用,直接回到信息流。
if (state !== null && state.topicId !== context.topicId) {
state = null;
clearPanel();
}
setActive(state !== null);
};
let scheduled = false;
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
}).observe(document.body, { childList: true, subtree: true });
window.addEventListener("popstate", () => {
if (state !== null) close();
});
sync();
}
// 桌面工作台:宽屏下把社区页排成左频道、中信息流、右热度三栏。
// 站点原本是移动端单列思路,1600px 下仍有大半屏留白;这里只重排现有内容,不请求新接口。
const DESKTOP_STYLE_ID = "heybox-up-desktop-style";
const DESKTOP_LEFT_ID = "heybox-up-left-rail";
const DESKTOP_RIGHT_ID = "heybox-up-right-rail";
const DESKTOP_PAGE_CLASS = "heybox-up-desktop";
const DESKTOP_HOT_TARGET_CLASS = "heybox-up-hot-target";
const DESKTOP_HOT_LIMIT = 6;
const DESKTOP_CSS = `
#heybox-up-left-rail, #heybox-up-right-rail { display: none; }
#heybox-up-left-rail,
#heybox-up-right-rail {
position: sticky;
/* 桌面隐藏站点顶部搜索条后,侧栏直接停在导航下方。 */
top: 80px;
max-height: calc(100vh - 100px);
overflow: auto;
font: 13px/1.55 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--heybox-up-text-2, #3c4249);
scrollbar-width: thin;
}
#heybox-up-left-rail .rail-block,
#heybox-up-right-rail .rail-block {
padding: 13px 10px;
border: 1px solid var(--heybox-up-border, rgba(20, 25, 30, .1));
border-radius: var(--heybox-up-radius-card, 14px);
background: var(--heybox-up-surface, #fff);
box-shadow: var(--heybox-up-shadow-1, none);
}
#heybox-up-left-rail .rail-block + .rail-block,
#heybox-up-right-rail .rail-block + .rail-block { margin-top: 12px; }
#heybox-up-left-rail .rail-title,
#heybox-up-right-rail .rail-title {
margin: 0 0 7px;
padding: 0 8px;
color: var(--heybox-up-muted, #6b7280);
font-size: 11px;
font-weight: 600;
line-height: 1.5;
letter-spacing: .08em;
}
#heybox-up-left-rail .rail-link,
#heybox-up-left-rail .rail-channel,
#heybox-up-right-rail .rail-link {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
height: 34px;
margin: 0;
padding: 0 8px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--heybox-up-text-2, #3c4249);
font: inherit;
font-size: 13px;
line-height: 34px;
text-align: left;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: background .14s ease, color .14s ease;
}
#heybox-up-left-rail .rail-link:hover,
#heybox-up-left-rail .rail-channel:hover,
#heybox-up-right-rail .rail-link:hover {
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
color: var(--heybox-up-text, #14191e);
}
#heybox-up-left-rail .rail-link.is-active {
background: rgba(111, 141, 179, .13);
color: var(--heybox-up-accent, #6f8db3);
font-weight: 600;
}
#heybox-up-left-rail .rail-link svg,
#heybox-up-right-rail .rail-link svg { width: 17px; height: 17px; flex: none; }
#heybox-up-left-rail .rail-channel img {
width: 22px;
height: 22px;
flex: none;
border-radius: 6px;
object-fit: cover;
background: var(--heybox-up-subtle, rgba(20, 25, 30, .06));
}
#heybox-up-left-rail .rail-channel .name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
#heybox-up-left-rail .channels { max-height: 288px; overflow: auto; scrollbar-width: thin; }
#heybox-up-left-rail .rail-link:focus-visible,
#heybox-up-left-rail .rail-channel:focus-visible,
#heybox-up-right-rail .rail-link:focus-visible,
#heybox-up-right-rail .hot-item:focus-visible {
outline: 2px solid var(--heybox-up-accent, #6f8db3);
outline-offset: -2px;
}
#heybox-up-right-rail .hot-list {
display: flex;
flex-direction: column;
gap: 1px;
margin: 0;
padding: 0;
list-style: none;
}
#heybox-up-right-rail .hot-item {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
column-gap: 7px;
width: 100%;
margin: 0;
padding: 8px 6px;
border: 0;
border-radius: 8px;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
transition: background .14s ease;
}
#heybox-up-right-rail .hot-item:hover { background: var(--heybox-up-subtle, rgba(20, 25, 30, .06)); }
#heybox-up-right-rail .hot-rank {
color: var(--heybox-up-muted, #6b7280);
font-size: 12px;
font-weight: 700;
line-height: 1.5;
font-variant-numeric: tabular-nums;
}
#heybox-up-right-rail .hot-item:nth-child(-n+3) .hot-rank { color: var(--heybox-up-accent, #6f8db3); }
#heybox-up-right-rail .hot-title {
display: -webkit-box;
overflow: hidden;
color: var(--heybox-up-text-2, #3c4249);
font-size: 13px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
#heybox-up-right-rail .hot-item:hover .hot-title { color: var(--heybox-up-text, #14191e); }
#heybox-up-right-rail .hot-meta {
grid-column: 2;
margin-top: 2px;
color: var(--heybox-up-muted, #6b7280);
font-size: 11px;
line-height: 1.5;
}
#heybox-up-right-rail .rail-empty {
margin: 0;
padding: 10px 8px;
color: var(--heybox-up-muted, #6b7280);
font-size: 12px;
line-height: 1.6;
}
#page-bbs-community .heybox-up-hot-target {
border-color: var(--heybox-up-accent, #6f8db3) !important;
box-shadow: 0 0 0 3px rgba(111, 141, 179, .18) !important;
}
@media (min-width: 1200px) {
/* 顶部搜索条会横跨信息流、压住左栏上沿;桌面把搜索入口收进左栏“浏览”。
站点还用固定 ::before 给搜索条铺 146px 高的遮罩;搜索条隐藏后把遮罩压到
导航高度,既保留导航下方 16px 的顶部留白,也避免滚动时正文从留白处露出。 */
#page-bbs-community.heybox-up-desktop .bbs-community__search-module {
display: none !important;
}
#page-bbs-community.heybox-up-desktop::before {
bottom: auto !important;
height: 80px !important;
}
/* 站点还让 main.list 的 8px sticky 伪元素停在原搜索条下沿;搜索条隐藏后
它会在 138px 处横切信息流并截获点击。保留其占位,取消吸顶与绘制。 */
#page-bbs-community.heybox-up-desktop main.list::before {
position: static !important;
background: transparent !important;
pointer-events: none !important;
}
#page-bbs-community.heybox-up-desktop .content {
display: grid !important;
width: 100% !important;
/* 左右两列等宽,中间栏永远落在视口正中;两侧栏贴近中间栏而不是贴屏幕边。 */
grid-template-columns:
minmax(0, 1fr)
/* 中栏再宽也要给两侧栏留出 208px + 288px + 间距,否则侧栏会压到正文上。 */
minmax(320px, min(var(--heybox-up-feed-w), calc(100vw - 560px)))
minmax(0, 1fr);
column-gap: 24px;
align-items: start;
max-width: none !important;
margin: 0 auto !important;
justify-content: stretch !important;
}
#page-bbs-community.heybox-up-desktop .content > #heybox-up-left-rail {
display: block;
grid-column: 1;
grid-row: 1;
justify-self: end;
width: 208px;
align-self: start;
}
#page-bbs-community.heybox-up-desktop .content > main.list {
grid-column: 2;
grid-row: 1;
width: auto !important;
max-width: none !important;
min-width: 0;
margin: 0 !important;
flex: none !important;
}
/* 评论预览只占右侧一列的真实宽度。旧实现再叠一层“侧栏 + 48px”右内边距,
会在面板与信息流之间留下近乎双倍于面板的空白,并把中栏挤向左侧。 */
body.heybox-up-rail-open #app > main { max-width: none !important; }
body.heybox-up-rail-open #page-bbs-community.heybox-up-desktop .content {
grid-template-columns:
minmax(var(--heybox-up-rail-w), 1fr)
minmax(320px, min(var(--heybox-up-feed-w), calc(100% - var(--heybox-up-rail-w) - var(--heybox-up-rail-w) - 48px)))
minmax(var(--heybox-up-rail-w), 1fr);
column-gap: 24px;
}
/* 左栏已有频道列表,原生横向频道条不再重复出现。 */
#page-bbs-community.heybox-up-desktop .bbs-home__topic-list-wrapper {
display: none !important;
}
}
@media (min-width: 1500px) {
body.heybox-up-desktop-on #app > main {
max-width: none !important;
}
body.heybox-up-desktop-on #page-bbs-community {
max-width: none !important;
width: auto !important;
}
#page-bbs-community.heybox-up-desktop .content > #heybox-up-right-rail {
display: block;
grid-column: 3;
grid-row: 1;
justify-self: start;
width: 288px;
align-self: start;
}
}
@media (min-width: 1500px) {
body.heybox-up-rail-open #page-bbs-community.heybox-up-desktop #heybox-up-right-rail {
display: none;
}
}
`;
function ensureDesktopStyle() {
let style = document.getElementById(DESKTOP_STYLE_ID);
if (!style) {
style = document.createElement("style");
style.id = DESKTOP_STYLE_ID;
style.textContent = DESKTOP_CSS;
} else if (style.textContent !== DESKTOP_CSS) {
style.textContent = DESKTOP_CSS;
}
const parent = document.head || document.documentElement;
if (parent && style.parentNode !== parent) parent.append(style);
}
function parseCompactCount(value) {
const match = /([0-9]+(?:\.[0-9]+)?)\s*(亿|万)?/.exec(value || "");
if (match === null) return 0;
const base = Number(match[1]);
if (!Number.isFinite(base)) return 0;
const unit = match[2] === "亿" ? 100000000 : match[2] === "万" ? 10000 : 1;
return Math.round(base * unit);
}
function createDesktopRailLink(item, active) {
const link = document.createElement("a");
link.className = "rail-link" + (active === true ? " is-active" : "");
link.href = item.href;
link.dataset.route = item.id;
link.innerHTML = item.icon;
const label = document.createElement("span");
label.textContent = item.label;
link.append(label);
link.addEventListener("click", (event) => {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigateToPath(item.href);
});
return link;
}
function createDesktopLeftRail() {
const root = document.createElement("aside");
root.id = DESKTOP_LEFT_ID;
root.setAttribute("aria-label", "浏览与频道");
const browse = document.createElement("section");
browse.className = "rail-block";
const browseTitle = document.createElement("p");
browseTitle.className = "rail-title";
browseTitle.textContent = "浏览";
browse.append(browseTitle);
const links = [
{ id: "community", label: "社区首页", href: "/app/bbs/home", icon: NAV_ICONS.community },
{ id: "favour", label: "我的收藏", href: "/app/user/favour/content", icon: NAV_ICONS.favour },
{ id: "article", label: "我的文章", href: "/creator/content_management/home?article_type=all", icon: NAV_ICONS.article },
{ id: "search", label: "全站搜索", href: "/app/search/list", icon: COMMUNITY_SEARCH_ICONS.search },
];
for (const item of links) browse.append(createDesktopRailLink(item, item.id === "community"));
const channelBlock = document.createElement("section");
channelBlock.className = "rail-block";
const channelTitle = document.createElement("p");
channelTitle.className = "rail-title";
channelTitle.textContent = "频道";
const channels = document.createElement("div");
channels.className = "channels";
channelBlock.append(channelTitle, channels);
root.append(browse, channelBlock);
return { root, channels };
}
function createDesktopRightRail() {
const root = document.createElement("aside");
root.id = DESKTOP_RIGHT_ID;
root.setAttribute("aria-label", "热门与快捷入口");
const hotBlock = document.createElement("section");
hotBlock.className = "rail-block";
const hotTitle = document.createElement("p");
hotTitle.className = "rail-title";
hotTitle.textContent = "热门排行";
const hot = document.createElement("ol");
hot.className = "hot-list";
hotBlock.append(hotTitle, hot);
const toolsBlock = document.createElement("section");
toolsBlock.className = "rail-block";
const toolsTitle = document.createElement("p");
toolsTitle.className = "rail-title";
toolsTitle.textContent = "快捷入口";
toolsBlock.append(toolsTitle);
const tools = [
{ id: "favour", label: "我的收藏", href: "/app/user/favour/content", icon: NAV_ICONS.favour },
{ id: "article", label: "我的文章", href: "/creator/content_management/home?article_type=all", icon: NAV_ICONS.article },
];
for (const item of tools) toolsBlock.append(createDesktopRailLink(item, false));
root.append(hotBlock, toolsBlock);
return { root, hot };
}
function readDesktopChannels() {
return Array.from(document.querySelectorAll("#page-bbs-community .bbs-home__topic-item")).map((button) => {
const nameNode = button.querySelector(".bbs-home__topic-name");
const imageNode = button.querySelector("img.bbs-home__topic-item-icon");
const name = normalizeRailText(nameNode === null ? button.textContent : nameNode.textContent).slice(0, 24);
return { name, icon: imageNode === null ? "" : imageNode.getAttribute("src") || "" };
}).filter((entry) => entry.name !== "");
}
function syncDesktopChannels(ui, cache) {
const channels = readDesktopChannels();
const signature = channels.map((entry) => entry.name + "\u0001" + entry.icon).join("\u0002");
if (signature === cache.channels) return;
cache.channels = signature;
ui.channels.textContent = "";
if (channels.length === 0) {
const empty = document.createElement("p");
empty.className = "rail-empty";
empty.textContent = "频道列表还没渲染好";
ui.channels.append(empty);
return;
}
for (const channel of channels) {
const button = document.createElement("button");
button.type = "button";
button.className = "rail-channel";
if (channel.icon !== "") {
const icon = document.createElement("img");
icon.src = channel.icon;
icon.alt = "";
icon.loading = "lazy";
icon.referrerPolicy = "no-referrer";
button.append(icon);
}
const name = document.createElement("span");
name.className = "name";
name.textContent = channel.name;
button.append(name);
button.addEventListener("click", () => {
const native = Array.from(document.querySelectorAll("#page-bbs-community .bbs-home__topic-item")).find((node) => {
const label = node.querySelector(".bbs-home__topic-name");
return normalizeRailText(label === null ? node.textContent : label.textContent) === channel.name;
});
if (!native) return;
for (const node of ui.channels.querySelectorAll(".rail-channel")) node.classList.remove("is-active");
button.classList.add("is-active");
native.click();
});
ui.channels.append(button);
}
}
function readDesktopHotItems() {
return Array.from(document.querySelectorAll("#page-bbs-community .bbs-home__content-item")).map((card, index) => {
const link = card.querySelector("a[href*='/app/bbs/link/']");
const href = link === null ? "" : link.getAttribute("href") || "";
const idMatch = /\/app\/bbs\/link\/(\d+)/.exec(href);
const titleNode = card.querySelector(".bbs-content__title");
const title = normalizeRailText(titleNode === null ? card.textContent : titleNode.textContent).slice(0, 80);
const commentNode = card.querySelector(".bbs-new-style-bottom__comment, .content-list__comment");
const likeNode = card.querySelector(".bbs-new-style-bottom__like, .content-list__like");
const comment = parseCompactCount(commentNode === null ? "" : commentNode.textContent);
const like = parseCompactCount(likeNode === null ? "" : likeNode.textContent);
return { card, index, id: idMatch === null ? "i" + index : idMatch[1], title, comment, like, score: comment * 2 + like };
}).filter((entry) => entry.title !== "").sort((left, right) => right.score - left.score).slice(0, DESKTOP_HOT_LIMIT);
}
function syncDesktopHot(ui, cache) {
const items = readDesktopHotItems();
const signature = items.map((item) => item.id + ":" + item.score + ":" + item.title).join("|");
if (signature === cache.hot) return;
cache.hot = signature;
ui.hot.textContent = "";
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "rail-empty";
empty.textContent = "信息流还没有可排序的热度数据";
ui.hot.append(empty);
return;
}
items.forEach((item, index) => {
const row = document.createElement("li");
const button = document.createElement("button");
button.type = "button";
button.className = "hot-item";
const rank = document.createElement("span");
rank.className = "hot-rank";
rank.textContent = String(index + 1);
const title = document.createElement("span");
title.className = "hot-title";
title.textContent = item.title;
const meta = document.createElement("span");
meta.className = "hot-meta";
const parts = [];
if (item.comment > 0) parts.push(item.comment + " 评论");
if (item.like > 0) parts.push(item.like + " 赞");
meta.textContent = parts.length === 0 ? "当前信息流" : parts.join(" · ");
button.append(rank, title, meta);
button.addEventListener("click", () => {
item.card.scrollIntoView({ behavior: "smooth", block: "center" });
item.card.classList.add(DESKTOP_HOT_TARGET_CLASS);
window.setTimeout(() => item.card.classList.remove(DESKTOP_HOT_TARGET_CLASS), 1400);
});
row.append(button);
ui.hot.append(row);
});
}
function watchDesktopShell() {
if (!document.body) return;
const left = createDesktopLeftRail();
const right = createDesktopRightRail();
const cache = { channels: "", hot: "" };
let scheduled = false;
const teardown = () => {
document.body.classList.remove("heybox-up-desktop-on");
const page = document.querySelector("#page-bbs-community");
if (page !== null) page.classList.remove(DESKTOP_PAGE_CLASS);
if (left.root.parentNode !== null) left.root.remove();
if (right.root.parentNode !== null) right.root.remove();
};
const sync = () => {
const page = document.querySelector("#page-bbs-community");
const content = page === null ? null : page.querySelector(".content");
if (page === null || content === null || page.querySelector("main.list") === null) {
teardown();
return;
}
ensureDesktopStyle();
page.classList.add(DESKTOP_PAGE_CLASS);
document.body.classList.add("heybox-up-desktop-on");
if (left.root.parentNode !== content) content.prepend(left.root);
if (right.root.parentNode !== content) content.append(right.root);
syncDesktopChannels(left, cache);
syncDesktopHot(right, cache);
};
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
sync();
});
}).observe(document.body, { childList: true, subtree: true });
window.addEventListener("popstate", () => requestAnimationFrame(sync));
sync();
}
function boot() {
applyHideCyPreference(readHideCyPreference());
watchTheme();
watchTopNav();
watchCommentSort();
watchFavourPage();
watchCommentRail();
watchPostPager();
watchCommunitySearch();
watchDesktopShell();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
})();