评论区回复默认展开 2 条,长串收在「展开更多」后面

1985 字
10 分钟
评论区回复默认展开 2 条,长串收在「展开更多」后面
评论区回复默认展开 2 条,长串收在「展开更多」后面

留言板里一条评论下面挂着 13 条回复,往下翻要滑过整整一屏。评论区本来是用来交流的,被长回复串这么一撑,反倒成了劝退现场。

第一版我做的是「回复全部默认收起,只留一个展开按钮」。真跑起来一看,方向偏了:访客第一眼看到的评论下面全是空的,每条想看的都得先点一下,比滑屏还烦。

于是改成默认展示 2 条,多出来的才收着 —— 有两条内容垫底,一眼能看出这条评论有没有人聊过,长度也可控。还有剩就给个「展开更多(还剩 M 条)」,每次再放 5 条;全部摊开后按钮变「收起回复」,箭头翻上去,点一下回到默认的 2 条。

默认展示 2 条,剩下的收在「展开更多」后面
默认展示 2 条,剩下的收在「展开更多」后面

只处理回复这一层#

顶层评论保持原样、全部显示 —— 那是评论区的主体,收起来访客第一眼就什么都看不到了。要处理的是每条评论下面那一串回复。这条线划清楚之后,剩下的都是实现问题。

还有一条边界没得商量:回复本来就不超过 2 条的评论,我们完全不碰 —— 不折叠、不打标记、连按钮都不插,跟没这个功能一样。只在真正会长的地方动手,是让这个功能不惹人烦的前提。

文件状态干什么
src/config/FooterConfig.html改(追加)按钮样式 + PAGE_SCRIPTS 全站加载条目
public/assets/js/comment-reply-collapse.js新增插按钮、切可见条数、盯着 DOM 变化

还是本站用惯的路子:页脚在 #swup-container 之外,切页不会被替换掉,所以样式写在它的 <style> 块里;脚本挂进 PAGE_SCRIPTS 全站加载(正则 /.*/,跟「最近评论」那条一样),没有 #waline 的页面自动空转。没有改任何主题源文件。

先搞清楚 Waline 渲染成什么#

Waline 是 Vue 应用,DOM 不是写死的,动手前得先把渲染结果 dump 出来看。第一次抓就抓错了:#waline .wl-comment 看起来最像「一条评论」,其实它是编辑器面板(.wl-comment > .wl-panel,里面是昵称、邮箱、网址三件套加一个 textarea)。

真正的结构是这样:

#waline
└── div
├── .wl-comment ← 编辑器(发评论那个框)
└── div
├── .wl-meta-head ← 「25 评论」+ 按正序 / 倒序 / 热度
├── .wl-cards ← 评论列表
│ └── .wl-card-item × 8 ← 顶层评论(id 就是评论的 objectId)
│ ├── .wl-user ← 左侧头像列
│ └── .wl-card
│ ├── .wl-head ← 昵称 / 时间 / 点赞 / 回复
│ ├── .wl-meta ← 归属地 / 浏览器 / 系统
│ ├── .wl-content ← 正文
│ └── .wl-quote ← 回复全挂在这里
│ └── .wl-card-item × N
└── .wl-meta-foot ← RSS 订阅 · Powered by Waline v3.16.1

这里有两个必须先确认的事实。

一是回复不是嵌套的顶层评论,二级条目并不是 .wl-cards 的直接子节点,而是塞在每张卡片自己的 .wl-quote 里。友链页显示「25 评论」= 8 条顶层 + 17 条回复,其中只有 4 条评论带回复,最长的那条挂了 13 条。

二是没有回复时 .wl-quote 依然存在。8 条顶层评论对应 8 个 .wl-quote,但只有 4 个里面有东西。所以判定条件不能写成「有没有 .wl-quote」,得看它有没有 .wl-card-item 直接子节点。

折叠的时候只认直接子#

