diff --git a/templates/MetaLab-2026/html/posts/new.html b/templates/MetaLab-2026/html/posts/new.html index a0002a9..0e0d93d 100644 --- a/templates/MetaLab-2026/html/posts/new.html +++ b/templates/MetaLab-2026/html/posts/new.html @@ -8,6 +8,7 @@ flex-direction: column; overflow: hidden; min-height: 0; + height: 100%; } #toolbar-container { flex-shrink: 0; @@ -16,6 +17,13 @@ flex: 1; overflow-y: auto; min-height: 0; + height: 100%; +} +.editor-pane .w-e-text-container { + height: 100% !important; +} +.editor-pane .w-e-text-container [data-slate-editor] { + min-height: 100%; }
diff --git a/templates/MetaLab-2026/static/css/posts.css b/templates/MetaLab-2026/static/css/posts.css index c41dc11..43f8c7e 100644 --- a/templates/MetaLab-2026/static/css/posts.css +++ b/templates/MetaLab-2026/static/css/posts.css @@ -104,15 +104,63 @@ margin-bottom: 12px; } +.post-detail-body a { + color: var(--color-accent); + text-decoration: underline; + text-underline-offset: 2px; + transition: color 0.2s; +} + +.post-detail-body a:hover { + color: #2980b9; +} + .post-detail-body pre { + position: relative; background: #f3f4f6; padding: 16px; + padding-top: 36px; border-radius: 8px; overflow-x: auto; font-size: 14px; line-height: 1.5; } +/* 代码块语言标签(编辑器内 + 预览页通用) */ +.post-detail-body pre, +#editor-container [data-slate-editor] pre { + position: relative; + background: #f3f4f6; + padding: 16px; + padding-top: 36px; + border-radius: 8px; + overflow-x: auto; + font-size: 14px; + line-height: 1.5; +} + +/* 通过 JS 动态注入语言标签,因为 CSS attr() 无法从 class 中提取 */ +.code-lang-label { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 4px 16px; + background: #e5e7eb; + color: #6b7280; + font-size: 11px; + font-weight: 600; + border-radius: 8px 8px 0 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; +} + +.post-detail-body pre code, +#editor-container [data-slate-editor] pre code { + background: transparent !important; + padding: 0 !important; + font-size: inherit !important; +} + .post-detail-body code { background: #f3f4f6; padding: 2px 6px; @@ -223,6 +271,12 @@ /* 编辑器整体容器 */ #editor-container { background: #fff; + height: 100%; +} + +/* wangEditor 内部文本容器撑满 */ +#editor-container .w-e-text-container { + height: 100% !important; } /* 编辑区域 */ @@ -379,6 +433,42 @@ font-style: normal !important; } +/* 编辑器内代码块样式 */ +#editor-container [data-slate-editor] pre { + position: relative; + background: #f3f4f6 !important; + padding: 12px 16px !important; + padding-top: 32px !important; + border-radius: 8px !important; + font-family: 'Menlo', 'Consolas', 'Monaco', 'Liberation Mono', monospace !important; + font-size: 13px !important; + line-height: 1.6 !important; + overflow-x: auto !important; + margin: 12px 0 !important; +} + +/* 编辑器内代码块语言标签 */ +#editor-container [data-slate-editor] pre::before { + content: attr(data-language); + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 3px 16px; + background: #e5e7eb; + color: #6b7280; + font-size: 11px; + font-weight: 600; + border-radius: 8px 8px 0 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; +} + +#editor-container [data-slate-editor] pre code { + background: transparent !important; + padding: 0 !important; + font-size: inherit !important; +} + /* 预览区 */ .preview-pane { width: 0; diff --git a/templates/MetaLab-2026/static/js/editor.js b/templates/MetaLab-2026/static/js/editor.js index f6cd942..8f723fa 100644 --- a/templates/MetaLab-2026/static/js/editor.js +++ b/templates/MetaLab-2026/static/js/editor.js @@ -31,12 +31,17 @@ let previewVisible = false; let syncTimer = null; let saveTimer = null; let splitTimer = null; +let draftCharCount = 0; let editor = null; /* ================================================================ CSRF Token ================================================================ */ function getCSRFToken() { + // 优先从 cookie 读取(与 CSRF Double Submit Cookie 一致) + const match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/); + if (match) return match[1]; + // fallback: 从 meta 标签读取 const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.getAttribute('content') : ''; } @@ -56,35 +61,58 @@ function initEditor() { const editorConfig = { placeholder: '在此输入正文...', onChange() { + // 同步更新代码块语言标签(立即生效) + updateCodeLabels(); + clearTimeout(syncTimer); syncTimer = setTimeout(() => { updateStats(); if (previewVisible) updatePreview(); }, 200); + // 字数触发:每新增 300 字符保存 + // 时间触发:停止输入 10 秒后自动保存(防止末尾内容丢失) + const currentLen = (editor ? editor.getText().length : 0); + if (currentLen - draftCharCount >= 300) { + draftCharCount = currentLen; + saveDraft(); + } clearTimeout(saveTimer); - saveTimer = setTimeout(saveDraft, 30000); + saveTimer = setTimeout(saveDraft, 10000); }, MENU_CONF: {}, }; // 配置上传图片 editorConfig.MENU_CONF['uploadImage'] = { - server: '/api/posts/upload-image', - fieldName: 'file', - maxFileSize: 10 * 1024 * 1024, - maxNumberOfFiles: 10, + maxFileSize: 5 * 1024 * 1024, allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], - headers: { - 'X-CSRF-Token': getCSRFToken(), - }, - customInsert(res, insertFn) { - if (res.success && res.data && res.data.url) { - insertFn(res.data.url, res.data.url, res.data.url); - } - }, - onFailed(xhr) { - console.error('图片上传失败', xhr); + // 自定义上传逻辑,适配后端 API + customUpload(file, insertFn) { + const fd = new FormData(); + fd.append('file', file); + + const csrf = getCSRFToken(); + + fetch('/api/posts/upload-image', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'X-CSRF-Token': csrf, + }, + body: fd, + }) + .then(r => r.json()) + .then(res => { + if (res.success && res.data && res.data.url) { + insertFn(res.data.url, res.data.url, res.data.url); + } else { + alert(res.message || '上传失败'); + } + }) + .catch(() => { + alert('上传请求失败,请检查网络'); + }); }, }; @@ -109,12 +137,11 @@ function initEditor() { // 初始化 updateStats(); + updateCodeLabels(); loadDraft(); - saveTimer = setTimeout(saveDraft, 30000); titleEl.addEventListener('input', () => { - clearTimeout(saveTimer); - saveTimer = setTimeout(saveDraft, 30000); + saveDraft(); }); // 滚动同步 @@ -134,6 +161,48 @@ function getEditorText() { return editor ? editor.getText() : ''; } +function getEditorHtml() { + return editor ? editor.getHtml() : ''; +} + +/* ================================================================ + 代码块语言标签 — 从 提取并注入标签
+ ================================================================ */
+function updateCodeLabels() {
+ if (!editor) return;
+
+ // wangEditor 编辑区 不带 class="language-xxx",
+ // 但 getHtml() 输出带 class="language-xxx"。
+ // 用 DOMParser 解析 HTML 获取语言,再注入到编辑区对应 pre。
+ try {
+ const html = editor.getHtml();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const htmlPres = doc.querySelectorAll('pre');
+
+ const editorEl = document.getElementById('editor-container');
+ if (!editorEl) return;
+ const domPres = editorEl.querySelectorAll('pre');
+
+ htmlPres.forEach((htmlPre, idx) => {
+ const domPre = domPres[idx];
+ if (!domPre || domPre.querySelector('.code-lang-label')) return;
+
+ const code = htmlPre.querySelector('code[class*="language-"]');
+ if (code) {
+ const match = code.className.match(/language-(\w+)/);
+ if (match && match[1]) {
+ const label = document.createElement('span');
+ label.className = 'code-lang-label';
+ label.textContent = match[1];
+ domPre.style.position = 'relative';
+ domPre.insertBefore(label, domPre.firstChild);
+ }
+ }
+ });
+ } catch (_) { /* 忽略 */ }
+}
+
function updateStats() {
const text = getEditorText();
const chars = text.length;
@@ -147,29 +216,32 @@ function updateStats() {
}
/* ================================================================
- Markdown 预览(服务端渲染)
+ 预览(wangEditor 直接展示 HTML,无需服务端渲染)
================================================================ */
-function renderMarkdown(md, callback) {
- fetch('/api/posts/preview', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() },
- body: JSON.stringify({ body: md })
- })
- .then(r => r.json())
- .then(d => callback(d.success ? d.data.html : md))
- .catch(() => callback(md));
-}
-
function updatePreview() {
if (!previewVisible) return;
- const md = getEditorText();
- if (!md.trim()) {
+ const html = editor ? editor.getHtml() : '';
+ if (html === '
' || !html.trim()) {
previewCont.innerHTML = '预览将在此处显示...';
return;
}
clearTimeout(splitTimer);
splitTimer = setTimeout(() => {
- renderMarkdown(md, html => { previewCont.innerHTML = html; });
+ previewCont.innerHTML = html;
+ // 预览区也注入语言标签
+ previewCont.querySelectorAll('pre').forEach(pre => {
+ if (pre.querySelector('.code-lang-label')) return;
+ const code = pre.querySelector('code[class*="language-"]');
+ if (code) {
+ const match = code.className.match(/language-(\w+)/);
+ if (match) {
+ const label = document.createElement('span');
+ label.className = 'code-lang-label';
+ label.textContent = match[1];
+ pre.insertBefore(label, pre.firstChild);
+ }
+ }
+ });
}, 300);
}
@@ -207,27 +279,17 @@ function syncScrollToEditor() {
================================================================ */
function saveDraft() {
const title = titleEl.value.trim();
- const body = getEditorText();
+ const bodyHtml = getEditorHtml();
+ const bodyText = getEditorText().trim();
- if (!body && !title) return;
+ // 空编辑区不保存(wangEditor 空内容 HTML 是
)
+ if (!bodyText && !title) return;
- const draft = { title, body, updatedAt: Date.now() };
+ const draft = { title, body: bodyHtml, updatedAt: Date.now() };
localStorage.setItem('mlb_draft_' + (isEdit ? postIdEl.value : 'new'), JSON.stringify(draft));
draftStatus.style.display = '';
draftStatus.textContent = '草稿已保存 ' + new Date().toLocaleTimeString();
-
- if (isEdit && postIdEl.value) {
- silentSave(title, body);
- }
-}
-
-function silentSave(title, body) {
- fetch('/api/posts/' + postIdEl.value, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCSRFToken() },
- body: JSON.stringify({ title, body })
- }).catch(() => {});
}
function loadDraft() {
@@ -237,11 +299,15 @@ function loadDraft() {
try {
const draft = JSON.parse(raw);
if (draft.body || draft.title) {
- if (!isEdit || !getEditorText()) {
- if (draft.body && confirm('检测到未保存的草稿(' + new Date(draft.updatedAt).toLocaleString() + '),是否恢复?')) {
- if (editor) editor.setHtml(draft.body);
- if (draft.title && !titleEl.value) titleEl.value = draft.title;
- }
+ // 如果编辑器已有内容(编辑模式从 DB 加载),跳过草稿恢复
+ if (isEdit && getEditorText().trim()) return;
+
+ if (draft.body && confirm('检测到未保存的草稿(' + new Date(draft.updatedAt).toLocaleString() + '),是否恢复?')) {
+ if (editor) editor.setHtml(draft.body);
+ if (draft.title && !titleEl.value) titleEl.value = draft.title;
+ } else {
+ // 用户拒绝恢复,清除旧草稿
+ clearDraft();
}
}
} catch (_) { localStorage.removeItem(key); }