news 2026/9/16 1:48:15

Java Web二手交易网站:Servlet+JSP+Bootstrap实战入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Web二手交易网站:Servlet+JSP+Bootstrap实战入门

简介:本资源是一个基于Java原生技术栈(JSP + Servlet)实现的轻量级二手物品交易网站源码,面向Java Web初学者与课程设计实践者,解决校园或小型社区场景下二手商品发布、浏览与交易的基础需求。压缩包共108个文件,总计5.55MB,涵盖33个Java后端逻辑类、21个JSP前端页面、19张商品展示图片,以及6个CSS样式文件和5个JavaScript交互脚本,完整支撑用户注册登录、商品管理、购物车及订单流程等核心功能。已有296人学习下载,说明其结构清晰、代码规范,适合作为JSP/Servlet入门项目进行调试、二次开发或教学演示。资源中包含Bootstrap相关样式文件(如bootstrap.min.css)、SQL数据库脚本及META-INF配置,目录组织合理,便于快速理解MVC分层逻辑与前后端交互机制,是掌握传统Java Web开发全流程的典型参考案例。

1. 一个能跑起来的二手交易网站,为什么 Java + Bootstrap 是新手最稳的起点?

你不需要部署云服务器、不用配 Nginx 反向代理、甚至不碰 Docker,就能在本地 Windows 或 macOS 上双击启动一个带登录、发布商品、搜索浏览功能的二手物品交易网站——这就是“基于 Java 的简单二手物品交易网站设计源码”真正落地的样子。它不是教学 Demo,而是用真实 Web 开发逻辑组织的最小可行系统:后端用 Servlet + JDBC 直连 H2 内存数据库(零安装),前端用 Bootstrap 5.3 的bootstrap.min.css实现响应式布局,所有页面通过 JSP 渲染,没有 Spring Boot 自动配置的黑盒,也没有 Vue/React 的构建链路干扰。适合刚学完 Java 基础、JDBC 和 HTML/CSS 的开发者,在 2 小时内完成从解压到访问http://localhost:8080的全过程。它不追求高并发或微服务架构,但每行代码都暴露在你眼皮底下:用户注册怎么校验手机号格式、商品图片如何存路径而非二进制、搜索关键词怎么防 SQL 注入、分页查询 LIMIT OFFSET 怎么和前端页码联动——这些正是 Java 面试题里高频出现的“手写 DAO 层”“JSP 表单处理”“Servlet 生命周期”等考点的实战场。如果你正卡在“学了一堆语法却写不出完整 Web 应用”的阶段,这个源码就是那根可拆解、可调试、可改出自己简历项目的脚手架。

2. 用 Servlet + JSP 搭建核心流程:从用户注册到商品发布的最小闭环

2.1 为什么选原生 Servlet 而非 Spring Boot?——看清技术栈的“呼吸感”

很多初学者一上来就啃 Spring Boot,结果连DispatcherServlet怎么被web.xml加载都不清楚。本项目坚持用web.xml显式配置 Servlet 映射,目的就是让你看清请求从浏览器发出到页面返回的每一层穿透:HTTP 请求 → Tomcat 容器 →web.xml匹配 URL → 对应 Servlet 类 →doPost()方法处理表单 → JDBC 执行 INSERT → 转发到 JSP 成果页。这种“慢速但透明”的路径,恰恰是理解 Java Web 基础的关键。比如RegisterServlet.java中关键代码段:

// RegisterServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username").trim(); String phone = request.getParameter("phone"); String password = request.getParameter("password"); // 1. 前端传来的 phone 是字符串,这里做基础格式校验(面试常考正则) if (!phone.matches("^1[3-9]\\d{9}$")) { request.setAttribute("error", "手机号格式不正确"); request.getRequestDispatcher("/register.jsp").forward(request, response); return; } // 2. 使用 PreparedStatement 防 SQL 注入(Java 面试八股文必答点) String sql = "INSERT INTO users (username, phone, password, create_time) VALUES (?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, phone); ps.setString(3, DigestUtils.md5Hex(password)); // 密码 MD5 加密,非明文存储 ps.setTimestamp(4, new Timestamp(System.currentTimeMillis())); int rows = ps.executeUpdate(); if (rows > 0) { response.sendRedirect("login.jsp?msg=注册成功,请登录"); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "注册失败:" + e.getMessage()); request.getRequestDispatcher("/register.jsp").forward(request, response); } }

