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

@ -123,7 +123,6 @@ func (ctrl *PostController) NewPage(c *gin.Context) {
c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{ c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{
"Title": "撰写帖子", "Title": "撰写帖子",
"ExtraCSS": "/static/css/posts.css", "ExtraCSS": "/static/css/posts.css",
"ExtraJS": "/static/js/editor.js",
})) }))
} }
@ -149,7 +148,6 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
"Title": "编辑帖子", "Title": "编辑帖子",
"Post": post, "Post": post,
"ExtraCSS": "/static/css/posts.css", "ExtraCSS": "/static/css/posts.css",
"ExtraJS": "/static/js/editor.js",
})) }))
} }

View File

@ -3,78 +3,118 @@
{{template "layout/nav.html" .}} {{template "layout/nav.html" .}}
<div class="container editor-container"> <div class="editor-layout">
<h1 class="editor-title">{{if .Post}}编辑帖子{{else}}撰写帖子{{end}}</h1> <form id="postForm" class="editor-form">
<form id="postForm" class="post-form">
<input type="hidden" id="postId" value="{{if .Post}}{{.Post.ID}}{{end}}"> <input type="hidden" id="postId" value="{{if .Post}}{{.Post.ID}}{{end}}">
<div class="form-group"> {{/* 标题区域 */}}
<label for="postTitle">标题</label> <div class="editor-header">
<input type="text" id="postTitle" class="form-input" value="{{if .Post}}{{.Post.Title}}{{end}}" maxlength="200" required> <input type="text" id="postTitle" class="editor-title-input"
value="{{if .Post}}{{.Post.Title}}{{end}}"
placeholder="输入文章标题..."
maxlength="200" required autofocus>
</div> </div>
{{/* 模式切换标签 */}} {{/* 工具栏 */}}
<div class="editor-modes">
<button type="button" class="mode-tab active" data-mode="wysiwyg">所见即所得</button>
<button type="button" class="mode-tab" data-mode="source">源码</button>
<button type="button" class="mode-tab" data-mode="split">分屏预览</button>
</div>
{{/* 工具条 */}}
<div class="editor-toolbar" id="toolbar"> <div class="editor-toolbar" id="toolbar">
<div class="tb-group">
<button type="button" class="tb-btn" data-action="bold" title="加粗 (Ctrl+B)"> <button type="button" class="tb-btn" data-action="bold" title="加粗 (Ctrl+B)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="italic" title="斜体 (Ctrl+I)"> <button type="button" class="tb-btn" data-action="italic" title="斜体 (Ctrl+I)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="strikethrough" title="删除线">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.85 7.08C6.85 4.37 9.45 3 12.24 3c1.64 0 3 .49 3.9 1.28.77.65 1.46 1.73 1.46 3.24h-3.01c0-.31-.05-.59-.15-.85-.29-.86-1.2-1.28-2.25-1.28-1.86 0-2.34 1.02-2.34 1.7 0 .48.25.88.74 1.21.38.25.77.48 1.41.7H7.39c-.21-.34-.54-.89-.54-1.92zM21 12v-2H3v2h9.62c1.15.45 1.96.75 1.96 1.97 0 1-.81 1.67-2.28 1.67-1.54 0-2.93-.54-2.93-2.51H6.4c0 2.21 1.79 4.21 4.4 4.21 3.61 0 5.2-2.48 5.2-4.3 0-1.43-.84-2.46-2.37-3.04H21z"/></svg>
</button>
<button type="button" class="tb-btn" data-action="code" title="行内代码 (Ctrl+`)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
</button>
</div>
<span class="tb-sep"></span> <span class="tb-sep"></span>
<button type="button" class="tb-btn" data-action="h2" title="标题">H2</button> <div class="tb-group">
<button type="button" class="tb-btn" data-action="h3" title="标题">H3</button> <button type="button" class="tb-btn" data-action="h1" title="一级标题">H1</button>
<button type="button" class="tb-btn" data-action="h2" title="二级标题">H2</button>
<button type="button" class="tb-btn" data-action="h3" title="三级标题">H3</button>
<button type="button" class="tb-btn" data-action="h4" title="四级标题">H4</button>
</div>
<span class="tb-sep"></span> <span class="tb-sep"></span>
<button type="button" class="tb-btn" data-action="link" title="链接"> <div class="tb-group">
<button type="button" class="tb-btn" data-action="link" title="插入链接 (Ctrl+K)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="image" title="图片"> <button type="button" class="tb-btn" data-action="image" title="插入图片 (Ctrl+Shift+I)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
</button> </button>
</div>
<span class="tb-sep"></span> <span class="tb-sep"></span>
<button type="button" class="tb-btn" data-action="quote" title="引用"> <div class="tb-group">
<button type="button" class="tb-btn" data-action="quote" title="引用块">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="code" title="行内代码">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
</button>
<button type="button" class="tb-btn" data-action="pre" title="代码块"> <button type="button" class="tb-btn" data-action="pre" title="代码块">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm4 2h6v2H9V7zm0 4h6v2H9v-2z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm4 2h6v2H9V7zm0 4h6v2H9v-2z"/></svg>
</button> </button>
</div>
<span class="tb-sep"></span> <span class="tb-sep"></span>
<div class="tb-group">
<button type="button" class="tb-btn" data-action="ul" title="无序列表"> <button type="button" class="tb-btn" data-action="ul" title="无序列表">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="ol" title="有序列表"> <button type="button" class="tb-btn" data-action="ol" title="有序列表">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>
</button> </button>
<button type="button" class="tb-btn" data-action="hr" title="分割线"></button> <button type="button" class="tb-btn" data-action="task" title="任务列表">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
</button>
<button type="button" class="tb-btn" data-action="table" title="插入表格">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 4v3h4V8H4zm6 0v3h4V8h-4zm6 0v3h4V8h-4zm-12 5v3h4v-3H4zm6 0v3h4v-3h-4zm6 0v3h4v-3h-4zM4 6h16V6H4v.01z"/></svg>
</button>
</div> </div>
<span class="tb-sep"></span>
<button type="button" class="tb-btn" data-action="hr" title="分割线">&mdash;</button>
{{/* 源码编辑器 */}} <div class="tb-spacer"></div>
<div class="editor-body">
<textarea id="postBody" class="form-textarea" rows="20" required
placeholder="使用 Markdown 编写正文...">{{if .Post}}{{.Post.Body}}{{end}}</textarea>
{{/* WYSIWYG 编辑器 */}} {{/* 右侧:预览切换 + 全屏 */}}
<div id="wysiwygEditor" class="wysiwyg-editor" contenteditable="true" style="display:none;"></div> <div class="tb-group tb-right">
<button type="button" class="tb-btn tb-toggle" id="btnTogglePreview" title="切换预览 (Ctrl+Shift+P)">
{{/* 分屏预览 */}} <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
<div id="splitPreview" class="split-preview" style="display:none;"> </button>
<div class="split-preview-content post-detail-body"></div> <button type="button" class="tb-btn" id="btnFullscreen" title="全屏编辑 (F11)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
</button>
</div> </div>
</div> </div>
<div class="editor-actions"> {{/* 代码块语言选择器(下拉菜单) */}}
<button type="submit" class="btn btn-primary">{{if .Post}}保存修改{{else}}发布帖子{{end}}</button> <div class="lang-picker" id="langPicker" style="display:none">
<div class="lang-picker-search">
<input type="text" id="langSearch" placeholder="搜索语言...">
</div>
<div class="lang-picker-list" id="langList"></div>
</div>
{{/* 编辑器主体:编辑区 + 预览区 */}}
<div class="editor-main">
<div class="editor-pane" id="editorPane">
<textarea id="postBody" style="display:none;">{{if .Post}}{{.Post.Body}}{{end}}</textarea>
</div>
<div class="preview-pane" id="previewPane">
<div class="preview-content post-detail-body" id="previewContent">
<em style="color:#9ca3af">预览将在此处显示...</em>
</div>
</div>
</div>
{{/* 状态栏 */}}
<div class="editor-statusbar">
<span class="status-item" id="wordCount">0 字</span>
<span class="status-item" id="lineCount">0 行</span>
<span class="status-item" id="readTime">约 1 分钟</span>
<span class="status-spacer"></span>
<span class="status-item status-draft" id="draftStatus" style="display:none">草稿已保存</span>
<button type="submit" class="btn btn-primary btn-submit">{{if .Post}}保存修改{{else}}发布帖子{{end}}</button>
</div> </div>
</form> </form>
@ -84,7 +124,28 @@
{{template "layout/footer.html" .}} {{template "layout/footer.html" .}}
{{if .ExtraJS}}<script src="{{.ExtraJS}}"></script>{{end}} {{/* CodeMirror 6 import map (MIT License, from jsdelivr CDN) */}}
<script type="importmap">
{
"imports": {
"@codemirror/state": "https://cdn.jsdelivr.net/npm/@codemirror/state@6.5.2/dist/index.js",
"@codemirror/view": "https://cdn.jsdelivr.net/npm/@codemirror/view@6.36.4/dist/index.js",
"@codemirror/commands": "https://cdn.jsdelivr.net/npm/@codemirror/commands@6.8.1/dist/index.js",
"@codemirror/language": "https://cdn.jsdelivr.net/npm/@codemirror/language@6.11.0/dist/index.js",
"@codemirror/lang-markdown": "https://cdn.jsdelivr.net/npm/@codemirror/lang-markdown@6.3.2/dist/index.js",
"@codemirror/language-data": "https://cdn.jsdelivr.net/npm/@codemirror/language-data@6.5.1/dist/index.js",
"@codemirror/autocomplete": "https://cdn.jsdelivr.net/npm/@codemirror/autocomplete@6.18.6/dist/index.js",
"@codemirror/search": "https://cdn.jsdelivr.net/npm/@codemirror/search@6.5.10/dist/index.js",
"@lezer/common": "https://cdn.jsdelivr.net/npm/@lezer/common@1.2.3/dist/index.js",
"@lezer/highlight": "https://cdn.jsdelivr.net/npm/@lezer/highlight@1.2.1/dist/index.js",
"@lezer/lr": "https://cdn.jsdelivr.net/npm/@lezer/lr@1.4.2/dist/index.js",
"@lezer/markdown": "https://cdn.jsdelivr.net/npm/@lezer/markdown@1.4.0/dist/index.js",
"crelt": "https://cdn.jsdelivr.net/npm/crelt@1.0.6/index.js",
"style-mod": "https://cdn.jsdelivr.net/npm/style-mod@4.1.2/src/style-mod.js",
"w3c-keyname": "https://cdn.jsdelivr.net/npm/w3c-keyname@2.2.8/index.js"
}
}
</script>
<script src="/static/js/common.js"></script> <script src="/static/js/common.js"></script>
</body> <script type="module" src="/static/js/editor.js"></script>
</html> {{end}}

