增加markdown支持。

This commit is contained in:
2026-07-28 15:51:54 +08:00
parent 98161a2fe8
commit 21be62fa7c
15 changed files with 353 additions and 6 deletions
+72
View File
@@ -212,6 +212,7 @@ async function showPostForm(isEdit = false) {
document.getElementById('post-form').reset();
document.getElementById('post-id').value = '';
setCover('');
mdShowEdit();
// 加载分类选项
const res = await fetch(`${API_BASE}/categories`);
@@ -247,6 +248,7 @@ async function editPost(id) {
).join('') || '';
document.getElementById('post-form-title').textContent = '编辑文章';
mdShowEdit();
showPage('post-form');
} catch (err) {
alert('加载文章失败');
@@ -295,6 +297,76 @@ async function handlePostSubmit(e) {
}
}
// ============ Markdown 编辑器 ============
// 在选区两侧插入包裹符号(加粗、斜体、链接等)
function mdWrap(prefix, suffix, placeholder) {
mdShowEdit();
const ta = document.getElementById('post-content');
const start = ta.selectionStart, end = ta.selectionEnd;
const selected = ta.value.substring(start, end) || placeholder;
ta.value = ta.value.substring(0, start) + prefix + selected + suffix + ta.value.substring(end);
ta.focus();
ta.selectionStart = start + prefix.length;
ta.selectionEnd = start + prefix.length + selected.length;
}
// 在行首插入前缀(标题、引用、列表等)
function mdInsertLine(prefix, placeholder) {
mdShowEdit();
const ta = document.getElementById('post-content');
const start = ta.selectionStart, end = ta.selectionEnd;
const selected = ta.value.substring(start, end) || placeholder;
// 不在行首时先换行,保证前缀生效
const needNewline = start > 0 && ta.value[start - 1] !== '\n';
const insert = (needNewline ? '\n' : '') + prefix + selected;
ta.value = ta.value.substring(0, start) + insert + ta.value.substring(end);
ta.focus();
ta.selectionStart = start + insert.length - selected.length;
ta.selectionEnd = start + insert.length;
}
// 插入表格模板
function mdInsertTable() {
mdShowEdit();
const ta = document.getElementById('post-content');
const table = '\n| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| 内容 | 内容 | 内容 |\n';
const start = ta.selectionStart;
ta.value = ta.value.substring(0, start) + table + ta.value.substring(ta.selectionEnd);
ta.focus();
ta.selectionStart = ta.selectionEnd = start + table.length;
}
// 切换到编辑模式
function mdShowEdit() {
document.getElementById('post-content').classList.remove('hidden');
document.getElementById('md-preview').classList.add('hidden');
document.getElementById('md-tab-edit').classList.add('active');
document.getElementById('md-tab-preview').classList.remove('active');
}
// 切换到预览模式(调用后端渲染,与前台显示效果一致)
async function mdShowPreview() {
const ta = document.getElementById('post-content');
const preview = document.getElementById('md-preview');
document.getElementById('md-tab-edit').classList.remove('active');
document.getElementById('md-tab-preview').classList.add('active');
ta.classList.add('hidden');
preview.classList.remove('hidden');
preview.innerHTML = '<p class="md-preview-tip">渲染中...</p>';
try {
const res = await fetch(`${API_BASE}/admin/markdown/preview`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ content: ta.value })
});
const data = await res.json();
preview.innerHTML = data.html || '<p class="md-preview-tip">暂无内容</p>';
} catch (err) {
preview.innerHTML = '<p class="md-preview-tip">预览失败,请检查网络</p>';
}
}
// 更改文章状态
async function changePostStatus(id, status) {
const actionText = status === 'published' ? '发布' : '下架';
+26 -3
View File
@@ -141,8 +141,31 @@
<textarea id="post-summary" rows="3"></textarea>
</div>
<div class="form-group">
<label>内容 (支持 HTML)</label>
<textarea id="post-content" rows="15" required></textarea>
<label>内容 (支持 Markdown 语法)</label>
<div class="md-editor">
<div class="md-toolbar">
<div class="md-tools" id="md-tools">
<button type="button" title="加粗" onclick="mdWrap('**','**','粗体文字')"><b>B</b></button>
<button type="button" title="斜体" onclick="mdWrap('*','*','斜体文字')"><i>I</i></button>
<button type="button" title="删除线" onclick="mdWrap('~~','~~','删除线文字')"><s>S</s></button>
<button type="button" title="标题" onclick="mdInsertLine('## ','标题')">H</button>
<button type="button" title="链接" onclick="mdWrap('[','](https://)','链接文字')">🔗</button>
<button type="button" title="图片" onclick="mdWrap('![','](https://)','图片描述')">🖼</button>
<button type="button" title="行内代码" onclick="mdWrap('`','`','代码')">&lt;/&gt;</button>
<button type="button" title="代码块" onclick="mdWrap('\n```\n','\n```\n','代码块')">{ }</button>
<button type="button" title="引用" onclick="mdInsertLine('> ','引用内容')">”</button>
<button type="button" title="无序列表" onclick="mdInsertLine('- ','列表项')">•</button>
<button type="button" title="有序列表" onclick="mdInsertLine('1. ','列表项')">1.</button>
<button type="button" title="表格" onclick="mdInsertTable()">⊞</button>
</div>
<div class="md-tabs">
<button type="button" id="md-tab-edit" class="active" onclick="mdShowEdit()">编辑</button>
<button type="button" id="md-tab-preview" onclick="mdShowPreview()">预览</button>
</div>
</div>
<textarea id="post-content" rows="15" required placeholder="支持 Markdown 语法,也兼容 HTML 内容"></textarea>
<div id="md-preview" class="md-preview hidden"></div>
</div>
</div>
<div class="form-group">
<label>
@@ -348,6 +371,6 @@
</div>
</div>
<script src="/static/admin/app.js?v=10"></script>
<script src="/static/admin/app.js?v=11"></script>
</body>
</html>
+170
View File
@@ -770,3 +770,173 @@ body {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
/* Markdown 编辑器 */
.md-editor {
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
}
.md-editor:focus-within {
border-color: #1890ff;
}
.md-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
padding: 6px 8px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.md-tools {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.md-tools button {
min-width: 30px;
height: 28px;
padding: 0 8px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: #555;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.md-tools button:hover {
background: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
}
.md-tabs {
display: flex;
gap: 4px;
}
.md-tabs button {
height: 28px;
padding: 0 14px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
color: #555;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.md-tabs button.active {
background: #1890ff;
border-color: #1890ff;
color: #fff;
}
.md-editor textarea {
display: block;
border: none !important;
border-radius: 0 !important;
resize: vertical;
font-family: Consolas, Monaco, 'Courier New', monospace;
line-height: 1.6;
}
.md-editor textarea:focus {
outline: none;
}
.md-preview {
min-height: 320px;
max-height: 600px;
overflow-y: auto;
padding: 16px;
background: #fff;
font-size: 14px;
line-height: 1.8;
color: #333;
}
.md-preview-tip {
color: #999;
text-align: center;
padding: 40px 0;
}
/* 预览区内容排版 */
.md-preview h1,
.md-preview h2,
.md-preview h3,
.md-preview h4 {
margin: 16px 0 8px;
font-weight: 600;
}
.md-preview p {
margin: 8px 0;
}
.md-preview blockquote {
margin: 12px 0;
padding: 8px 16px;
border-left: 4px solid #1890ff;
background: #f5f9ff;
color: #666;
}
.md-preview pre {
margin: 12px 0;
padding: 12px 16px;
background: #f6f8fa;
border-radius: 6px;
overflow-x: auto;
}
.md-preview code {
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 13px;
background: #f0f0f0;
padding: 2px 5px;
border-radius: 3px;
}
.md-preview pre code {
background: none;
padding: 0;
}
.md-preview table {
border-collapse: collapse;
margin: 12px 0;
width: 100%;
}
.md-preview th,
.md-preview td {
border: 1px solid #d9d9d9;
padding: 8px 12px;
text-align: left;
}
.md-preview th {
background: #fafafa;
}
.md-preview img {
max-width: 100%;
}
.md-preview ul,
.md-preview ol {
margin: 8px 0;
padding-left: 24px;
}