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 7659aee468 feat: 新增创作中心(/studio),支持数据概览、内容管理、草稿箱、写文章
- 新增 StudioController,依赖 studioUseCase 接口,遵循 ISP 接口隔离
- PostRepo 新增 FindByUserIDAndStatus 方法,按用户+状态分页查询
- PostService 新增 ListByUser、GetOverview 及 StudioOverview 统计
- /studio 页面路由(概览/管理/草稿箱/写文章/数据分析)+ /api/studio API
- 复用 Vditor 编辑器,studio-editor.js 对接 /api/studio/posts 端点
- 新增 studio.css(B站风格三栏布局+统计卡片+状态筛选tabs)
- 模板引擎注册 add/subtract 函数,分页模板中直接使用
2026-05-30 19:55:29 +08:00

210 lines
6.3 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 端点
*/
let vditor = null
let draftTimer = null
let submitting = false
function getCsrfToken() {
var match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/)
return match ? match[1] : ''
}
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('vditor')
if (!container) return
const postId = document.getElementById('postId')?.value
const isEdit = !!postId
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: '在此输入正文...',
toolbar: [
'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)
}
updateWordCount()
},
input(value) {
scheduleDraft(value)
updateWordCount()
},
})
document.getElementById('postForm')?.addEventListener('submit', handleSubmit)
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 ? '保存修改' : '发布帖子'
}
}
}