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:
@ -3,78 +3,118 @@
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="container editor-container">
|
||||
<h1 class="editor-title">{{if .Post}}编辑帖子{{else}}撰写帖子{{end}}</h1>
|
||||
|
||||
<form id="postForm" class="post-form">
|
||||
<div class="editor-layout">
|
||||
<form id="postForm" class="editor-form">
|
||||
<input type="hidden" id="postId" value="{{if .Post}}{{.Post.ID}}{{end}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="postTitle">标题</label>
|
||||
<input type="text" id="postTitle" class="form-input" value="{{if .Post}}{{.Post.Title}}{{end}}" maxlength="200" required>
|
||||
{{/* 标题区域 */}}
|
||||
<div class="editor-header">
|
||||
<input type="text" id="postTitle" class="editor-title-input"
|
||||
value="{{if .Post}}{{.Post.Title}}{{end}}"
|
||||
placeholder="输入文章标题..."
|
||||
maxlength="200" required autofocus>
|
||||
</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">
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
<div class="tb-group">
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</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>
|
||||
<button type="button" class="tb-btn" data-action="h2" title="标题">H2</button>
|
||||
<button type="button" class="tb-btn" data-action="h3" title="子标题">H3</button>
|
||||
<div class="tb-group">
|
||||
<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>
|
||||
<button type="button" class="tb-btn" data-action="link" title="链接">
|
||||
<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 type="button" class="tb-btn" data-action="image" title="图片">
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tb-sep"></span>
|
||||
<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>
|
||||
</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="代码块">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm4 2h6v2H9V7zm0 4h6v2H9v-2z"/></svg>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tb-sep"></span>
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
<button type="button" class="tb-btn" data-action="hr" title="分割线">—</button>
|
||||
</div>
|
||||
<div class="tb-group">
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
</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>
|
||||
<span class="tb-sep"></span>
|
||||
<button type="button" class="tb-btn" data-action="hr" title="分割线">—</button>
|
||||
|
||||
{{/* 源码编辑器 */}}
|
||||
<div class="editor-body">
|
||||
<textarea id="postBody" class="form-textarea" rows="20" required
|
||||
placeholder="使用 Markdown 编写正文...">{{if .Post}}{{.Post.Body}}{{end}}</textarea>
|
||||
<div class="tb-spacer"></div>
|
||||
|
||||
{{/* WYSIWYG 编辑器 */}}
|
||||
<div id="wysiwygEditor" class="wysiwyg-editor" contenteditable="true" style="display:none;"></div>
|
||||
|
||||
{{/* 分屏预览 */}}
|
||||
<div id="splitPreview" class="split-preview" style="display:none;">
|
||||
<div class="split-preview-content post-detail-body"></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>
|
||||
</button>
|
||||
<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 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>
|
||||
</form>
|
||||
|
||||
@ -84,7 +124,28 @@
|
||||
|
||||
{{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>
|
||||
</body>
|
||||
</html>
|
||||
<script type="module" src="/static/js/editor.js"></script>
|
||||
{{end}}
|
||||
|
||||
@ -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
Reference in New Issue
Block a user