增加markdown支持。
This commit is contained in:
@@ -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
@@ -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('','图片描述')">🖼</button>
|
||||
<button type="button" title="行内代码" onclick="mdWrap('`','`','代码')"></></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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user