文章目录
工具介绍
在日常的文档编写和博客创作中,Markdown因其简洁的语法和良好的可读性而广受欢迎。然而,当文档结构复杂、标题层级较多时,手动维护标题序号不仅耗时耗力,而且容易出错。所以,开发了这款Markdown标题序号编辑器,它具备以下突出优点。
核心优势
-
完全本地运行
- 单HTML文件实现,无需安装任何软件
- 不依赖网络连接,所有操作均在浏览器中完成
- 保护隐私安全,文件内容不会上传到任何服务器
-
智能序号管理
- 自动识别文档中的最大标题层级,智能生成序号
- 支持多级标题序号自动编排(如1.1, 1.2.1等)
- 完美处理已有序号的文档,重新编号时保持逻辑正确
-
极致简洁体验
- 拖拽文件即可使用,操作直观简单
- 响应式设计,适配各种设备屏幕
- 清爽的界面布局,专注核心功能
-
完备的功能集
- 一键添加/移除标题序号
- 一键复制处理结果到剪贴板
- 一键下载覆盖原文件
- 一键重置编辑器状态
-
贴心的交互设计
- 顶部弹出式通知提示,不干扰内容布局
- 右下角返回顶部按钮,方便长文档操作
- 实时预览修改内容,所见即所得
- 动画效果平滑过渡,提升使用体验
-
专业的技术实现
- 纯前端实现,无需后端支持
- 采用现代Web API(如Clipboard API)
- 精心设计的算法确保序号逻辑准确
- 完整错误处理机制,操作安全可靠
使用指南
基本使用方法
-
打开工具
- 复制下方完整代码,将其以HTML文件的形式保存到本地
- 双击用浏览器打开(推荐Chrome/Firefox/Edge等现代浏览器)
-
导入Markdown文件
- 方式一:直接拖拽.md文件到指定区域
- 方式二:点击区域选择文件
-
编辑标题序号
- 点击"添加标题序号":自动为所有标题添加智能编号
- 点击"移除标题序号":一键清除所有标题编号
-
获取结果
- "复制内容":将处理后的文本复制到剪贴板
- "下载文件":保存处理后的文件(默认使用原文件名)
- "重置":清空当前内容,重新开始
-
处理复杂文档
- 工具会自动识别文档结构,无论最大标题是几级,都会从1开始编号
- 例如:如果文档最大标题是###,编号会是1., 1.1., 1.2.等
-
编辑中途调整
- 随时可以切换"添加"和"移除"序号,系统会保持正确的编号逻辑
- 新增无序号标题后,再次添加序号会自动重新编排
注意事项
- 工具仅处理标准的Markdown标题语法(#、##等)
- 建议在处理前备份重要文档
- 极少数情况下,特殊格式的标题可能需要手动调整
- 如需处理超大文件(10MB以上),建议分章节操作
部分截图
亮色主题:

夜间模式:
完整代码
复制下方的代码,保存至一个后缀为 .html
的文件中,双击用浏览器打开即可使用。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown标题序号编辑器</title>
<style>
:root {
--primary-color: #4a6fa5;
--secondary-color: #6b8cae;
--accent-color: #ff7e5f;
--light-color: #f8f9fa;
--dark-color: #343a40;
--success-color: #28a745;
--info-color: #17a2b8;
--warning-color: #ffc107;
--error-color: #dc3545;
--border-radius: 8px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--bg-color: #f5f7fa;
--text-color: #343a40;
--container-bg: white;
--preview-bg: #f8f9fa;
--preview-border: #ddd;
--placeholder-color: #6c757d;
}
/* 科技感主题 */
.tech-theme {
--primary-color: #3a86ff;
--secondary-color: #8338ec;
--accent-color: #ff006e;
--light-color: #1a1a2e;
--dark-color: #e6e6e6;
--success-color: #06d6a0;
--info-color: #118ab2;
--warning-color: #ffd166;
--error-color: #ef476f;
--bg-color: #0f0f1b;
--text-color: #e6e6e6;
--container-bg: #1a1a2e;
--preview-bg: #16213e;
--preview-border: #3a3a5d;
--placeholder-color: #8a8a9d;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--bg-color);
margin: 0;
padding: 20px;
/* padding-bottom: 0px;
padding-top: 0px; */
display: flex;
flex-direction: column;
align-items: center;
min-height: 90vh;
position: relative;
transition: all 0.3s ease;
}
.container {
width: 100%;
max-width: 900px;
background-color: var(--container-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 25px;
margin-bottom: 10px;
transition: all 0.3s ease;
}
h1 {
color: var(--primary-color);
text-align: center;
margin-bottom: 25px;
font-weight: 600;
transition: all 0.3s ease;
}
.drop-area {
border: 3px dashed var(--secondary-color);
border-radius: var(--border-radius);
padding: 35px 20px;
text-align: center;
margin-bottom: 25px;
transition: all 0.3s ease;
background-color: rgba(106, 140, 174, 0.05);
cursor: pointer;
position: relative;
}
.drop-area.highlight {
border-color: var(--accent-color);
background-color: rgba(255, 126, 95, 0.1);
}
.drop-area p {
font-size: 18px;
color: var(--secondary-color);
margin: 0;
transition: all 0.3s ease;
}
.drop-area .icon {
font-size: 48px;
color: var(--secondary-color);
margin-bottom: 15px;
transition: all 0.3s ease;
}
.new-file-btn {
position: absolute;
right: 15px;
bottom: 15px;
background: none;
border: none;
color: var(--secondary-color);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 15px;
transition: all 0.3s ease;
}
.new-file-btn:hover {
background-color: rgba(106, 140, 174, 0.1);
color: var(--primary-color);
}
.file-info {
margin-top: 20px;
padding: 15px;
background-color: var(--light-color);
border-radius: var(--border-radius);
display: none;
transition: all 0.3s ease;
}
.file-info.show {
display: block;
}
.file-info p {
margin: 5px 0;
word-break: break-all;
transition: all 0.3s ease;
}
.toolbar {
position: sticky;
top: 0;
background-color: var(--container-bg);
padding: 15px 0;
z-index: 100;
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
border-bottom: 1px solid var(--preview-border);
margin-bottom: 15px;
}
button {
padding: 10px 18px;
border: none;
border-radius: var(--border-radius);
background-color: var(--primary-color);
color: white;
font-size: 15px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
button:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
transform: none;
opacity: 0.7;
}
button.download {
background-color: var(--success-color);
}
button.download:hover {
background-color: #218838;
}
button.remove {
background-color: var(--accent-color);
}
button.remove:hover {
background-color: #e66a4d;
}
button.copy {
background-color: var(--info-color);
}
button.copy:hover {
background-color: #138496;
}
button.reset {
background-color: #6c757d;
}
button.reset:hover {
background-color: #5a6268;
}
.preview-container {
margin-top: 15px;
display: none;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.preview-title {
font-size: 18px;
font-weight: 600;
color: var(--primary-color);
transition: all 0.3s ease;
}
.edit-toggle-container {
display: flex;
align-items: center;
gap: 8px;
}
.edit-toggle-label {
font-size: 14px;
color: var(--text-color);
cursor: pointer;
user-select: none;
}
.edit-toggle {
width: 16px;
height: 16px;
cursor: pointer;
}
.preview-content {
max-height: calc(100vh - 400px);
min-height: 200px;
overflow-y: auto;
padding: 15px;
background-color: var(--preview-bg);
border-radius: var(--border-radius);
border: 1px solid var(--preview-border);
white-space: pre-wrap;
font-family: 'Courier New', Courier, monospace;
transition: all 0.3s ease;
outline: none;
}
.preview-content.editable {
cursor: text;
border: 1px solid var(--info-color);
}
.preview-content[placeholder]:empty:before {
content: attr(placeholder);
color: var(--placeholder-color);
opacity: 0.8;
}
footer {
/* margin-top: 20px; */
text-align: center;
color: var(--secondary-color);
font-size: 13px;
padding: 15px;
width: 100%;
transition: all 0.3s ease;
}
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 999;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
background-color: var(--secondary-color);
transform: translateY(-3px);
}
.theme-toggle {
position: fixed;
bottom: 90px;
right: 30px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--accent-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 999;
}
.theme-toggle:hover {
transform: rotate(30deg) scale(1.1);
}
.notification-container {
position: fixed;
top: 20px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 1000;
pointer-events: none;
}
.notification {
padding: 12px 24px;
border-radius: var(--border-radius);
color: white;
font-weight: 500;
box-shadow: var(--box-shadow);
transform: translateY(-100px);
opacity: 0;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
margin-bottom: 10px;
max-width: 80%;
text-align: center;
}
.notification.show {
transform: translateY(0);
opacity: 1;
}
.notification.success {
background-color: var(--success-color);
}
.notification.info {
background-color: var(--info-color);
}
.notification.warning {
background-color: var(--warning-color);
}
.notification.error {
background-color: var(--error-color);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: var(--container-bg);
padding: 25px;
border-radius: var(--border-radius);
width: 90%;
max-width: 400px;
box-shadow: var(--box-shadow);
}
.modal-title {
margin-top: 0;
color: var(--primary-color);
}
.modal-input {
width: 100%;
padding: 10px;
margin: 15px 0;
border: 1px solid var(--preview-border);
border-radius: var(--border-radius);
background-color: var(--preview-bg);
color: var(--text-color);
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
@media (max-width: 768px) {
.toolbar {
position: static;
flex-direction: column;
align-items: center;
}
button {
width: 100%;
}
.back-to-top, .theme-toggle {
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
font-size: 18px;
}
.theme-toggle {
bottom: 70px;
}
}
</style>
</head>
<body>
<div id="notificationContainer" class="notification-container"></div>
<div id="newFileModal" class="modal">
<div class="modal-content">
<h3 class="modal-title">创建新Markdown文件</h3>
<input type="text" id="newFileName" class="modal-input" placeholder="输入文件名(无需后缀)">
<div class="modal-buttons">
<button id="cancelNewFile" class="reset">取消</button>
<button id="confirmNewFile" class="download">创建</button>
</div>
</div>
</div>
<div class="container">
<h1>Markdown标题序号编辑器</h1>
<div id="dropArea" class="drop-area">
<div class="icon">📄</div>
<p>拖拽Markdown文件到此处</p>
<p><small>或点击选择文件</small></p>
<button id="newFileBtn" class="new-file-btn">
<span>+</span> 新建文件
</button>
<input type="file" id="fileInput" accept=".md,.markdown" style="display: none;">
</div>
<div id="fileInfo" class="file-info">
<p><strong>文件名:</strong> <span id="fileName"></span></p>
<p><strong>大小:</strong> <span id="fileSize"></span></p>
</div>
<div class="toolbar" id="toolbar">
<button id="addNumbersBtn" disabled>
<span>🔢</span> 添加标题序号
</button>
<button id="removeNumbersBtn" disabled class="remove">
<span>✖️</span> 移除标题序号
</button>
<button id="copyBtn" disabled class="copy">
<span>⎘</span> 复制内容
</button>
<button id="downloadBtn" disabled class="download">
<span>⤵️</span> 下载文件
</button>
<button id="resetBtn" class="reset">
<span>↺</span> 重置
</button>
</div>
<div id="previewContainer" class="preview-container">
<div class="preview-header">
<div class="preview-title">预览</div>
<div class="edit-toggle-container">
<label class="edit-toggle-label" for="editToggle">编辑模式</label>
<input type="checkbox" id="editToggle" class="edit-toggle">
</div>
</div>
<div id="previewContent" class="preview-content" placeholder="请加载或创建Markdown文件..."></div>
</div>
</div>
<div id="backToTop" class="back-to-top" title="返回顶部">↑</div>
<div id="themeToggle" class="theme-toggle" title="切换主题">🌓</div>
<!-- <footer>
Markdown标题序号编辑器 © 2025 | 双击HTML文件即可使用
</footer> -->
<script>
// DOM元素
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const addNumbersBtn = document.getElementById('addNumbersBtn');
const removeNumbersBtn = document.getElementById('removeNumbersBtn');
const copyBtn = document.getElementById('copyBtn');
const downloadBtn = document.getElementById('downloadBtn');
const resetBtn = document.getElementById('resetBtn');
const editToggle = document.getElementById('editToggle');
const previewContainer = document.getElementById('previewContainer');
const previewContent = document.getElementById('previewContent');
const backToTop = document.getElementById('backToTop');
const themeToggle = document.getElementById('themeToggle');
const notificationContainer = document.getElementById('notificationContainer');
const newFileBtn = document.getElementById('newFileBtn');
const newFileModal = document.getElementById('newFileModal');
const newFileName = document.getElementById('newFileName');
const cancelNewFile = document.getElementById('cancelNewFile');
const confirmNewFile = document.getElementById('confirmNewFile');
const toolbar = document.getElementById('toolbar');
let currentFile = null;
let originalContent = '';
let modifiedContent = '';
let isTechTheme = false;
// 初始化 - 监听滚动显示返回顶部按钮
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTop.classList.add('visible');
} else {
backToTop.classList.remove('visible');
}
});
// 返回顶部功能
backToTop.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// 切换主题
themeToggle.addEventListener('click', () => {
isTechTheme = !isTechTheme;
if (isTechTheme) {
document.body.classList.add('tech-theme');
showNotification('已切换至科技主题', 'info');
} else {
document.body.classList.remove('tech-theme');
showNotification('已切换至默认主题', 'info');
}
});
// 拖拽事件处理
dropArea.addEventListener('click', (e) => {
if (e.target === dropArea || e.target === dropArea.querySelector('.icon') || e.target === dropArea.querySelector('p')) {
fileInput.click();
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFile(e.target.files[0]);
}
});
// 阻止默认拖拽行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// 高亮拖拽区域
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('highlight');
}
function unhighlight() {
dropArea.classList.remove('highlight');
}
// 处理拖放文件
dropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const file = dt.files[0];
if (file && (file.name.endsWith('.md') || file.name.endsWith('.markdown'))) {
handleFile(file);
} else {
showNotification('请拖拽Markdown文件(.md或.markdown)', 'warning');
}
}, false);
// 新建文件处理
newFileBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
newFileModal.style.display = 'flex';
newFileName.focus();
});
cancelNewFile.addEventListener('click', () => {
newFileModal.style.display = 'none';
newFileName.value = '';
});
confirmNewFile.addEventListener('click', () => {
const name = newFileName.value.trim();
if (name) {
const filename = name.endsWith('.md') ? name : `${name}.md`;
createNewFile(filename);
newFileModal.style.display = 'none';
newFileName.value = '';
} else {
showNotification('请输入有效的文件名', 'warning');
}
});
function createNewFile(filename) {
currentFile = { name: filename };
originalContent = '';
modifiedContent = '';
fileName.textContent = filename;
updateFileSize();
fileInfo.classList.add('show');
updatePreview();
enableButtons();
editToggle.checked = true;
toggleEditMode();
previewContent.focus();
showNotification(`已创建新文件: ${filename}`, 'success');
}
// 文件处理
function handleFile(file) {
currentFile = file;
fileName.textContent = file.name;
fileInfo.classList.add('show');
const reader = new FileReader();
reader.onload = (e) => {
originalContent = e.target.result;
modifiedContent = originalContent;
updateFileSize();
updatePreview();
enableButtons();
showNotification('文件已加载: ' + file.name, 'success');
};
reader.onerror = (e) => {
showNotification('读取文件失败: ' + e.target.error, 'error');
};
reader.readAsText(file);
}
// 更新文件大小显示
function updateFileSize() {
const content = previewContent.textContent;
const bytes = new Blob([content]).size;
fileSize.textContent = formatFileSize(bytes);
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function enableButtons() {
addNumbersBtn.disabled = false;
removeNumbersBtn.disabled = false;
copyBtn.disabled = false;
downloadBtn.disabled = false;
previewContainer.style.display = 'block';
}
// 编辑模式切换
editToggle.addEventListener('change', toggleEditMode);
function toggleEditMode() {
if (editToggle.checked) {
previewContent.contentEditable = true;
previewContent.classList.add('editable');
showNotification('已进入编辑模式', 'info');
previewContent.focus();
} else {
previewContent.contentEditable = false;
previewContent.classList.remove('editable');
modifiedContent = previewContent.textContent;
updateFileSize();
showNotification('已退出编辑模式', 'info');
}
}
// 处理回车键
previewContent.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
// 阻止默认行为并插入换行
e.preventDefault();
document.execCommand('insertLineBreak');
}
});
// 内容变化时更新文件大小
previewContent.addEventListener('input', () => {
updateFileSize();
});
// 标题序号处理
addNumbersBtn.addEventListener('click', () => {
modifiedContent = addHeadingNumbers(previewContent.textContent);
updatePreview();
showNotification('已添加标题序号', 'success');
});
removeNumbersBtn.addEventListener('click', () => {
modifiedContent = removeHeadingNumbers(previewContent.textContent);
updatePreview();
showNotification('已移除标题序号', 'success');
});
function addHeadingNumbers(content) {
const lines = content.split('\n');
let counters = [0, 0, 0, 0, 0, 0]; // h1到h6
let maxLevel = 6;
// 首先确定文档中最大的标题级别
const findMaxLevel = () => {
for (const line of lines) {
if (line.startsWith('#')) {
const level = line.match(/^#+/)[0].length;
if (level < maxLevel) {
maxLevel = level;
}
}
}
return maxLevel;
};
maxLevel = findMaxLevel();
return lines.map(line => {
const match = line.match(/^(#+)\s*(?:\d+\.?\.?)*\s*(.*)/);
if (match) {
const level = match[1].length;
const titleText = match[2].trim();
// 重置子级计数器
for (let i = level; i < counters.length; i++) {
counters[i] = 0;
}
// 增加当前级别计数器
counters[level - 1]++;
// 生成序号 (只从最大级别开始)
let number = '';
for (let i = maxLevel - 1; i < level; i++) {
if (i === maxLevel - 1) {
number += counters[i];
} else {
number += '.' + counters[i];
}
}
return `${match[1]} ${number}. ${titleText}`;
}
return line;
}).join('\n');
}
function removeHeadingNumbers(content) {
return content.replace(/^(#+)\s*(?:\d+\.?\.?)*\s*(\.?)\s*/gm, (match, hashes, dot) => {
return `${hashes} `;
});
}
// 复制内容到剪贴板
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(previewContent.textContent);
showNotification('内容已复制到剪贴板', 'success');
} catch (err) {
showNotification('复制失败: ' + err, 'error');
}
});
// 重置功能
resetBtn.addEventListener('click', () => {
if (confirm('确定要重置编辑器吗?所有未保存的更改将丢失。')) {
currentFile = null;
originalContent = '';
modifiedContent = '';
fileInfo.classList.remove('show');
previewContainer.style.display = 'none';
addNumbersBtn.disabled = true;
removeNumbersBtn.disabled = true;
copyBtn.disabled = true;
downloadBtn.disabled = true;
fileInput.value = '';
editToggle.checked = false;
toggleEditMode();
showNotification('编辑器已重置', 'info');
}
});
// 显示通知消息
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notificationContainer.appendChild(notification);
// 触发动画
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 3秒后移除通知
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 400);
}, 3000);
}
// 预览更新
function updatePreview() {
previewContent.textContent = modifiedContent;
updateFileSize();
}
// 下载处理
downloadBtn.addEventListener('click', () => {
if (!currentFile) return;
const blob = new Blob([previewContent.textContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentFile.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('文件已下载: ' + currentFile.name, 'success');
});
</script>
</body>
</html>