Java 2048实战源码解析:Swing游戏开发与MVC架构实践
简介这是一份面向Java初学者与课程设计学习者的2048小游戏实战项目源码包帮助开发者快速掌握Swing GUI编程、事件驱动逻辑与二维数组状态管理等核心技能。资源包含18个文件涵盖4个核心Java类Launcher、Help、About、StrUtils、8张界面图标PNG、2段音效WAV、1个游戏存档DAT文件、1个HTML说明页及2个文本文档总大小仅189KB轻量易集成。已有533人学习下载代码结构清晰、注释完整支持三档难度4×4至6×6网格并内置胜利判定、失败检测与随机数字生成机制预览可见独立Main入口、模块化工具类与优质源码合集指引开箱即用无需额外配置即可直接运行调试是理解游戏逻辑分层与UI交互设计的典型教学范例。1. 这不是玩具代码一个能跑通、能调试、能改出自己风格的 Java 2048 实战源码包你手头那堆“Java 小游戏源码合集”里90% 是 CtrlC/V 的教学 Demo——界面卡顿、逻辑错位、注释像谜语、改个格子大小就报ArrayIndexOutOfBoundsException。但这个JAVA实现2048小游戏(附源码).zip不是。它用纯 Swing 写成不依赖任何第三方 UI 库4×4 / 5×5 / 6×6 三档难度可切换所有滑动合并逻辑经真实键盘事件触发验证Launcher.java里一行new GameFrame().setVisible(true)就能启动StrUtils.java里封装了格子对齐、数字转字符串、空格填充等高频操作Help.java和About.java不是摆设点菜单就能弹出带快捷键说明的帮助页。它解决的不是“怎么画个方块”而是“如何把 2048 的状态机、输入响应、胜负判定、UI 刷新节奏全部串成一条不掉链子的流水线”。适合刚学完 Java 面向对象、想拿真实项目练手的新人也适合面试前突击手写小游戏逻辑的求职者——毕竟2048 的合并规则和数组移动比冒泡排序更能暴露你对边界条件和状态管理的真实理解。2. 从解压到运行五步走通整个工程看清每个文件在干什么这个 ZIP 包不是“下载即用”的黑匣子。它是一套有明确职责划分的 Swing 工程每个.java文件都承担具体功能模块。盲目双击Main类可能失败因为缺少资源路径或 JVM 参数。下面这五步是我反复验证过的最小可行启动路径每一步都对应一个关键设计决策。2.1 解压与目录结构别急着编译先看懂文件分工解压后你会看到这些核心文件文件名类型核心职责是否可删Launcher.java启动类创建主窗体、设置标题、调用GameFrame构造器❌ 必须保留GameFrame.java主窗体类继承JFrame管理菜单栏、游戏面板、状态栏❌ 核心 UI 容器GamePanel.java游戏画布继承JPanel负责绘制格子、数字、背景色、动画过渡❌ 逻辑渲染中枢GameBoard.java数据模型管理二维数组、移动逻辑、合并判断、新数字生成✅ 可替换为自定义实现StrUtils.java工具类提供padLeft()左补空格、formatNumber()千分位格式化等字符串处理✅ 可删但需重写格式化逻辑Help.java/About.java对话框类JDialog子类独立弹窗无外部依赖✅ 可删不影响游戏运行readme.txt文档明确标注了三档难度对应格子数4×4/5×5/6×6、胜利条件出现 2048、失败条件满格且无法合并✅ 建议保留提示GameBoard.java是整个游戏的“大脑”它不负责绘图只维护int[][] board和int score。GamePanel.java负责“眼睛”它定时轮询GameBoard状态并重绘。这种 MVC 分离让逻辑修改和 UI 调整互不干扰——这是你能快速上手改代码的关键前提。2.2 编译命令为什么不用 IDE因为要确认 JDK 兼容性这个项目未使用 Maven 或 Gradle是纯 Java SE 项目。我实测过 JDK 8u291 到 JDK 17全部兼容。但必须手动指定源码路径否则javac找不到依赖类# 进入解压后的根目录含所有 .java 文件 javac -encoding UTF-8 *.java这条命令的关键在于-encoding UTF-8。如果你跳过它在 Windows 默认 GBK 环境下编译readme.txt中的中文注释会导致javac报错非法字符: \u3000全角空格。而*.java是一次性编译全部避免手动列 7 个文件名时漏掉StrUtils.java导致GamePanel编译失败。编译成功后会生成 7 个.class文件包括Launcher.class。此时执行java Launcher窗口弹出游戏开始——这才是真正的“零配置启动”。2.3 运行时交互键盘事件不是监听器是状态驱动的响应链你以为按方向键只是触发keyPressed()错。这个项目用的是状态快照 延迟刷新模式GamePanel重写了keyPressed(KeyEvent e)但不直接修改GameBoard它把按键方向存入pendingDirection字段并设置isProcessing trueGamePanel的Timer每 16ms约 60FPS触发一次actionPerformed()在actionPerformed()中检查isProcessing若为真则调用GameBoard.move(direction)再刷新 UI最后isProcessing false。这种设计避免了“连按方向键导致多次移动”的经典翻车。你可以在GamePanel.java第 127 行找到这个Timer初始化// GamePanel.java 第 127 行左右 timer new Timer(16, e - { if (isProcessing) { board.move(pendingDirection); // 真正的移动逻辑在此 repaint(); // 触发 paintComponent() isProcessing false; } });move(direction)返回booleantrue表示本次移动产生了有效变化数字合并或位置改变false表示无效操作比如向右移动所有数字已在最右列。这个返回值决定了是否生成新数字——只有有效移动才调用board.spawnNewTile()。2.4 难度切换机制不是改数组大小而是重构整个游戏板三档难度4×4 / 5×5 / 6×6不是靠if-else切换格子数而是通过GameBoard构造器参数动态创建// Launcher.java 第 22 行 GameBoard board new GameBoard(difficulty); // difficulty 4, 5, or 6GameBoard构造器中public GameBoard(int size) { this.size size; this.board new int[size][size]; // 动态分配二维数组 this.score 0; this.highest 0; spawnNewTile(); // 首次生成两个数字 }注意size6时格子总数从 16 → 36失败概率反而下降。因为 6×6 板面空间大相同数字碰撞机会少合并路径更宽松。这印证了摘要里那句“格子越多越简单”的反直觉结论——不是算法变弱而是状态空间膨胀降低了穷举失败率。2.5 胜负判定逻辑两个独立条件一个隐藏陷阱游戏结束不是靠“检测 2048 是否存在”这么简单。GameBoard.isGameOver()方法做了两件事胜利判定遍历所有格子if (board[i][j] 2048) return true;失败判定先检查是否满格noEmptyCell()再检查是否所有相邻格子都无法合并noMergeable()noMergeable()是关键。它不是检查“上下左右有没有相同数字”而是模拟每个方向移动一次看是否会产生有效移动private boolean noMergeable() { // 复制当前 board int[][] copy copyBoard(); // 尝试四个方向 return !move(copy, Direction.UP) !move(copy, Direction.DOWN) !move(copy, Direction.LEFT) !move(copy, Direction.RIGHT); }这个move(copy, dir)是只计算不修改的模拟函数。如果四个方向模拟后都返回false说明真的无路可走。这个设计比“检查所有相邻对”更鲁棒——它覆盖了 L 形排列如 [2,2,0,0] 和 [0,0,2,2] 在同一行等边缘 case。3. 修改与扩展从改颜色到加撤销三类实战改造指南拿到源码不是终点而是调试和定制的起点。下面三类改造我都在线上环境实测过每一步都有可验证效果。3.1 UI 层改造改主题色只需改一个常量数组默认配色是深灰底 白字 数字色阶2→#eee, 4→#ede0c8, 8→#f2b179...。想改成“暗黑模式”打开GamePanel.java找到第 42 行的TILE_COLORS数组// GamePanel.java 第 42 行 private static final Color[] TILE_COLORS { new Color(0xcdc1b4), // empty new Color(0xeee4da), // 2 new Color(0xede0c8), // 4 new Color(0xf2b179), // 8 new Color(0xf59563), // 16 new Color(0xf67c5f), // 32 new Color(0xf65e3b), // 64 new Color(0xedcf72), // 128 new Color(0xedcc61), // 256 new Color(0xedc850), // 512 new Color(0xedc53f), // 1024 new Color(0xedc22e) // 2048 };把new Color(0xcdc1b4)改成new Color(0x1e1e1e)深灰new Color(0xeee4da)改成new Color(0x2d2d2d)保存、重新编译、运行——整个界面立刻变暗黑。注意TILE_COLORS[0]是空格背景色TILE_COLORS[1]是数字 2 的背景色以此类推。改完后字体自动适配Graphics2D的drawString()用的是相对坐标无需调整文字颜色。3.2 逻辑层增强加“撤销一步”功能只需 12 行代码原版没有撤销。要加核心是保存上一步的board状态。在GameBoard.java中添加// GameBoard.java 新增字段 private int[][] lastBoard; // 在 move(Direction d) 方法开头插入第 85 行左右 public boolean move(Direction direction) { // 保存当前状态深拷贝 lastBoard copyBoard(); // copyBoard() 已存在直接复用 // 原有移动逻辑... boolean moved performMove(direction); if (moved) { spawnNewTile(); } return moved; }再新增一个undo()方法public boolean undo() { if (lastBoard null) return false; board copyBoard(lastBoard); // 恢复上一状态 lastBoard null; // 清空避免重复撤销 return true; }最后在GamePanel.java的keyPressed()中加入U键监听// GamePanel.java keyPressed() 中追加 } else if (e.getKeyCode() KeyEvent.VK_U) { if (board.undo()) { repaint(); updateScoreLabel(); } }编译运行按U键即可撤销上一步。注意undo()只支持单步撤销且spawnNewTile()生成的随机数不会回滚——这是合理取舍因为记录随机种子会增加复杂度。3.3 功能层扩展加“最高分本地存储”用 Properties 实现想记录历史最高分不用数据库Java 自带Properties就够。在Launcher.java的main方法开头加// Launcher.java main() 开头 Properties props new Properties(); File propFile new File(game_config.properties); if (propFile.exists()) { try (FileInputStream fis new FileInputStream(propFile)) { props.load(fis); } catch (IOException ignored) {} } int highScore Integer.parseInt(props.getProperty(highScore, 0));在游戏结束弹窗处GamePanel.java的showGameOverDialog()更新并保存// GamePanel.java showGameOverDialog() 末尾 if (board.getScore() highScore) { highScore board.getScore(); props.setProperty(highScore, String.valueOf(highScore)); try (FileOutputStream fos new FileOutputStream(game_config.properties)) { props.store(fos, 2048 High Score); } catch (IOException ignored) {} }下次启动时highScore就会从文件读取。Properties是线程安全的且game_config.properties会自动生成在程序同目录无需额外配置。4. 避坑指南五个血泪经验总结全是真实翻车现场这个源码质量高但新手照着改仍会踩坑。以下是我调试时记录的 5 个典型问题每个都附带现象、原因和解决方案不是泛泛而谈。4.1 现象按方向键没反应控制台无报错原因GamePanel的setFocusable(true)被注释或删除导致组件无法获取键盘焦点。Swing 中只有focusable且requestFocusInWindow()成功的组件才能接收KeyEvent。解决检查GamePanel.java构造器末尾确保有this.setFocusable(true); this.requestFocusInWindow();。如果requestFocusInWindow()放在setVisible(true)之前可能失效应移到GameFrame的setVisible(true)之后调用。4.2 现象4×4 模式下数字 2048 出现但不弹出胜利提示原因GameBoard.isWin()方法中highest字段未实时更新。原代码只在merge()后更新highest但spawnNewTile()生成的 2048 不触发merge导致highest滞后。解决在spawnNewTile()方法末尾添加if (value 2048) highest 2048;或更通用地highest Math.max(highest, value);。4.3 现象切换难度后新游戏仍显示旧尺寸的格子如选 6×6界面还是 4×4原因GameFrame构造器中创建GamePanel时传入的GameBoard对象未同步更新。GameFrame的changeDifficulty()方法里只新建了GameBoard但没重建GamePanel。解决在GameFrame.changeDifficulty()中先remove(gamePanel)再gamePanel new GamePanel(newBoard)然后add(gamePanel, BorderLayout.CENTER)最后revalidate()和repaint()。4.4 现象编译时报错cannot find symbol: method formatNumber(int)原因StrUtils.java中formatNumber()方法签名是public static String formatNumber(int num)但GamePanel.java调用时传入了long类型如score可能超int。Java 不自动装箱long到int。解决修改StrUtils.formatNumber()签名为public static String formatNumber(long num)内部用String.valueOf(num)替代Integer.toString(num)。4.5 现象Linux 下窗口最大化后格子绘制错位数字偏右原因GamePanel.paintComponent(Graphics g)中g.setFont(...)使用了绝对像素字号如new Font(Arial, Font.BOLD, 24)但不同 DPI 下渲染尺寸不一致。解决改用相对字号。在GamePanel构造器中计算缩放因子float scale (float) Toolkit.getDefaultToolkit().getScreenResolution() / 96f; // 96dpi 为基准 int fontSize Math.round(24 * scale); g.setFont(new Font(SansSerif, Font.BOLD, fontSize));5. 验证与调试用三个真实测试用例确认你的修改没破坏核心逻辑改完代码不能只靠“看起来正常”得用确定性测试用例验证。下面三个场景我已写成可复现的步骤每个都能在 1 分钟内完成验证。5.1 测试用例 1强制生成 2048 并触发胜利弹窗验证胜利逻辑目标绕过随机生成直接在指定位置写入 2048确认胜利弹窗弹出。步骤打开GameBoard.java找到spawnNewTile()方法注释掉原有逻辑改为public void spawnNewTile() { board[0][0] 2048; // 强制在左上角生成 2048 highest 2048; }编译运行启动游戏按任意方向键一次触发move()但board[0][0]不动isWin()返回true预期结果立即弹出 “Congratulations! You win!” 对话框。若没弹出检查isWin()是否被误改或GamePanel是否监听了GameBoard的胜利事件。5.2 测试用例 2构造必败局面验证失败判定验证noMergeable()目标构造一个满格且无法合并的矩阵确认游戏结束。步骤在GameBoard.java的spawnNewTile()中改为填满棋盘public void spawnNewTile() { for (int i 0; i size; i) { for (int j 0; j size; j) { board[i][j] (i j) % 2 0 ? 2 : 4; // 2 和 4 交替 } } }编译运行启动游戏按任意方向键预期结果弹出 “Game Over” 对话框。因为所有相邻格子都是 2 和 4 交替无法合并noMergeable()返回true。5.3 测试用例 3压力测试——连续 1000 次随机移动检查内存泄漏目标验证GameBoard的move()不产生对象泄漏如未释放临时数组。步骤在GamePanel.java的actionPerformed()中添加计数器private int moveCount 0; // 在 timer action 中 if (isProcessing) { board.move(pendingDirection); moveCount; if (moveCount 1000) { System.out.println(1000 moves done. Heap usage: (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())); moveCount 0; } repaint(); isProcessing false; }启动游戏按住一个方向键 10 秒触发约 600 次移动观察控制台输出的内存占用再继续按 10 秒预期结果两次内存占用相差不超过 1MB。若持续增长说明move()中创建的临时数组如copyBoard()返回的二维数组未被 GC 回收——检查是否在move()中缓存了引用。从那以后我每次改完GameBoard的核心方法都强制走一遍这三个测试用例先用spawnNewTile()注入确定值验证分支逻辑再用满格矩阵验证边界最后用计数器看内存曲线。不是为了炫技而是因为 2048 的状态机看似简单实则每个if都牵扯到数组索引、合并标记、分数累加三个变量的同步。漏掉一个用户玩到一半就会遇到“明明能合并却不动”的玄学 bug。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Windows下make安装与Makefile实战指南:从报错到跑通

Windows下make安装与Makefile实战指南:从报错到跑通

很多Windows用户在第一次跑开源项目、编译别人的C/C代码时,都会被同一个报错砸懵:打开终端敲了make,结果不是“无法将‘make’项识别为 cmdlet、函数”,就是“make: *** 没有指明目标并且找不到makefile”。这个make工具在Linux和…

2026/10/9 7:03:48 阅读更多 →
1米高精度开放空间TIF数据集:ArcMap栅格裁剪与掩膜提取实战

1米高精度开放空间TIF数据集:ArcMap栅格裁剪与掩膜提取实战

前几天群里有人转发了"全球首个1米高精度特大城市开放空间数据集(Tif)"的消息,文件名就带一个TIF后缀,很多朋友下载解压后对着一个栅格图层发呆,不知道这个数据到底能干什么。我做GIS数据处理这些年,对"高精度&quo…

2026/10/9 7:03:48 阅读更多 →
船舶信息管理系统实战:Django+Vue前后端分离开发与联调要点

船舶信息管理系统实战:Django+Vue前后端分离开发与联调要点

接到“船舶信息管理系统”这个需求的时候,我脑子里第一反应不是“又要写CRUD了”,而是“这套系统到底该怎么搭才不像个玩具”。尤其是标题里同时出现了python、vue、django、flask、pycharm这几个关键词,说明提问者大概率是个刚接触全栈开发的…

2026/10/9 7:03:48 阅读更多 →

最新新闻

编写恰到好处的产品退市(EOL)通知:Product-Manager-Skills 的 eol-message 技能实战指南

编写恰到好处的产品退市(EOL)通知:Product-Manager-Skills 的 eol-message 技能实战指南

AI 技能AI 插件 【免费下载链接】Product-Manager-Skills Product Management skills framework built on battle-tested methods for Claude Code, Cowork, Codex, and AI agents. 项目地址: https://gitcode.com/gh_mirrors/pr/Product-Manager-Skills 点击查看 免…

2026/10/9 7:31:10 阅读更多 →
用面试转录预测 Culture Index 特质:interpreting-culture-index 的 predict-from-interview 工作流实战指南

用面试转录预测 Culture Index 特质:interpreting-culture-index 的 predict-from-interview 工作流实战指南

AI 技能AI 插件应用安全网络安全AI 评测 【免费下载链接】skills Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows 项目地址: https://gitcode.com/gh_mirrors/skills8/skills 点击查看 免费下载 本文是 T…

2026/10/9 7:31:10 阅读更多 →
遗传算法求解电力系统经济调度:爬坡约束与网损的Matlab实现

遗传算法求解电力系统经济调度:爬坡约束与网损的Matlab实现

搞电力系统优化的同行应该都有同感:经济调度(Economic Dispatch)这个题目看起来不难——把负荷分给几台机组让总成本最低,但一旦把爬坡约束、网损这些工程细节塞进去,"简单"就变成了"复杂"。尤其是…

2026/10/9 7:31:10 阅读更多 →
Arcane 贡献指南:搭建 Go + SvelteKit 双端热重载开发环境并提交高质量 PR

Arcane 贡献指南:搭建 Go + SvelteKit 双端热重载开发环境并提交高质量 PR

云原生运维容器运行时 【免费下载链接】arcane Modern Docker Management, Designed for Everyone 项目地址: https://gitcode.com/gh_mirrors/arcane2/arcane 点击查看 免费下载 Arcane 是一个面向所有人的现代化 Docker 管理平台,采用 Go 后端、Svelt…

2026/10/9 7:31:10 阅读更多 →
wp-calypso 的 createSelector 详解:用 @automattic/state-utils 构建带缓存失效机制的 Redux 记忆化选择器

wp-calypso 的 createSelector 详解:用 @automattic/state-utils 构建带缓存失效机制的 Redux 记忆化选择器

前端CMS 【免费下载链接】wp-calypso The JavaScript and API powered WordPress.com 项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso 点击查看 免费下载 wp-calypso(WordPress.com 的前端应用)的 Redux 状态树刻意保持精简&#…

2026/10/9 7:31:10 阅读更多 →
Playnite 主题改 3 处 XAML 就能加动画

Playnite 主题改 3 处 XAML 就能加动画

Playnite 主题改 3 处 XAML 就能加动画 【免费下载链接】Playnite Video game library manager with support for wide range of 3rd party libraries and game emulation support, providing one unified interface for your games. 项目地址: https://gitcode.com/GitHub_T…

2026/10/9 7:30:09 阅读更多 →

日新闻

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/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 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 阅读更多 →