This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/templates/MetaLab-2026/static/js/studio-editor.js
Victor_Jay abe1388f8c fix: 统一 CSRF token 获取方式,修复 token 刷新后缓存失效
- utils.js getCSRFToken() 改为从 cookie 优先读取,meta 标签作为后备
- show.html 5处移除 csrfToken 缓存变量,改用 getCSRFToken() 动态获取
- studio/posts.html/drafts.html 移除 {{ .CSRFToken }} 模板硬编码
- studio-editor.js 移除本地 getCsrfToken(),改用共享 getCSRFToken()
- settings/index.html messages/index.html 改用 getCSRFToken()
- admin posts.js/comments.js 移除本地/死代码 CSRF 获取,统一用共享方法
2026-06-02 18:47:22 +08:00

213 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Studio Editor — 创作中心 Vditor 编辑器
* 基于 editor.js使用 /api/studio/posts 端点
* CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供
*/
let vditor = null
let draftTimer = null
let submitting = false
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('vditor')
if (!container) return
const postId = document.getElementById('postId')?.value
const isEdit = !!postId
const isLocked = document.getElementById('isLocked')?.value === '1'
let initialMD = ''
const editBodyEl = document.getElementById('editBody')
if (editBodyEl) {
initialMD = editBodyEl.value
}
vditor = new Vditor('vditor', {
mode: 'wysiwyg',
cdn: '/static/vditor',
height: '100%',
lang: 'zh_CN',
placeholder: isLocked ? '此文已锁定,不可编辑' : '在此输入正文...',
readonly: isLocked,
toolbar: isLocked ? [] : [
'headings', 'bold', 'italic', 'strike', '|',
'line', 'code', 'inline-code', 'link', 'quote', '|',
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
'upload', 'table', '|',
'undo', 'redo', '|',
'fullscreen', 'code-theme', '|',
'outline', 'preview', 'devtools',
],
customWysiwygToolbar(type, popover) {},
counter: { enable: true, type: 'text' },
hint: {
extend: [
{ key: '[zone:event:', value: '[zone:event:活动ID]' },
{ key: '[zone:game:', value: '[zone:game:游戏Slug]' },
{ key: '[zone:poll:', value: '[zone:poll:投票ID]' },
{ key: '[zone:resource:', value: '[zone:resource:资源ID]' },
],
},
outline: { enable: false, position: 'right' },
preview: {
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
hljs: {
style: 'github-dark',
enable: true,
langs: [
'javascript', 'typescript', 'python', 'java', 'go', 'rust',
'c', 'cpp', 'csharp', 'php', 'ruby', 'swift', 'kotlin',
'html', 'css', 'scss', 'xml', 'json', 'yaml', 'markdown',
'sql', 'bash', 'shell', 'powershell', 'dockerfile', 'nginx',
'diff', 'http', 'graphql', 'makefile',
],
},
markdown: { codeBlockPreview: true },
},
upload: {
url: '/api/posts/upload-image',
fieldName: 'file',
max: 5 * 1024 * 1024,
accept: 'image/jpg,image/jpeg,image/png,image/gif,image/webp',
},
value: initialMD,
after() {
if (!isEdit) {
setTimeout(() => restoreDraft(), 100)
} else if (initialMD) {
// 编辑模式:显式设置稿件内容,确保正确加载
vditor.setValue(initialMD)
}
updateWordCount()
},
input(value) {
scheduleDraft(value)
updateWordCount()
},
})
document.getElementById('postForm')?.addEventListener('submit', handleSubmit)
if (!isLocked) {
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault()
document.getElementById('postForm')?.dispatchEvent(new Event('submit'))
}
})
}
})
function updateWordCount() {
const el = document.getElementById('wordCount')
if (!el || !vditor) return
const counterEl = document.querySelector('.vditor-counter')
if (counterEl) {
el.textContent = counterEl.textContent
}
}
function scheduleDraft(md) {
clearTimeout(draftTimer)
draftTimer = setTimeout(() => saveDraft(md), 2000)
}
function saveDraft(md) {
const title = document.getElementById('postTitle')?.value || ''
if (!md.trim() && !title.trim()) return
try {
localStorage.setItem('draft_post_title', title)
localStorage.setItem('draft_post_body_md', md)
const statusEl = document.getElementById('draftStatus')
if (statusEl) {
statusEl.style.display = ''
setTimeout(() => { statusEl.style.display = 'none' }, 2000)
}
} catch (e) { /* ignore */ }
}
function restoreDraft() {
try {
const title = localStorage.getItem('draft_post_title')
const md = localStorage.getItem('draft_post_body_md')
if (title) document.getElementById('postTitle').value = title
if (md && vditor) {
vditor.setValue(md)
updateWordCount()
}
} catch (e) { /* ignore */ }
}
function clearDraft() {
try {
localStorage.removeItem('draft_post_title')
localStorage.removeItem('draft_post_body_md')
} catch (e) { /* ignore */ }
}
async function handleSubmit(e) {
e.preventDefault()
if (submitting) return
submitting = true
const submitBtn = document.querySelector('.btn-submit')
if (submitBtn) {
submitBtn.disabled = true
submitBtn.textContent = '提交中...'
}
try {
const postId = document.getElementById('postId')?.value
const title = document.getElementById('postTitle')?.value.trim()
if (!title) { alert('请输入标题'); return }
const body = vditor ? vditor.getValue() : ''
if (!body.trim()) { alert('请输入正文'); return }
const isEdit = !!postId
const url = isEdit ? '/api/studio/posts/' + postId : '/api/studio/posts'
const method = isEdit ? 'PUT' : 'POST'
const resp = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken(),
},
body: JSON.stringify({ title, body }),
})
const data = await resp.json()
if (data.success) {
clearDraft()
const newPostId = data.data?.id || data.data?.ID || postId
if (newPostId) {
window.location.href = '/posts/' + newPostId
} else {
window.location.href = '/studio/posts'
}
} else {
alert(data.message || '操作失败')
}
} catch (err) {
alert('请求失败,请重试')
} finally {
submitting = false
if (submitBtn) {
submitBtn.disabled = false
const isEdit = !!document.getElementById('postId')?.value
submitBtn.textContent = isEdit ? '保存修改' : '发布帖子'
}
}
}