Waline 里点「回复」会在原位插入一个编辑框,回复的回复也会往 .wl-quote 里塞条目。如果按 querySelectorAll(".wl-card-item") 一把捞,捞出来的可能混着深层嵌套的条目,层级就乱了 —— 数出来的「N 条回复」是错的,按序号切可见条数也会切错位置。

所以取子项一律走直接子:

function directItems(quote) {
var out = [];
for (var i = 0; i < quote.children.length; i++) {
var el = quote.children[i];
if (el.classList && el.classList.contains("wl-card-item")) out.push(el);
}
return out;
}

好处是那个编辑框压根不会被算进来,它没有 wl-card-item 这个类。按钮也一样,找它的时候只翻直接子,避免抓到深层同名元素。

显示控制也不走内联 style,而是加一个类,把样式留在页脚那份 CSS 里:

.wl-quote > .ff-rp-hidden {
display: none;
}

不写内联是因为不想跟 Vue 自己的 style 绑定打架;选择器前面挂 .wl-quote > 是把优先级抬到 (0,2,0),稳过 Waline 自带的 .wl-card-item,同时又是未分层 CSS,也压得过主题 @layer 里的规则。

观察器挂 body,不挂 #waline#

Waline 的渲染是异步的:#waline 这个容器先由 Astro 吐出来,评论列表要等客户端脚本从服务端拉完数据才长出来。而本站又套了 swup 无刷新切页,#waline 会随着页面容器被整个换掉。

如果观察器挂在 #waline 上,换页那一刻它监视的节点就没了,之后新渲染出来的评论区再也没人管 —— 表现就是「首页进去有折叠,站内点进文章页就没了」。挂在 document.body 上就没这问题,换页后重新长出来的节点照样能接住:

new MutationObserver(schedule).observe(document.body, {
childList: true,
subtree: true,
});

代价是回调会被全站所有 DOM 变动触发,所以加了 160ms 防抖,并且第一件事就是 getElementById("waline"),没有就直接返回。

Vue 重绘会把按钮带走#

这是最需要防的一条。折叠状态是我们手动打在 DOM 上的(一个按钮节点 + 一个 data-ff-shown),而 Waline 会在切换排序、提交评论、加载分页时重绘列表。节点被重建之后,我们插的按钮可能没了,dataset 也可能残留 —— 要是只认 dataset,就会出现「标记说处理过了、但按钮其实不在」,那条评论的回复从此永久失踪。

所以判定写成了双重条件:标了已处理,且按钮确实还在,才算处理过。任一条不满足就重新接一遍。

// 按钮还在就说明这条已经接过了,别重复插
if (quote.dataset.ffRp === "1" && findButton(quote)) return;

按钮只在会长的地方出现#

判定很糙,但很关键:

// 回复本来就没超过默认条数:原样摊着,不插按钮,也不打标记
if (items.length <= DEFAULT_SHOWN) return;

注意这里连标记都不打。打过标记的意思是「这条我处理过了,以后不用再看」,而短回复串是会变长的 —— 有人接着回两条,它就超了。留着未处理状态,下一轮扫描(Waline 重绘时会触发)就能把它接上,不用刷新页面。

代价是按钮位置会随回复内容高度浮动,不像固定在每张卡片底部那样整齐。换来的是评论区里只有真正长的地方才多出一行东西。

实测#

在本地 dev 上对着友链页跑了几轮:

友链页 8 条顶层评论,回复数分别是 2 / 13 / 1 / 0 / 0 / 1 / 0 / 0:

场景结果
首屏渲染只有那条 13 回复的插了按钮:「展开更多(还剩 11 条)」,前 2 条平铺;其余 7 条(回复 2 / 1 / 0 条)完全没碰
点「展开更多」7 条,按钮变「还剩 6 条」
再点12 条,变「还剩 1 条」
再点13 条全部显示,变「收起回复」,箭头翻转
再点回到默认的 2 条,变回「展开更多(还剩 11 条)」
swup 无刷新切页(首页 → 友链页)按钮照常出现,无丢失
切换排序(整个列表重渲染)按钮数量不变、每条评论最多 1 个、无重复插入,回到默认 2 条