提示:这段代码直接对应 Java 面试题中“如何防止 SQL 注入”“Servlet 如何处理 POST 表单”“JDBC 连接池是否必要”三个高频问题。注意DBUtil.getConnection()返回的是 H2 内存数据库连接,无需额外安装 MySQL;DigestUtils.md5Hex()来自 Apache Commons Codec,jar 包已包含在lib/目录下,避免新手陷入 Maven 依赖冲突。

2.2 Bootstrap 5.3 的 CSS 如何驱动整个前端骨架?

项目前端不写一行 CSS,全部依赖bootstrap.min.css提供的类名系统。关键不是“用没用 Bootstrap”,而是怎么用得精准且不冗余。例如商品列表页index.jsp中的商品卡片结构:

<!-- index.jsp 片段 --> <div class="row g-4"> <c:forEach items="${goodsList}" var="g"> <div class="col-12 col-md-6 col-lg-4"> <div class="card h-100 border-0 shadow-sm"> <img src="${g.imagePath}" class="card-img-top object-fit-cover" style="height: 200px;" alt="${g.title}"> <div class="card-body d-flex flex-column"> <h5 class="card-title text-truncate">${g.title}</h5> <p class="card-text text-muted flex-grow-1 mb-2">${g.description}</p> <div class="d-flex justify-content-between align-items-center"> <span class="h5 text-primary fw-bold">¥${g.price}</span> <a href="detail.jsp?id=${g.id}" class="btn btn-sm btn-outline-primary">查看详情</a> </div> </div> </div> </div> </c:forEach> </div>

参数说明

  • row g-4:Bootstrap 5 的栅格系统,g-4表示列间距 1.5rem(16px × 0.9375);
  • col-12 col-md-6 col-lg-4:响应式断点,手机单列、平板双列、桌面三列;
  • object-fit-cover+style="height:200px":强制图片等比缩放填充容器,解决二手图尺寸不一问题;
  • text-truncate:单行文本溢出显示省略号,避免长标题撑破卡片;
  • flex-column+flex-grow-1:让描述文字区域自动撑满剩余空间,价格和按钮固定在底部。
    这些不是炫技,而是真实二手场景需求:用户扫一眼要快速识别价格、图片、标题,Bootstrap 类名就是你的“CSS 速查字典”。

2.3 数据库设计直击二手业务本质:三张表搞定核心关系

项目仅用三张表支撑全部功能,结构精简但覆盖关键业务约束:

表名字段说明
usersid(PK), username, phone(UNIQUE), password, create_time用户表,手机号唯一索引,防止重复注册
goodsid(PK), title, description, price(DECIMAL), image_path, user_id(FK), status(TINYINT)商品表,status=1表示上架,0表示已售出/下架
ordersid(PK), goods_id(FK), buyer_id(FK), seller_id(FK), create_time订单表,记录谁买了谁的商品(简化版,无支付状态)

H2 数据库初始化脚本src/main/resources/init.sql在应用首次启动时自动执行:

-- init.sql CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, phone VARCHAR(11) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, image_path VARCHAR(200) DEFAULT '/images/default.jpg', user_id BIGINT NOT NULL, status TINYINT DEFAULT 1, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );

注意ON DELETE CASCADE是关键设计——当用户注销时,其发布的所有商品自动删除,避免孤儿数据。这比在 Java 代码里手动遍历删除更可靠,也是数据库设计面试题常考点。

3. 本地运行四步法:从解压到访问 localhost:8080 的完整命令链

3.1 环境准备:JDK 8+ 与 Tomcat 9 的最小化安装验证

本项目兼容 JDK 8 至 JDK 17,但必须使用 Tomcat 9.x(Tomcat 10+ 默认启用 Jakarta EE 9 命名空间,javax.servlet.*包名变为jakarta.servlet.*,会导致编译报错)。验证环境是否就绪:

# 检查 JDK 版本(需 1.8+) java -version # 输出应类似:openjdk version "17.0.1" 2021-10-19 # 检查 Tomcat 版本(需 9.0.x) $CATALINA_HOME/bin/version.sh # Linux/macOS %CATALINA_HOME%\bin\version.bat # Windows # 输出应含 "Apache Tomcat/9.0.xx"

提示:若未安装 Tomcat,去官网下载apache-tomcat-9.0.83.zip(最新稳定版),解压后设置环境变量CATALINA_HOME指向解压目录。Windows 用户注意路径不要含中文或空格。

3.2 源码部署:将项目放入 Tomcat webapps 并启动服务

项目结构为标准 Java Web Archive(WAR)布局,无需 Maven 构建,直接复制即可:

# 假设源码解压在 ~/project/secondhand/ # Linux/macOS 执行: cp -r ~/project/secondhand/ $CATALINA_HOME/webapps/secondhand # Windows 执行(PowerShell): Copy-Item -Path "C:\project\secondhand" -Destination "$env:CATALINA_HOME\webapps\secondhand" -Recurse # 启动 Tomcat(后台运行,方便查看日志) $CATALINA_HOME/bin/startup.sh # Linux/macOS %CATALINA_HOME%\bin\startup.bat # Windows

启动后观察$CATALINA_HOME/logs/catalina.out日志,确认无ClassNotFoundExceptionSQLException错误。正常启动末尾会输出:

INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [1234] milliseconds

此时访问http://localhost:8080/secondhand/即可看到首页。

3.3 关键配置文件解析:web.xml 与 DBUtil.java 的联动逻辑

WEB-INF/web.xml是整个应用的“中枢神经”,定义了 Servlet 映射和过滤器:

<!-- web.xml 片段 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 注意:JSP 页面提交 form action="/login" --> </servlet-mapping> <!-- 全局字符编码过滤器,解决中文乱码 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.example.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

DBUtil.java则封装了 H2 数据库连接逻辑,关键在于内存模式 URL 和自动建表:

// DBUtil.java private static final String URL = "jdbc:h2:mem:secondhand;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"; private static final String USER = "sa"; private static final String PASSWORD = ""; public static Connection getConnection() throws SQLException { // 第一次调用时触发 init.sql 执行 try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) { conn.createStatement().execute("RUNSCRIPT FROM 'src/main/resources/init.sql'"); return conn; } }

参数说明

  • mem:secondhand:H2 内存数据库,重启 Tomcat 后数据丢失,适合开发调试;
  • DB_CLOSE_DELAY=-1:允许连接池复用,避免频繁创建销毁连接;
  • RUNSCRIPT FROM '...':在每次获取连接时执行初始化脚本,确保表结构存在。
    这种设计让新手免于手动建库建表,但必须确保init.sql文件路径在类路径下(即src/main/resources/编译后位于WEB-INF/classes/)。

3.4 常见启动失败排查表:按错误日志快速定位根因

错误日志关键词根本原因解决方案
java.lang.ClassNotFoundException: javax.servlet.http.HttpServletTomcat 版本过高(10+),包名已变降级到 Tomcat 9.0.x,或修改所有import javax.servlet.*jakarta.servlet.*(不推荐新手)
org.h2.jdbc.JdbcSQLNonTransientException: Database "mem:secondhand" not foundinit.sql路径错误或未被加载检查DBUtil.javaRUNSCRIPT路径是否为src/main/resources/init.sql,确认该文件存在于WEB-INF/classes/
HTTP Status 404 – /secondhand/项目未正确放入webapps/目录,或目录名不匹配确认$CATALINA_HOME/webapps/secondhand/下存在WEB-INF/index.jsp,URL 必须带/secondhand/
java.sql.SQLException: Unique constraint violation用户注册时手机号重复清空 H2 内存数据库:停止 Tomcat,删除$CATALINA_HOME/work/Catalina/localhost/secondhand/目录

4. 商品搜索与分页的实战优化:从模糊匹配到用户体验细节

4.1 模糊搜索的 SQL 实现与性能边界

二手交易的核心操作是“找东西”,搜索不能只靠LIKE '%关键词%'。项目采用组合策略提升准确率:

// SearchServlet.java String keyword = request.getParameter("q").trim(); if (keyword.isEmpty()) { // 空搜索返回全部上架商品 sql = "SELECT * FROM goods WHERE status = 1 ORDER BY create_time DESC LIMIT ? OFFSET ?"; } else { // 分词搜索:标题 AND 描述 OR 标题 OR 描述(模拟搜索引擎逻辑) sql = "SELECT * FROM goods WHERE status = 1 AND " + "(title LIKE ? OR description LIKE ? OR title LIKE ?) " + "ORDER BY create_time DESC LIMIT ? OFFSET ?"; ps.setString(1, "%" + keyword + "%"); // 标题包含 ps.setString(2, "%" + keyword + "%"); // 描述包含 ps.setString(3, keyword + "%"); // 标题前缀匹配(提高首字母输入体验) }

为什么这样写?

  • title LIKE ?description LIKE ?覆盖大部分用户输入习惯;
  • title LIKE ?(前缀匹配)让“iphone”能命中 “iPhone 12 Pro”,而'%iphone%'无法利用索引;
  • H2 内存数据库对小数据量(<1000 条)全表扫描无压力,无需建全文索引。
    这正是 Java 面试题中“如何优化模糊搜索”的务实答案:不盲目上 Elasticsearch,先用 SQL 层级策略解决问题。

4.2 前端分页组件的 Bootstrap 5 实现与后端参数协同

分页不是简单循环<li>,而是前后端严格约定参数。index.jsp中分页 HTML:

<!-- 分页导航 --> <nav aria-label="商品列表分页"> <ul class="pagination justify-content-center"> <li class="page-item ${pageNo <= 1 ? 'disabled' : ''}"> <a class="page-link" href="?page=${pageNo-1}&q=${param.q}">上一页</a> </li> <c:forEach begin="1" end="${totalPages}" var="i"> <li class="page-item ${i == pageNo ? 'active' : ''}"> <a class="page-link" href="?page=${i}&q=${param.q}">${i}</a> </li> </c:forEach> <li class="page-item ${pageNo >= totalPages ? 'disabled' : ''}"> <a class="page-link" href="?page=${pageNo+1}&q=${param.q}">下一页</a> </li> </ul> </nav>

后端GoodsServlet.java计算逻辑:

int pageSize = 9; // 每页 9 条,适配 Bootstrap 卡片三列布局 int pageNo = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int offset = (pageNo - 1) * pageSize; // 查询总条数(用于计算总页数) String countSql = "SELECT COUNT(*) FROM goods WHERE status = 1 " + (keyword.isEmpty() ? "" : "AND (title LIKE ? OR description LIKE ?)"); // ... 执行 countSql 获取 totalCount ... int totalPages = (int) Math.ceil((double) totalCount / pageSize); request.setAttribute("totalPages", totalPages); request.setAttribute("pageNo", pageNo);

关键协同点

  • 前端href="?page=${pageNo-1}&q=${param.q}"保证搜索关键词q在翻页时保留;
  • Math.ceil()计算总页数,避免totalCount % pageSize == 0时少算一页;
  • disabled类由 EL 表达式动态控制,禁用无效翻页链接,这是 Bootstrap 5 推荐的无障碍实践。

4.3 图片上传的极简实现:不依赖第三方库的 JSP 文件处理

二手网站必须支持用户上传商品图。项目采用cos.jar(O'Reilly 的 classic upload)实现,因其轻量(仅 100KB)且无需 Servlet 3.0+ 注解:

<!-- publish.jsp 表单 --> <form action="publish" method="post" enctype="multipart/form-data"> <div class="mb-3"> <label class="form-label">商品图片</label> <input type="file" name="image" class="form-control" accept="image/*"> </div> <!-- 其他字段 --> <button type="submit" class="btn btn-primary">发布商品</button> </form>

PublishServlet.java处理逻辑:

// PublishServlet.java MultipartRequest multi = new MultipartRequest( request, getServletContext().getRealPath("/images/"), // 保存到 webapps/secondhand/images/ 5 * 1024 * 1024, // 5MB 限制 "UTF-8", new DefaultFileRenamePolicy() ); String title = multi.getParameter("title"); String imagePath = "/images/" + multi.getFilesystemName("image"); // 路径存入数据库

注意cos.jar已预置在WEB-INF/lib/下,无需额外下载。getRealPath("/images/")返回绝对路径,确保文件写入webapps/secondhand/images/目录,浏览器可通过http://localhost:8080/secondhand/images/filename.jpg直接访问。这是 Java Web 开发中“文件上传”考点的标准解法。

5. 从源码到面试:三个可直接复用的 Java 面试高频问题应答模板

5.1 “请手写一个 JDBC 查询方法”——以 GoodsDAO.findAll() 为蓝本

面试官要的不是完美 ORM,而是你对资源管理、异常处理、SQL 注入防护的理解。直接复用项目中的GoodsDAO.java

public List<Goods> findAll(int offset, int limit) { List<Goods> list = new ArrayList<>(); String sql = "SELECT id, title, price, image_path, create_time FROM goods " + "WHERE status = 1 ORDER BY create_time DESC LIMIT ? OFFSET ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, limit); // 防止恶意传入超大数字 ps.setInt(2, offset); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setId(rs.getLong("id")); g.setTitle(rs.getString("title")); g.setPrice(rs.getBigDecimal("price")); g.setImagePath(rs.getString("image_path")); g.setCreateTime(rs.getTimestamp("create_time")); list.add(g); } } } catch (SQLException e) { throw new RuntimeException("查询商品列表失败", e); // 包装为运行时异常,上层统一处理 } return list; }

应答要点

  • 强调try-with-resources自动关闭连接,避免连接泄漏;
  • 指出LIMIT ? OFFSET ?参数化防止注入,且offset由前端传入需校验范围(如offset >= 0 && offset < 10000);
  • 说明RuntimeException包装是为了让 Controller 层用@ExceptionHandler统一返回 JSON 错误,符合 RESTful 设计。

5.2 “JSP 和 Servlet 如何配合处理用户登录?”——用 LoginServlet + login.jsp 拆解生命周期

这不是背概念,而是讲清数据流向:

  1. 用户在login.jsp输入账号密码,表单action="login"提交到LoginServlet
  2. LoginServlet.doPost()request.getParameter()获取参数,用PreparedStatement查询数据库;
  3. 若密码匹配,将User对象存入HttpSessionrequest.getSession().setAttribute("user", user)
  4. 重定向到index.jsp,该页面通过<c:if test="${not empty sessionScope.user}">判断登录态,显示“欢迎,${sessionScope.user.username}”;
  5. 退出登录时LogoutServlet调用session.invalidate(),清除所有 session 属性。

面试加分项:指出request.getSession(true)默认创建新会话,而request.getSession(false)仅获取不创建,避免无意义会话膨胀。

5.3 “Bootstrap 如何实现响应式商品网格?”——用 index.jsp 的 col-* 类名链回答

不要说“用了栅格系统”,要具体到类名作用链:

  • class="row g-4":定义弹性盒子容器,g-4设置列间距为1.5rem(Bootstrap 5 的 spacing scale);
  • class="col-12 col-md-6 col-lg-4"col-12在超小屏(<576px)占满整行,col-md-6在中屏(≥768px)占半行(2 列),col-lg-4在大屏(≥992px)占 1/3 行(3 列);
  • class="card h-100"h-100让卡片高度继承父容器,解决不同商品标题长度导致卡片高度不一的问题;
  • class="object-fit-cover":CSS 属性object-fit: cover强制图片等比缩放并裁剪,保持视觉一致性。

这个回答把“响应式”从抽象概念落到具体类名、像素值和 CSS 属性,证明你真正在项目里调过这些样式,而不是复制粘贴。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:46:08

交叉验证切分方法全解析:KFold、分层CV与时序CV的选型指南

我们做模型评估的时候&#xff0c;有一件事特别容易糊弄过去&#xff1a;交叉验证到底怎么切数据。大多数人默认点一下KFold&#xff0c;跑出来的分数高就开心&#xff0c;分数低就调参&#xff0c;很少有人停下来想一想——K折切分的方式&#xff0c;决定了你评估出来的"…

作者头像 李华
网站建设 2026/9/16 1:46:06

基于视觉暂留的LED风扇旋转字幕设计与实现——从原理图到源码解析

简介&#xff1a;面向电子爱好者与嵌入式初学者&#xff0c;这套以LED风扇为主题的完整工程资料将旋转字幕显示、NFC近场通信模块、原理图与源码整合在一起&#xff0c;是一份可动手实践的项目参考。压缩包共14个文件&#xff0c;大小6.86MB&#xff0c;包含bmp图案资源、exe与…

作者头像 李华
网站建设 2026/9/16 1:46:03

拒绝学术听证会警告!留学生搞定Turnitin查重与AI检测的终极避坑指南

很多留学生在提交英文论文前&#xff0c;都经历过被Turnitin标红的恐慌。明明是自己逐字手敲&#xff0c;论文查重率和AI相似度依然可能超标&#xff0c;甚至面临学术审查。面对复杂的学术门槛&#xff0c;专业的留学生论文辅导成了许多人顺利毕业的刚需。但这行水深&#xff0…

作者头像 李华
网站建设 2026/9/16 1:44:24

HEALPix原理与healpy实战:球面数据处理核心指南

简介&#xff1a;本资源是Python科学计算领域关键天文数据处理库healpy的源码发布包&#xff08;v1.12.5&#xff09;&#xff0c;面向天文学、宇宙学及球面数据分析方向的Python开发者与科研人员&#xff0c;解决HEALPix格式球面数据的读写、投影、傅里叶变换、可视化与统计分…

作者头像 李华
网站建设 2026/9/16 1:43:41

ANSYS流固耦合分析:耦合策略、Workbench搭建与收敛调试指南

简介&#xff1a;这是一份面向ANSYS工程师与力学仿真初学者的流固耦合教学资料包&#xff0c;围绕工程实例演示ANSYS Workbench中FSI分析的全流程。压缩包共139个文件&#xff0c;约291MB&#xff0c;涵盖agdb几何模型、msh网格、dat/cas求解数据、wbpj/mechdb工程文件以及avi/…

作者头像 李华
网站建设 2026/9/16 1:43:13

/v1 报错拦路?Highcharts MCP 的 Base URL 用 TaoToken 填

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

作者头像 李华