仿微信聊天系统源码WinForm实战:TCP长连接与消息不丢不卡顿拆解
简介这是一份基于WinForm技术实现的仿微信聊天系统源码面向C#编程初学者及对Windows桌面应用开发感兴趣的开发者可作为学习即时通讯客户端架构的实践参考。项目围绕WinForm控件布局、Socket网络通信、多线程与异步处理、XML/JSON序列化、SQLite等轻量数据库存储、用户认证授权、消息推送更新以及事件驱动编程等核心知识点展开帮助读者理解从界面搭建到消息收发的完整流程。压缩包共1274个文件以316个dll依赖库、267个xml配置、119个cs源码、18个resx资源及66张png界面素材为主另含csproj、sln等工程文件整体约45.3MB目录结构便于按模块查阅。目前已有849人学习下载。通过研读源码读者可掌握WinForm桌面应用的开发流程与简单IM系统的实现思路并为进一步学习WPF、UWP或Web应用开发打下基础。1. 仿微信聊天系统源码WinForm 桌面端 IM 的最小可用拆解拿到「仿微信聊天系统源码(基于WinForm实现).zip」这个标题多数人的第一反应是解压、双击 sln、F5 跑起来看看长什么样。但真正做过桌面 IM 的人会先问三个问题通信层用什么、消息怎么落库、UI 用什么控件扛住高频刷新。WinForm 做聊天界面本身不难难的是把「看起来像微信」和「消息不丢、不乱序、不卡 UI」这两件事同时做到。这套源码方向适合两类人一是想拿一个完整 C# WinForm 项目练手、顺便理解 TCP 长连接和消息队列的初学者二是手上已经有业务系统想嵌一个内部即时通讯模块的工程师。它解决的不是「做一个微信」而是「用 WinForm 把一套可运行的 C/S 聊天链路跑通」包括登录、好友列表、单聊、消息气泡、离线消息这几个核心闭环。下面按「先跑通、再拆解、后避坑」的顺序讲每一步都落到能复现的命令和参数上。2. 环境准备与源码结构把 WinForm 聊天项目在本地跑起来2.1 开发环境与依赖版本怎么选这套源码基于 WinForm意味着它锁死在 .NET Framework 体系里而不是 .NET 6/8 的跨平台路线。常见做法是用 Visual Studio 2015 或 2017 打开因为热词里反复出现 vs2015很多老项目的 TargetFramework 就是 v4.5 或 v4.5.2。如果你装的是 VS2022打开时可能会提示「目标框架未安装」这时候不要急着改框架版本先去 Visual Studio Installer 里勾选「.NET Framework 4.5 目标包」和「.NET Framework 4.5.2 开发工具」否则一堆引用会飘红。判断项目框架版本的方法很简单用文本编辑器打开 .csproj 文件找这几行!-- 打开任意 .csproj确认目标框架和输出类型 -- TargetFrameworkVersionv4.5.2/TargetFrameworkVersion OutputTypeWinExe/OutputType UseWindowsFormstrue/UseWindowsFormsTargetFrameworkVersion决定你能用哪些 APIOutputType为 WinExe 说明是桌面程序而非控制台。如果源码里同时有 Client、Server、Common 三个项目说明它是标准 C/S 结构Client 是 WinForm 界面Server 是控制台或 Windows 服务Common 放协议实体和工具类。先确认这三个项目都能编译再谈运行。2.2 数据库与连接字符串配置聊天系统离不开消息存储这类源码通常用 SQL Server少数用 SQLite。先找 App.config 或 App.Config 里的连接字符串!-- App.config 中的连接配置按本机实例名修改 -- connectionStrings add nameChatDb connectionStringData Source.;Initial CatalogWeChatLike;User IDsa;Password你的密码 providerNameSystem.Data.SqlClient / /connectionStringsData Source.表示本机默认实例如果你装的是 Express 版要写成.\SQLEXPRESS。Initial Catalog是数据库名源码包里一般会带一个 .sql 建库脚本先在 SSMS 里执行它把表结构建出来。常见的表有 User用户、Friend好友关系、Message消息记录、OfflineMessage离线消息。建完表后把连接字符串里的账号密码改成你本机的再启动 Server 项目看到控制台打印「服务已启动监听端口 8888」之类的日志才算通信层活了。2.3 启动顺序与端口占用排查C/S 项目最容易翻车的地方是启动顺序。正确顺序是先跑 Server再跑 Client而且 Client 可以多开几个实例模拟多用户。如果 Client 一连就断先查端口# Windows 下查看端口占用确认 8888 没被别的进程抢 netstat -ano | findstr :8888 # 如果被占用用 tasklist 找到进程名 tasklist | findstr 进程PID端口被占用是新手最常见的坑尤其是本机装了其他 IM 工具或调试工具时。改端口要同时改 Server 的监听端口和 Client 的连接端口两边必须一致。另外Windows 防火墙可能拦截首次监听的 Server 进程弹窗时要点「允许访问」否则本机 Client 都连不上。跑通之后你会看到一个仿微信的登录窗输入账号密码进主界面左边好友列表右边聊天区这就是最小可用状态。3. 通信层拆解TCP 长连接、粘包处理与消息协议设计3.1 为什么用 TCP 而不是 HTTP 轮询聊天系统的核心是「服务器能主动推消息给客户端」。HTTP 是请求-响应模型服务器没法主动推只能靠客户端轮询延迟高、开销大。所以这类源码基本都用 TCP 长连接客户端登录后保持一条 Socket 连接服务器收到消息后遍历在线连接转发。WinForm 里用System.Net.Sockets.TcpClient和TcpListener就够了不需要引入第三方库。关键点是一条 TCP 连接是字节流没有消息边界。你发两次「你好」和「在吗」接收端可能一次收到「你好在吗」这就是粘包。解决办法是自定义协议头常见格式是「4 字节长度 消息体」。下面是一个最小实现// 发送先写 4 字节长度再写 JSON 消息体 public static void SendMessage(NetworkStream stream, string json) { byte[] body Encoding.UTF8.GetBytes(json); byte[] header BitConverter.GetBytes(body.Length); // 小端序4 字节 stream.Write(header, 0, header.Length); stream.Write(body, 0, body.Length); stream.Flush(); } // 接收先读满 4 字节头再按长度读满消息体 public static string ReadMessage(NetworkStream stream) { byte[] header ReadExactly(stream, 4); int len BitConverter.ToInt32(header, 0); byte[] body ReadExactly(stream, len); return Encoding.UTF8.GetString(body); }BitConverter.GetBytes默认小端序收发两端必须一致否则长度解析错位。ReadExactly要自己实现因为NetworkStream.Read不保证一次读满必须循环读直到凑够字节数。参数上长度用 int 足够单条消息超过 2GB 不现实消息体用 UTF8 编码中文不会乱码。这套协议简单但够用是绝大多数 WinForm 聊天源码的通用做法。3.2 消息实体与 JSON 序列化选型消息体一般用 JSON字段包括消息类型、发送者、接收者、内容、时间戳。序列化工具常见两种Newtonsoft.Json和System.Text.Json。老项目多用 Newtonsoft因为 .NET Framework 4.5 时代 System.Text.Json 还没出生。实体类大概长这样public class ChatMessage { public string Type { get; set; } // Login / Chat / Logout / Heartbeat public string From { get; set; } // 发送者账号 public string To { get; set; } // 接收者账号 public string Content { get; set; } // 消息正文 public long Timestamp { get; set; } // Unix 毫秒时间戳 }Type字段是协议路由的关键服务器收到后 switch 分发Login 走登录校验Chat 走转发和落库Heartbeat 只回一个心跳包维持连接。时间戳用 Unix 毫秒而不是 DateTime避免时区和序列化格式差异。注意 JSON 序列化时不要带 BOM否则接收端解析会多出不可见字符这是血泪经验。3.3 心跳机制与断线重连长连接不可能永远不断网络抖动、路由器超时都会让连接悄悄死掉。所以必须有心跳客户端每隔 30 秒发一个 Heartbeat服务器收到后回一个 Heartbeat连续 3 次没收到就判定断线触发重连。重连逻辑要放在独立线程里不能阻塞 UI 线程。// 心跳定时器30 秒一次连续 3 次失败触发重连 private int _missedHeartbeats 0; private void HeartbeatTimer_Tick(object sender, EventArgs e) { if (_missedHeartbeats 3) { Reconnect(); // 重连先关旧连接再新建 TcpClient _missedHeartbeats 0; return; } try { SendMessage(_stream, {\Type\:\Heartbeat\}); } catch { _missedHeartbeats; } }_missedHeartbeats是失败计数器发送异常就加一成功就清零。重连时要注意先释放旧 Socket否则句柄泄漏。心跳间隔不要设太短30 秒是经验值太短浪费流量太长断线发现慢。这套机制跑通后你把网线拔了再插上客户端应该能自动恢复这是验证通信层是否健壮的最直接方法。4. WinForm 界面实现消息气泡、好友列表与 UI 不卡顿4.1 用 ListBox 还是自定义控件画消息气泡微信聊天界面的核心是消息气泡左右分栏自己发的在右边对方发的在左边。WinForm 原生控件没有气泡常见做法有两种一是用RichTextBox拼 HTML 式内容二是自定义UserControl画气泡。前者简单但样式受限后者灵活但要处理绘制和测量。我一般推荐自定义控件因为可控性强也方便做头像、时间戳、已读状态。自定义气泡的关键是重写OnPaint用Graphics画圆角矩形和文字protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 抗锯齿气泡边缘才不毛糙 Rectangle rect new Rectangle(0, 0, this.Width - 1, this.Height - 1); using (GraphicsPath path GetRoundRect(rect, 8)) // 8 是圆角半径 using (Brush brush new SolidBrush(IsSelf ? Color.LightGreen : Color.White)) { g.FillPath(brush, path); g.DrawString(Content, Font, Brushes.Black, new PointF(10, 10)); } }SmoothingMode.AntiAlias必须开否则圆角会有锯齿界面美化就无从谈起。GetRoundRect是自己写的圆角路径方法圆角半径 8 像素接近微信观感。IsSelf决定气泡颜色和左右对齐。注意DrawString的换行要手动处理长文本要按宽度折行否则会溢出气泡。4.2 好友列表与未读消息红点好友列表通常用ListView或DataGridView前者轻量后者适合带多列数据。红点提示用自绘实现在ListView的DrawItem事件里判断该好友有未读消息就画一个红色圆点加数字。未读计数存在内存字典里收到消息时更新点开聊天窗口时清零。private Dictionarystring, int _unread new Dictionarystring, int(); private void FriendList_DrawItem(object sender, DrawListViewItemEventArgs e) { e.DrawDefault true; string friendId e.Item.Tag as string; if (_unread.ContainsKey(friendId) _unread[friendId] 0) { e.Graphics.FillEllipse(Brushes.Red, e.Bounds.Right - 20, e.Bounds.Top 5, 16, 16); e.Graphics.DrawString(_unread[friendId].ToString(), Font, Brushes.White, e.Bounds.Right - 18, e.Bounds.Top 6); } }e.DrawDefault true表示先按默认样式画再叠加红点这样不用自己画整行。红点位置按e.Bounds动态算列表滚动也不会错位。未读字典的 key 用好友账号value 是条数收到消息时_unread[from]打开窗口时移除 key。4.3 跨线程更新 UI 与 Invoke 的正确姿势Socket 接收是在后台线程跑的收到消息后要更新 UI但 WinForm 控件只能由创建它的线程访问跨线程直接改会抛InvalidOperationException。正确做法是用Control.Invoke或BeginInvoke切回 UI 线程private void OnMessageReceived(ChatMessage msg) { if (this.InvokeRequired) // 判断是否在非 UI 线程 { this.BeginInvoke(new ActionChatMessage(OnMessageReceived), msg); return; } // 到这里已经在 UI 线程可以安全操作控件 AppendMessageToChatBox(msg); UpdateUnreadCount(msg.From); }InvokeRequired判断当前线程是否是 UI 线程是就直接执行不是就BeginInvoke排队。用BeginInvoke而不是Invoke因为Invoke是同步阻塞高频消息下会拖慢接收线程。这是 WinForm 聊天系统 UI 不卡顿的关键很多人界面一卡一卡的就是这里写错了。5. 避坑与排查仿微信聊天源码最常见的 5 个翻车点5.1 现象消息偶尔丢失或顺序错乱原因TCP 只保证字节流可靠不保证你「一次发送对应一次接收」。如果接收端没有按协议头读满长度或者多线程同时写同一个 Socket消息就会交错。解决所有发送走同一个锁接收端严格按「4 字节长度 消息体」循环读读满才解析。顺序问题可以在消息体里带自增序号接收端按序号排序后再显示。5.2 现象客户端一启动就闪退无异常提示原因多半是连接字符串错、数据库没建表或者 Server 没启动。WinForm 默认的异常弹窗有时被吞掉。解决在Program.cs的Application.Run外面包一层 try-catch把异常写到日志文件同时确认 Server 先于 Client 启动数据库脚本已执行。用netstat确认端口在监听。5.3 现象中文消息显示成乱码原因编码不一致。发送端用 UTF8接收端用 Default或者反过来。解决全链路统一 UTF8包括Encoding.UTF8.GetBytes和Encoding.UTF8.GetStringJSON 序列化也指定 UTF8。数据库字段用nvarchar而不是varchar否则中文存进去就变问号。5.4 现象界面发消息后卡住几秒原因在 UI 线程里做了同步网络发送或数据库写入。解决发送消息丢到线程池或Task.Run里执行UI 线程只负责把消息追加到气泡列表。数据库写入也异步化或者用消息队列缓冲。记住一条UI 线程只碰控件不碰 IO。5.5 现象打包成安装程序后连不上服务器原因开发时连的是localhost打包后客户端装到别的机器连接字符串或服务器 IP 还是本机。解决把服务器地址做成配置文件或登录界面可填不要硬编码。用 VS 自带的 Installer Projects 或 Inno Setup 打包时确认配置文件随安装包一起分发且安装目录有写权限。6. 进阶技巧把聊天记录做成可检索的本地缓存跑通基础功能后真正拉开差距的是消息检索。微信能搜聊天记录靠的是本地数据库索引。WinForm 项目里可以引入 SQLite 做客户端本地缓存把收到的消息同步写一份到本地查询时直接走 SQL不用每次向服务器要。建表时给Content字段加全文索引或者简单点用LIKE加时间范围。-- SQLite 本地消息表按会话和时间建索引 CREATE TABLE LocalMessage ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SessionId TEXT NOT NULL, -- 会话标识单聊用对方账号 FromUser TEXT NOT NULL, Content TEXT NOT NULL, Timestamp INTEGER NOT NULL ); CREATE INDEX idx_session_time ON LocalMessage(SessionId, Timestamp);SessionId是会话维度单聊就是对方账号群聊就是群 ID。索引建在(SessionId, Timestamp)上按会话查最近 N 条时走索引速度很快。写入用事务批量提交比如每 20 条或每 2 秒 flush 一次避免频繁 IO。验证方法很直接断开服务器打开历史会话搜索关键词能秒出结果就说明本地缓存生效了。再进一步可以把搜索做成增量加载滚动到底部时再查更早的记录避免一次性加载几万条卡死界面。我自己做这类项目最大的习惯是任何网络回调先切 UI 线程任何 IO 先想好失败重试任何配置都不硬编码。这套源码方向值不值得投入如果你只是想练手它能让你把 TCP、协议设计、WinForm 自绘、异步 UI 这几块串起来性价比很高如果要上生产通信层和存储层得按上面的思路加固别直接拿 demo 当成品。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