View File

@ -162,263 +162,259 @@
gap: 12px; gap: 12px;
} }
/* ========== 编辑器 ========== */ /* ========== 编辑器布局(全宽) ========== */
.editor-container { .editor-layout {
max-width: 1024px; height: calc(100vh - 56px);
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 {
display: flex; display: flex;
gap: 4px; flex-direction: column;
margin-bottom: 0; overflow: hidden;
} }
.mode-tab { .editor-form {
padding: 8px 18px; display: flex;
border: 1px solid #d1d5db; flex-direction: column;
border-bottom: none; height: 100%;
border-radius: 8px 8px 0 0;
background: #f9fafb;
color: #6b7280;
font-size: 13px;
font-weight: 500;
cursor: pointer;
position: relative;
z-index: 1;
} }
.mode-tab.active { /* ---- 标题区域 ---- */
.editor-header {
padding: 16px 24px 12px;
border-bottom: 1px solid #e5e7eb;
background: #fff; background: #fff;
color: #6366f1; flex-shrink: 0;
border-color: #d1d5db;
z-index: 2;
margin-bottom: -1px;
padding-bottom: 9px;
} }
/* ========== 工具条 ========== */ .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 { .editor-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
padding: 6px 8px; padding: 5px 16px;
background: #fff; background: #fafafa;
border: 1px solid #d1d5db; border-bottom: 1px solid #e5e7eb;
border-top: none; min-height: 38px;
border-radius: 0 0 8px 8px; flex-shrink: 0;
min-height: 40px; overflow-x: auto;
flex-wrap: wrap;
} }
/* 编辑区上边距 */ .tb-group {
.editor-toolbar + .editor-body, display: flex;
.editor-modes + .editor-body { align-items: center;
margin-top: 0; gap: 1px;
} }
.tb-btn { .tb-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; width: 30px;
height: 30px; height: 28px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
background: transparent; background: transparent;
color: #4b5563; color: #4b5563;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 12px;
font-weight: 600; font-weight: 600;
transition: background 0.15s; transition: background 0.12s, color 0.12s;
flex-shrink: 0;
} }
.tb-btn:hover { .tb-btn:hover {
background: #f3f4f6; background: #e5e7eb;
color: #111; color: #111;
} }
.tb-btn:active {
background: #d1d5db;
}
.tb-btn svg { .tb-btn svg {
width: 18px; width: 16px;
height: 18px; height: 16px;
} }
.tb-sep { .tb-sep {
width: 1px; width: 1px;
height: 20px; height: 20px;
background: #e5e7eb; background: #e5e7eb;
margin: 0 4px; margin: 0 6px;
flex-shrink: 0;
} }
/* ========== 编辑区 ========== */ .tb-spacer {
.editor-body { flex: 1;
position: relative; }
clear: both;
.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; overflow: hidden;
} }
.form-textarea { .lang-picker-search input {
width: 100%; width: 100%;
padding: 12px 14px; padding: 10px 12px;
border: 1px solid #d1d5db; border: none;
border-radius: 8px; border-bottom: 1px solid #e5e7eb;
font-size: 15px; font-size: 13px;
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;
outline: none; outline: none;
word-wrap: break-word; box-sizing: border-box;
overflow-wrap: break-word;
} }
.wysiwyg-editor:focus { .lang-picker-list {
border-color: #6366f1; max-height: 240px;
box-shadow: 0 0 0 2px rgba(99,102,241,0.15); overflow-y: auto;
padding: 4px 0;
} }
.wysiwyg-editor h1 { font-size: 2em; margin: 0.5em 0 0.3em; } .lang-picker-item {
.wysiwyg-editor h2 { font-size: 1.6em; margin: 0.5em 0 0.3em; } padding: 6px 12px;
.wysiwyg-editor h3 { font-size: 1.3em; margin: 0.4em 0 0.25em; } font-size: 13px;
.wysiwyg-editor p { margin: 0 0 0.5em; } color: #374151;
.wysiwyg-editor pre { cursor: pointer;
}
.lang-picker-item:hover {
background: #f3f4f6; 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; .lang-picker-item.selected {
padding: 1px 5px; background: #e0e7ff;
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 {
color: #6366f1; color: #6366f1;
} }
.wysiwyg-editor hr {
border: none; /* ---- 编辑器主体(编辑区 + 预览区) ---- */
border-top: 2px solid #e5e7eb; .editor-main {
margin: 1em 0; flex: 1;
} display: flex;
.wysiwyg-editor table { overflow: hidden;
border-collapse: collapse; min-height: 0;
width: 100%;
margin: 0.5em 0;
}
.wysiwyg-editor th, .wysiwyg-editor td {
border: 1px solid #d1d5db;
padding: 6px 12px;
text-align: left;
} }
/* ========== 分屏预览 ========== */ .editor-pane {
.editor-body.split-mode .form-textarea { flex: 1;
width: 50%; overflow: hidden;
float: left; min-width: 0;
border-radius: 8px 0 0 8px;
border-right: none;
} }
.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%; width: 50%;
float: right; min-width: 320px;
padding: 0; }
box-sizing: border-box;
.preview-content {
height: 100%;
overflow-y: auto; overflow-y: auto;
padding: 20px 28px;
} }
.split-preview .split-preview-content { /* ---- 状态栏 ---- */
padding: 14px 18px; .editor-statusbar {
background: #f9fafb; display: flex;
border: 1px solid #d1d5db; align-items: center;
border-radius: 0 8px 8px 0; gap: 16px;
min-height: 400px; padding: 6px 20px;
font-size: 15px; background: #fafafa;
line-height: 1.8; border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #9ca3af;
flex-shrink: 0;
min-height: 34px;
} }
.split-preview .split-preview-content h1, .status-item {
.split-preview .split-preview-content h2, white-space: nowrap;
.split-preview .split-preview-content h3 {
margin-top: 18px;
margin-bottom: 8px;
} }
.split-preview .split-preview-content pre { .status-spacer {
background: #e5e7eb; flex: 1;
padding: 12px; }
border-radius: 6px;
overflow-x: auto; .status-draft {
color: #059669;
font-weight: 500;
}
.btn-submit {
padding: 5px 18px;
font-size: 13px; font-size: 13px;
border-radius: 6px;
} }
.split-preview .split-preview-content blockquote { /* ---- 全屏模式 ---- */
border-left: 4px solid #6366f1; .editor-layout.fullscreen {
padding-left: 14px; position: fixed;
color: #6b7280; top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
height: 100vh;
background: #fff;
} }
/* ========== 404 ========== */ /* ========== 404 ========== */

File diff suppressed because it is too large Load Diff