点开一次变成 7 条,按钮跟进到「还剩 6 条」
点开一次变成 7 条,按钮跟进到「还剩 6 条」

折叠的是噪声,不是内容 —— 想聊的人照样点一下就看全,不想看的人不用再被一条 13 连回复的串推着往下滑。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
评论区回复默认展开 2 条,长串收在「展开更多」后面
https://www.9ll.uk/posts/blog/commentreplycollapse/
作者
HY
发布于
2026-10-15
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
回复带上引用:别再让读者猜「@昵称」在回哪一句
博客Waline 的二级回复只渲染一行「@昵称:」,谁在回谁很清楚,回的是哪一句全靠猜。翻 DOM 发现它其实留了线索 —— 回复里那个 @昵称 的链接 href 就是被回复评论的 objectId。顺着锚点把目标评论捞回来,摘成引用块插在下面,点它还能滚过去高亮。踩了个坑:剥 @ 前缀时忘了 clone,把页面上那行整个删了。
2
全站加了个「最近评论」:数据直连 Waline,主题文件一个没改
博客在参考站的右栏看到「最近评论」组件,想在自己站上也挂一个,而且要求全站都有。评论数据就在自己的 Waline 后端里,直接调它的公开接口;DOM 和样式走页脚注入,脚本全站加载,桌面挂右栏、移动端挂底部组件栏。踩了三个坑:Waline 的 orig 字段是渲染前的 HTML、类名和外包装撞车吃掉 32px 内边距、窄栏 flex 布局把昵称挤成一个字。
3
全站鼠标指针换成《俺妹》主题:亮色桐乃、深色黑猫
博客这次更新把整站的鼠标指针换了一遍:亮色主题配高坂桐乃、深色主题配五更琉璃(黑猫),各 28 枚共 56 枚,正常选择、链接、文本、禁用、拖拽这些场景全部覆盖。素材来自两张总览图,切图时发现它们不是普通白底图 —— 人物衣服和箭头内部都是纯白,跟我页面的白同色,按我处理贴纸那套「全局去白底」会把图形整个掏空。CSS 那边我把「用哪张图 + 热点坐标」全塞进了自定义属性,28 条规则只写一遍,切换主题只换一组变量。
4
把动态页改成了朋友圈的样子
博客动态页想换个样子,参照的是另一个同主题的站 —— 但数据源不变,还是继续接 memos。扒完两边才发现,上游动态卡片的 DOM 本来就齐 —— 头像、作者、时间、地点、置顶徽章、图片网格、评论按钮一个不缺,差的只是样式。于是这轮改造零改动主题源文件:一份只在动态页加载的样式表,加一个把页脚外壳搬进正文的小脚本。中间踩了三个安静的坑,都是「不报错、但就是不生效」那种。
5
全站加了个「天气预报」:接口免费,而且按访客 IP 定位
博客刷参考站时看到他右栏挂着一张天气预报卡,从标题竖条到「收起」按钮整个搬了过来。接口在 uapis.cn,免费、不用 key,而且不传参数时按访客 IP 定位 —— 谁来串门看到的就是谁头顶上的天。踩了两个坑:参考站那套图标是 Astro 构建期渲染的 symbol、动态插进去取不到;指标网格的 auto-fit 在 280px 右栏只排得出一列,卡片被顶到 925px。
随机文章随机推荐

评论区

Profile Image of the Author
HY
Hello, I'm HY.
公告
迷茫.... 为了访问速度,本站将会在11月上旬至中旬迁移到0dm.cn并完成备案
分类
标签
最新动态
站点统计
文章
74
分类
8
标签
192
总字数
161,979
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录