refactor: 用 CodeMirror 6 重构 MD 编辑器,废弃自研 WYSIWYG

- 引入 CodeMirror 6 (MIT) 替代 contenteditable WYSIWYG 编辑器,消除 execCommand 废弃 API 风险
- 全新全宽三栏布局:标题栏、工具栏、编辑区+可切换预览、状态栏
- 工具栏新增删除线/H1/H4/任务列表/表格/预览切换/全屏按钮
- 支持粘贴图片自动上传 (Ctrl+V)、拖拽上传、上传进度反馈
- 代码块语言下拉选择器替代 prompt() 弹窗,支持搜索和键盘导航
- 自动保存草稿 (30s 防抖,localStorage + 后端静默保存)
- 分屏实时预览 (Ctrl+Shift+P),编辑器与预览区双向滚动同步
- 字数/行数/阅读时间统计、全屏编辑 (F11)
- 快捷键:Ctrl+B/I/K/`/Shift+I/S/Shift+P + Tab/Shift+Tab
- 删除旧的 htmlToMarkdown、syncMarkdownToWysiwyg、mode-tab 等废弃代码
- 后端 API 零改动,仅移除控制器中不再需要的 ExtraJS 字段
This commit is contained in:
2026-05-27 19:38:42 +08:00
parent 8a8d801187
commit 9801edc313
4 changed files with 852 additions and 700 deletions

View File

@ -162,263 +162,259 @@
gap: 12px;
}
/* ========== 编辑器 ========== */
.editor-container {
max-width: 1024px;
margin: 0 auto;
padding: 32px 16px;
}
.editor-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 24px;
}
.post-form .form-group {
margin-bottom: 16px;
}
.post-form label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
}
/* ========== 模式标签 ========== */
.editor-modes {
/* ========== 编辑器布局(全宽) ========== */
.editor-layout {
height: calc(100vh - 56px);
display: flex;
gap: 4px;
margin-bottom: 0;
flex-direction: column;
overflow: hidden;
}
.mode-tab {
padding: 8px 18px;
border: 1px solid #d1d5db;
border-bottom: none;
border-radius: 8px 8px 0 0;
background: #f9fafb;
color: #6b7280;
font-size: 13px;
font-weight: 500;
cursor: pointer;
position: relative;
z-index: 1;
.editor-form {
display: flex;
flex-direction: column;
height: 100%;
}
.mode-tab.active {
/* ---- 标题区域 ---- */
.editor-header {
padding: 16px 24px 12px;
border-bottom: 1px solid #e5e7eb;
background: #fff;
color: #6366f1;
border-color: #d1d5db;
z-index: 2;
margin-bottom: -1px;
padding-bottom: 9px;
flex-shrink: 0;
}
/* ========== 工具条 ========== */
.editor-title-input {
width: 100%;
padding: 6px 0;
border: none;
font-size: 26px;
font-weight: 700;
color: #111;
outline: none;
background: transparent;
line-height: 1.4;
}
.editor-title-input::placeholder {
color: #c4c4c4;
}
/* ---- 工具栏 ---- */
.editor-toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 6px 8px;
background: #fff;
border: 1px solid #d1d5db;
border-top: none;
border-radius: 0 0 8px 8px;
min-height: 40px;
flex-wrap: wrap;
padding: 5px 16px;
background: #fafafa;
border-bottom: 1px solid #e5e7eb;
min-height: 38px;
flex-shrink: 0;
overflow-x: auto;
}
/* 编辑区上边距 */
.editor-toolbar + .editor-body,
.editor-modes + .editor-body {
margin-top: 0;
.tb-group {
display: flex;
align-items: center;
gap: 1px;
}
.tb-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 30px;
width: 30px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: #4b5563;
cursor: pointer;
font-size: 13px;
font-size: 12px;
font-weight: 600;
transition: background 0.15s;
transition: background 0.12s, color 0.12s;
flex-shrink: 0;
}
.tb-btn:hover {
background: #f3f4f6;
background: #e5e7eb;
color: #111;
}
.tb-btn:active {
background: #d1d5db;
}
.tb-btn svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}
.tb-sep {
width: 1px;
height: 20px;
background: #e5e7eb;
margin: 0 4px;
margin: 0 6px;
flex-shrink: 0;
}
/* ========== 编辑区 ========== */
.editor-body {
position: relative;
clear: both;
.tb-spacer {
flex: 1;
}
.tb-right {
margin-left: auto;
}
.tb-toggle.active {
background: #e0e7ff;
color: #6366f1;
}
/* ---- 代码块语言选择器 ---- */
.lang-picker {
position: absolute;
top: 38px;
left: 50%;
transform: translateX(-50%);
width: 220px;
background: #fff;
border: 1px solid #d1d5db;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
z-index: 100;
overflow: hidden;
}
.form-textarea {
.lang-picker-search input {
width: 100%;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 15px;
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
line-height: 1.7;
resize: vertical;
box-sizing: border-box;
min-height: 400px;
}
/* WYSIWYG 编辑器 */
.wysiwyg-editor {
min-height: 400px;
padding: 14px 18px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
line-height: 1.8;
color: #1f2937;
padding: 10px 12px;
border: none;
border-bottom: 1px solid #e5e7eb;
font-size: 13px;
outline: none;
word-wrap: break-word;
overflow-wrap: break-word;
box-sizing: border-box;
}
.wysiwyg-editor:focus {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99,102,241,0.15);
.lang-picker-list {
max-height: 240px;
overflow-y: auto;
padding: 4px 0;
}
.wysiwyg-editor h1 { font-size: 2em; margin: 0.5em 0 0.3em; }
.wysiwyg-editor h2 { font-size: 1.6em; margin: 0.5em 0 0.3em; }
.wysiwyg-editor h3 { font-size: 1.3em; margin: 0.4em 0 0.25em; }
.wysiwyg-editor p { margin: 0 0 0.5em; }
.wysiwyg-editor pre {
.lang-picker-item {
padding: 6px 12px;
font-size: 13px;
color: #374151;
cursor: pointer;
}
.lang-picker-item:hover {
background: #f3f4f6;
padding: 12px 16px;
border-radius: 6px;
font-family: 'Menlo','Consolas',monospace;
font-size: 14px;
overflow-x: auto;
margin: 0.5em 0;
}
.wysiwyg-editor code {
background: #f3f4f6;
padding: 1px 5px;
border-radius: 3px;
font-family: 'Menlo','Consolas',monospace;
font-size: 0.9em;
}
.wysiwyg-editor blockquote {
border-left: 4px solid #6366f1;
padding: 2px 14px;
color: #6b7280;
margin: 0.5em 0;
}
.wysiwyg-editor ul, .wysiwyg-editor ol {
padding-left: 24px;
margin: 0.4em 0;
}
.wysiwyg-editor li {
margin-bottom: 2px;
}
.wysiwyg-editor img {
max-width: 100%;
border-radius: 6px;
}
.wysiwyg-editor a {
.lang-picker-item.selected {
background: #e0e7ff;
color: #6366f1;
}
.wysiwyg-editor hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 1em 0;
}
.wysiwyg-editor table {
border-collapse: collapse;
width: 100%;
margin: 0.5em 0;
}
.wysiwyg-editor th, .wysiwyg-editor td {
border: 1px solid #d1d5db;
padding: 6px 12px;
text-align: left;
/* ---- 编辑器主体(编辑区 + 预览区) ---- */
.editor-main {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
/* ========== 分屏预览 ========== */
.editor-body.split-mode .form-textarea {
width: 50%;
float: left;
border-radius: 8px 0 0 8px;
border-right: none;
.editor-pane {
flex: 1;
overflow: hidden;
min-width: 0;
}
.split-preview {
.editor-pane .cm-editor {
height: 100%;
}
.editor-pane .cm-editor .cm-scroller {
font-family: 'Menlo', 'Consolas', 'Monaco', 'Liberation Mono', monospace;
font-size: 14px;
line-height: 1.65;
}
.editor-pane .cm-editor .cm-content {
padding: 16px 20px;
}
.editor-pane .cm-editor .cm-gutters {
border-right: 1px solid #e5e7eb;
background: #fafafa;
color: #9ca3af;
}
/* 预览区 */
.preview-pane {
width: 0;
overflow: hidden;
border-left: 1px solid #e5e7eb;
background: #fff;
transition: width 0.2s ease;
}
.preview-pane.visible {
width: 50%;
float: right;
padding: 0;
box-sizing: border-box;
min-width: 320px;
}
.preview-content {
height: 100%;
overflow-y: auto;
padding: 20px 28px;
}
.split-preview .split-preview-content {
padding: 14px 18px;
background: #f9fafb;
border: 1px solid #d1d5db;
border-radius: 0 8px 8px 0;
min-height: 400px;
font-size: 15px;
line-height: 1.8;
/* ---- 状态栏 ---- */
.editor-statusbar {
display: flex;
align-items: center;
gap: 16px;
padding: 6px 20px;
background: #fafafa;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #9ca3af;
flex-shrink: 0;
min-height: 34px;
}
.split-preview .split-preview-content h1,
.split-preview .split-preview-content h2,
.split-preview .split-preview-content h3 {
margin-top: 18px;
margin-bottom: 8px;
.status-item {
white-space: nowrap;
}
.split-preview .split-preview-content pre {
background: #e5e7eb;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
.status-spacer {
flex: 1;
}
.status-draft {
color: #059669;
font-weight: 500;
}
.btn-submit {
padding: 5px 18px;
font-size: 13px;
border-radius: 6px;
}
.split-preview .split-preview-content blockquote {
border-left: 4px solid #6366f1;
padding-left: 14px;
color: #6b7280;
/* ---- 全屏模式 ---- */
.editor-layout.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
height: 100vh;
background: #fff;
}
/* ========== 404 ========== */

File diff suppressed because it is too large Load Diff