数据库课设实战:电力公司收费系统表结构设计与计费事务实现

数据库课设实战:电力公司收费系统表结构设计与计费事务实现

简介:这份数据库课程设计文档面向高校计算机相关专业学生,围绕「某电力公司收费管理信息系统」这一典型课题,提供从需求分析到数据库落地的完整设计思路。内容涵盖客户、用电类型、员工、用电信息、费用管理、收费登记等六张核心表的关系模型…

2026/10/9 11:36:37 阅读更多 →
ESP32+WS2812B心跳灯带实战:从GPIO到外部中断完整入门

ESP32+WS2812B心跳灯带实战:从GPIO到外部中断完整入门

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 11:36:37 阅读更多 →
校园局域网课设实战:DHCP+VLAN+ACL硬核闭环

校园局域网课设实战:DHCP+VLAN+ACL硬核闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 11:36:37 阅读更多 →

最新新闻

显示器是输出设备:从显卡信号到面板显示的技术链路与选购要点

显示器是输出设备:从显卡信号到面板显示的技术链路与选购要点

1. 先把概念说清楚:显示器到底充当的是什么角色1.1 从计算机系统的功能分类说起如果你问一个刚接触电脑的人,显示器是什么设备,他多半会脱口而出:“不就是那块屏幕嘛。”这话没错,但从计算机系统的功能分类来看&#x…

2026/10/9 12:06:14 阅读更多 →
宝塔线+CR指标组合:从画法到实战的完整技术分析框架

宝塔线+CR指标组合:从画法到实战的完整技术分析框架

很多做技术分析的朋友,都绕不开这两个指标:一个是看似简单却总被误读的宝塔线,另一个是明明很实用却容易被忽略的CR指标。宝塔线说白了是一个“价位突破追踪器”,它把每天收盘价与前期的关键高点、低点做比较,用红绿实…

2026/10/9 12:06:14 阅读更多 →
IntelliJ IDEA插件开发实战:从Gradle环境搭建到语言类插件与发布全链路

IntelliJ IDEA插件开发实战:从Gradle环境搭建到语言类插件与发布全链路

简介:这份《IntelliJ Platform Plugin 开发指导手册》面向 Java 开发者与 IDE 插件爱好者,帮助读者从零起步掌握 IntelliJ IDEA 插件开发,并逐步进阶到语言类高级插件。手册由上册、下册与附录三份文档组成,内容分为四部分&#x…

2026/10/9 12:06:14 阅读更多 →
Window11为什么出现codex桌面端频繁打不开和网络等待问题解决

Window11为什么出现codex桌面端频繁打不开和网络等待问题解决

目录 先修复 用其他AI助手解决 应用商店问题 清理缓存 等待下载完成 waiting for network 环境:window11 问题:codex运行报错/运行无反应,未打开桌面端但是又进程 先修复 用其他AI助手解决 这不失为一个方案,有次启动失败…

2026/10/9 12:06:14 阅读更多 →
【2027大数据精品毕设】基于大数据的高频电力消耗数据可视化与分析,附源码_数据可视化_数据分析_毕设选题_开题ppt_大数据项目_文档指导

【2027大数据精品毕设】基于大数据的高频电力消耗数据可视化与分析,附源码_数据可视化_数据分析_毕设选题_开题ppt_大数据项目_文档指导

💖💖作者:计算机毕业设计杰瑞 💙💙个人简介:曾长期从事计算机专业培训教学,本人也热爱上课教学,语言擅长Java、微信小程序、Python、Golang、安卓Android等,开发项目包括…

2026/10/9 12:06:14 阅读更多 →
Python print渲染8x8点阵字:从取模到终端显示全流程

Python print渲染8x8点阵字:从取模到终端显示全流程

1. 从一个打印需求说起:点阵字到底能玩出什么花样很多人第一次接触点阵字,是在老式收银机、电子秤或者公交站牌上。那种由一个个小圆点拼出来的数字和字母,远看粗糙,近看却有一种独特的机械美感。后来做嵌入式开发的朋友告诉我&am…

2026/10/9 12:05:13 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →