HTML5多图片上传预览源码解析:从选图到FormData提交
简介这份资源是一套基于HTML5 File API与拖放API实现的多图片上传预览源码适合Web前端初中级开发者、网站维护人员以及需要快速集成图片选择功能的项目团队。项目完全在浏览器端完成本地图片读取与预览无需服务器参与用户既可以通过文件输入框一次选择多张图片也可以直接把图片拖进预览区域利用FileReader将图片转换为Base64数据URL并即时渲染出缩略图。压缩包共18个文件包含HTML入口页面、5个JavaScript脚本覆盖核心上传逻辑、jQuery依赖等、1个CSS样式表与9张界面状态图标整体只有145KB目录结构清晰便于直接引用或学习改造。目前已有1470人学习下载源码已经作者测试可运行除了基础上传预览功能包内还附有核心控制模块能帮助开发者深入理解文件读取闭包传参、拖放事件处理以及原生JavaScript与jQuery协同工作的方式非常适合作为教学示例或项目基础模板。1. 多图上传预览一个让你少写三天业务代码的前端利器图片上传预览这个需求几乎每个做过后台管理系统或者内容发布平台的人都躲不过。但真正上手会发现单图上传还好说一旦涉及到多图选择、本地预览、删除重新选、限制数量、压缩处理这一套组合拳踩坑的概率成倍上升。这次拆的这个HTML5多图片上传预览源码包是一套已经跑通的前端完整实现不依赖任何后端接口也能独立演示和二次开发。它做的事情很简单选择一个或多个图片文件本地立即显示出缩略图支持逐张删除和全量清空选完还能拿到统一格式的图片数据对象方便丢给接口做后续的 FormData 提交。纯原生 HTML5 加上少量 JavaScript 代码没有任何框架依赖意味着直接双击 index.html 就能看到效果也可以轻松嵌进 Vue、React 或者传统的多页面项目里。对于正在做 CMS 后台、电商商品发布、用户头像裁剪这类模块的开发者来说这份源码的价值在于它把多图上传里那些“看起来简单、做起来麻烦”的边界情况都处理干净了。2. 整体架构与目录结构一个文件搞定核心扩展点都留好了2.1 目录组成与技术选型解压这份源码之后你会看到非常典型的原生前端工程结构核心就三个文件index.html、style.css 和 upload.js外加一个简单的 README 说明文档。这种结构在某些人眼里可能显得“不够工程化”但在我看来恰好是它最大的优势——没有构建步骤、没有 npm install、没有 node_modules 那一大堆东西你拿到手的是一份可以直接阅读、理解和改动的纯文本文件。技术栈这块是标准的 HTML5 File API 加 URL.createObjectURL。前者负责读取用户选中的文件对象并获取其类型、大小、名称等元数据后者负责生成一个临时的本地预览地址把图片直接渲染在页面上。和传统做法相比这种方案绕开了 base64 编码的性能损耗也不需要通过 canvas 先把图片画一遍才能预览内存占用小得多。html5-image-upload/ ├── index.html // 页面骨架与表单结构 ├── style.css // 布局样式与缩略图卡片样式 ├── upload.js // 核心逻辑预览、校验、删除、数据打包 └── README.md // 使用说明与参数配置文件数量就这么点但分工清晰。index.html 负责搭架子upload.js 里装的才是真正值钱的逻辑。后续做二次开发时最常动的也是 upload.js 这个文件。2.2 核心设计思路输入框隐藏用 label 触发选择大部分初学多图上传的人都会在页面上放一个 input[typefile]然后直接靠这个原生控件去触发文件选择。但原生控件在不同浏览器下的 UI 长得完全不一样尤其是 Safari 里丑得让人不想看。这份源码采用了一个非常经典的“隐藏 input label 触发”模式input 设置display: none藏起来用一个自定义样式的 label 元素去模拟按钮的外观通过 label 的 for 属性指向 input 的 id点击 label 就等于点击了 input。input typefile idfileInput acceptimage/* multiple hidden / label forfileInput classupload-btn选择图片/label这种做法在用户交互层面几乎无感知成本。你点的是一个有圆角、有背景色、有 hover 效果的按钮但实际触发的是原生的文件选择器。别看就这么一个细节它决定了你后续改样式、做皮肤切换、适配移动端的时候能省多少事——你永远不需要跟原生控件的样式死磕。这块的另外一个设计要点是multiple属性。没有这个属性input 一次只能选择一个文件多图上传就无从谈起。acceptimage/*的作用是让文件选择器默认过滤非图片格式的选项但注意这只是一个“辅助筛选”并不是严格的格式校验真正的校验在 upload.js 里做。2.3 预览容器的状态设计页面上有一个预览容器默认是空的。随着用户不断选择图片容器内部会动态追加一个个缩略图卡片每个卡片包含图片预览、文件名显示和删除按钮。同时页面底部还有一个文件队列状态条实时显示已经选择了多少张图片、还能选择多少张。div idpreviewContainer classpreview-grid/div div classqueue-status 已选择 span idcount0/span 张最多可上传 span idmaxCount6/span 张 /div这个状态条看着不起眼但它是用户体验很重要的部分。没有它用户选了两张图之后根本不知道有没有选成功不知道还能不能继续加。有这个实时反馈整个交互闭环就完整了——选择、反馈、操作、再反馈。3. 核心模块深度拆解从文件选择到缩略图渲染的完整链路3.1 全局参数区所有可调参数集中在文件头部upload.js 文件开头定义了一个配置对象这是整个脚本最先落地的“参数开关”。我一般习惯把这类可调参数集中在文件头部目的是让使用这份源码的人不用通读全部逻辑就能完成基本需求调整。const CONFIG { maxCount: 6, // 最大允许选择的图片数量 maxSizeMB: 5, // 单张图片大小上限 acceptTypes: [image/jpeg, image/png, image/gif, image/webp], // 允许上传的 MIME 类型 previewQuality: 0.92, // 如果开启 canvas 压缩压缩质量参数 useCompress: false // 是否开启本地压缩默认不开启 };逻辑说明maxCount控制整个队列里最多能容纳的图片数量。初始为 6你改成 9 或者 12 都行取决于业务需求。maxSizeMB是单张文件大小限制。这是在前端做的“软限制”真正的硬校验还是要在后端再做一次前端的限制只是为了帮用户早点发现问题。acceptTypes规定了哪些 MIME 类型可以进入队列。jpeg 和 png 基本是标配gif 存在是因为很多场景需要保留动画webp 则是为了适配现代浏览器的高效格式需求。最后的useCompress和previewQuality是留作扩展的。后续如果要做“选择超大图后自动压缩再上传”只需要把这两个开关打开就能切入压缩流程。这里特别解释一下为什么校验要写进 CONFIG 而不是散落在代码各处当产品经理跟你说“图片上限从 6 张改成 10 张”的时候你只需要改动这一行。这是个很小的习惯但真的能省下不少来回翻代码的时间。3.2 文件选择监听与前置过滤input 元素监听change事件一旦用户完成文件选择事件触发脚本开始处理 FileList 对象。FileList 本质上是一个类数组对象每个元素是 File 对象。这里需要特别注意的是同一个 input 第二次选择文件时新的 FileList 会覆盖旧的所以如果已经选过的图片需要保留就必须在新选择进来之前把旧队列保存下来。let selectedFiles []; fileInput.addEventListener(change, function (e) { const newFiles Array.from(e.target.files); let validFiles []; let rejectedCount 0; for (const file of newFiles) { if (!CONFIG.acceptTypes.includes(file.type)) { rejectedCount; continue; } if (file.size CONFIG.maxSizeMB * 1024 * 1024) { rejectedCount; continue; } if (selectedFiles.length validFiles.length CONFIG.maxCount) { rejectedCount; continue; } validFiles.push(file); } selectedFiles selectedFiles.concat(validFiles); renderPreview(); updateStatus(); e.target.value ; if (rejectedCount 0) { alert(有 rejectedCount 个文件未通过校验请检查格式、大小和数量限制); } });逻辑说明Array.from(e.target.files)这一步必须要做。FileList 是类数组对象直接调用数组的 forEach 或者 filter 会报错转成真正的数组之后才能用循环处理。遍历每个文件时依次做三项校验MIME 类型是否在允许列表内、大小是否超过限制、当前队列加上新文件的容量是否超过 maxCount。顺序很重要先类型后大小是因为类型判断最廉价用一个字符串比对就能排除掉一大半不合法的文件。selectedFiles selectedFiles.concat(validFiles)实现了新旧文件合并也就是支持多次选择、持续追加而不是每次选择都覆盖上一次的结果。e.target.value 这行是很多新手会漏掉的。input 的文件列表不手动清空的话第二次选择相同的文件会触发不了 change 事件因为浏览器认为值没变。清空 value 等于重置控件的状态保证每次选择都有响应。最后一段 rejectedCount 的逻辑是给用户一个温和的提示说明某些文件没进来。这在产品运营后台里很常见——运营人员选了一堆图结果因为格式或者大小被悄悄过滤掉了如果不提示他们根本不知道发生了什么。3.3 预览渲染用 createObjectURL 生成缩略图文件通过校验进入队列之后需要立刻在页面中渲染出缩略图。渲染的核心是URL.createObjectURL()这个方法它会为每一个 File 对象生成一个blob:开头的临时 URL。这个 URL 可以赋值给 img 标签的 src 属性浏览器会直接从内存中读取文件内容渲染图片整个过程不需要等待网络请求所以预览几乎是即时出现的。function renderPreview() { const container document.getElementById(previewContainer); container.innerHTML ; selectedFiles.forEach((file, index) { const card document.createElement(div); card.className preview-card; const img document.createElement(img); img.src URL.createObjectURL(file); img.alt file.name; const fileName document.createElement(div); fileName.className file-name; fileName.textContent file.name; // 文件名过长时省略显示 if (file.name.length 18) { fileName.textContent file.name.substring(0, 15) ...; } const deleteBtn document.createElement(div); deleteBtn.className delete-btn; deleteBtn.textContent ×; // 删除按钮点击时移除对应文件 deleteBtn.addEventListener(click, function () { selectedFiles.splice(index, 1); renderPreview(); updateStatus(); }); card.appendChild(img); card.appendChild(fileName); card.appendChild(deleteBtn); container.appendChild(card); }); }逻辑说明整个渲染过程是一个全量重绘的思路。每次队列发生变化——无论是新增还是删除——都把整个预览容器的内容清空然后根据最新队列重新生成所有卡片。这种做法的代价是 DOM 操作略频繁但胜在状态永远一致不会出现“某个卡片忘了更新”的隐蔽 bug。删除按钮的 index 是外层 forEach 循环的 index。这里有个潜在的坑当用户删除了一张图片之后后面的图片 index 全部前移而新增图片时又追加在末尾如果 splice 用的是旧 index 就会删错对象。这块代码巧妙地通过删除后立即重绘调用了 renderPreview规避了这个问题因为重绘之后所有按钮的 index 都刷新了。URL.createObjectURL(file)生成的 URL 需要手动回收。虽然不在每次 renderPreview 里手动 revokeObjectURL是因为全量重绘时老的 URL 已经不再被引用但浏览器内部的资源释放只是延迟的如果你处理的图片非常多且非常大建议在清空容器之前先回收旧 URL。这份源码在全部选择清空时才做统一回收。3.4 数据打包为接口提交准备的最终输出格式用户选完图、调整好顺序、删掉不想要的最后一步要做的就是把选中的图片集合提交给后端。现代上传接口常用的方案是 FormData也就是以 multipart/form-data 格式发 POST 请求。这份源码里提供了一行代码就能完成队列到 FormData 的转换方法。function getFormData() { const formData new FormData(); selectedFiles.forEach((file, index) { formData.append(images[], file, file.name); }); return formData; } // 调用示例 function uploadImages() { if (selectedFiles.length 0) { alert(请先选择至少一张图片); return; } const formData getFormData(); // 这里可以配合 fetch 或 axios 发送请求 // fetch(/api/upload, { method: POST, body: formData }) }逻辑说明formData.append(images[], file, file.name)这行的核心是三个参数。第一个参数images[]是字段名中括号是 PHP 后端喜欢见的命名格式如果你们的后端是 Java 或者 Node.js直接改成images或者files就好。第二个参数 file 是 File 对象本身。第三个参数 file.name 是文件名给文件名之后再传输可以帮助后端正确识别文件的原始名称和扩展名。为什么用 FormData 而不用 JSONJSON 只能传字符串图片文件的二进制内容要先变成 base64体积膨胀约 30%而且后端还要多一步解码。FormData 是浏览器原生的表单提交编码方式文件内容直接以二进制流放进请求体里效率更高。这样核心业务逻辑就完成了闭环选择文件、校验过滤、生成预览、删除调整、最后打包提交。整个过程中页面不刷新、用户体验接近原生 App。4. 避坑指南多图上传开发中常见的几个翻车点4.1 文件类型判断不可靠只看扩展名会放行伪图片很多人校验图片格式用的是file.name.endsWith(.jpg)或者file.name.endsWith(.png)这种方式但这真的不靠谱。文件的扩展名用户可以手动改把一个文本文件改成 .jpg里面的内容根本不是图片硬塞进队列之后预览会破图。原因文件扩展名只是一段字符串和文件的实际二进制内容没有必然关系。而file.type是浏览器根据文件的真实 MIME 类型识别的基于文件头部的魔数判断比扩展名可靠得多。解决用file.type做校验配合acceptTypes白名单。但注意接受列表不要太窄比如只接受 image/jpeg 和 image/png 会把很多网站的 webp 缩略图拦在外面具体放哪些类型取决于你的业务场景。4.2 已选文件被覆盖第二次选择把之前的图全丢了假如没有把旧队列在 change 事件里保存下来直接selectedFiles newFiles那么用户第一次选了两张图再点选择按钮又加了两张第二次的 FileList 会直接覆盖第一次的结果页面上的预览只剩下最后两张。用户反复确认之后会以为上传功能有 bug。原因input 的 files 属性每次选择都是全新的 FileList 对象旧引用已经不存在了。如果没有手动做数组合并旧的就没有了。解决每次 change 之后做selectedFiles selectedFiles.concat(validFiles)旧队列必须当保存对象来处理。我一般还会在团队里统一约定任何涉及文件列表的多选场景都要先 concat 再重绘。4.3 清空按钮失效数组清空但界面没变化这种问题的根源多半出在“变量引用”上。如果你把 selectedFiles 直接赋值给一个常量const selected selectedFiles然后在删除时用了这个常量去splice会发现数组某个瞬间莫名其妙变成了空但页面还是老样子。这其实是 JS 引用传递导致的如果你用selectedFiles []来清空但页面渲染时读的是另一个变量引用旧引用指向的数组内容其实没变。原因引用和值在 JavaScript 里是两个概念。直接传引用给对方双方指向同一个内存地址。你通过引用清空或者说通过其他变量操作画面状态并不会自动更新数据层。解决最稳妥的方式是清空时重新赋值一个新数组selectedFiles []然后强制调用 renderPreview 重绘。不要只依赖内部方法操作容器也不要怀疑为什么重绘函数没被触发——重绘必须由外部显式调用。4.4 浏览器兼容性Safari 的 URL.createObjectURL 内存泄漏问题Safari 对URL.createObjectURL()的支持虽然存在但在某些版本下如果只创建不回收内存占用会逐步上涨。尤其是用户在同一个页面里频繁选择不同的图片几十张之后整个标签页的内存可能冲到 200MB 以上。原因createObjectURL 创建的临时 URL 不是没有生命周期的它们会在文档卸载时才被全部释放。单页应用里文档生存期很长如果每次都创建不回收泄漏就像滚雪球一样积少成多。解决在替换图片或者删除图片时主动调URL.revokeObjectURL(prevUrl)。具体在这份源码里可以给每个卡片挂一个 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

相关新闻

CSAPP datalab位操作实战:从补码到饱和加法的底层思维训练

CSAPP datalab位操作实战:从补码到饱和加法的底层思维训练

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

2026/10/16 2:19:51 阅读更多 →
C++抽卡模拟器源码解析:概率保底机制与二次开发实战

C++抽卡模拟器源码解析:概率保底机制与二次开发实战

简介:这是一份面向C初学者与原神玩家的抽卡模拟器源码工程,用C复刻游戏内祈愿逻辑,帮助学习者在无需消耗原石的前提下理解抽卡概率与保底机制。资源共38个文件,以cpp与hpp源码、txt与log日志、pdb与obj编译中间文件、sln与vcxproj…

2026/10/16 2:19:51 阅读更多 →
AI毕业论文写作指南:从选题到答辩的全流程工具盘点

AI毕业论文写作指南:从选题到答辩的全流程工具盘点

专科生的毕业论文,真的没必要硬扛。每年到了毕业季,总有一批人被论文折磨得焦头烂额:选题没思路、文献读不懂、正文憋不出来、格式调到头大、查重不过关、答辩前一天还在改PPT。我当年也经历过这一整套流程,深知其中的痛苦。但这两…

2026/10/16 2:18:51 阅读更多 →

最新新闻

DNS配置决定数据库集群高可用:从Oracle SCAN到SQL Server故障转移

DNS配置决定数据库集群高可用:从Oracle SCAN到SQL Server故障转移

1. DNS在设计高可用数据库集群时为何常常被低估做数据库运维这些年,我越来越发现一个尴尬的事实:很多团队在搭建Oracle RAC或者SQL Server集群时,把大量精力放在存储选型、网络心跳、共享盘仲裁这些“硬核”环节上,唯独对DNS配置的…

2026/10/16 4:07:13 阅读更多 →
网络放大器设置问题:数据结构与算法课程设计C++实现方案

网络放大器设置问题:数据结构与算法课程设计C++实现方案

简介:这份资源面向山东大学数据结构与算法课程设计的学习者,聚焦实验四「网络放大器设置问题」的完整求解方案。题目以加权有向无环图描述汽油传送网络,要求在保证最小压力Pmin的前提下,用最少数量的放大器使相邻放大器间距离不超…

2026/10/16 4:07:13 阅读更多 →
智能井盖传感器全链路实战:从MEMS感知到NB-IoT上报

智能井盖传感器全链路实战:从MEMS感知到NB-IoT上报

智能井盖传感器这活儿,圈外人觉得就是给井盖安个报警器,真做起来才发现,从一颗MEMS芯片的原始读数到云端地图上一个小红点,中间要过九重关卡。这篇文章把我在实际项目里踩过的路完整走一遍,重点聊从MEMS感知到NB-IoT上…

2026/10/16 4:07:13 阅读更多 →
深度优先与广度优先遍历:从连通图到非连通图的完整实现

深度优先与广度优先遍历:从连通图到非连通图的完整实现

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

2026/10/16 4:07:13 阅读更多 →
Vue事件修饰符详解:原理、组合与实战场景

Vue事件修饰符详解:原理、组合与实战场景

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

2026/10/16 4:07:13 阅读更多 →
SQLITE-WEB 远程访问完全指南:监听、防火墙、SSH 转发与安全加固

SQLITE-WEB 远程访问完全指南:监听、防火墙、SSH 转发与安全加固

如果你手上有几个 SQLite 数据库文件,散落在服务器或自己的电脑里,想用浏览器直接看表、跑 SQL、导出 CSV,SQLITE-WEB 是我用得最顺手的工具之一。它本质上是一个用 Go 写的轻量 Web 服务,把一个 SQLite 文件暴露成可交互的管理页…

2026/10/16 4:06:13 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →