Files
NebulaShell/website/community/assets/js/editor.js
Falck 76147bae94 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架

🧩 核心特性:
  - 插件热插拔 (importlib 动态加载)
  - 依赖自动解析 (拓扑排序 + 循环检测)
  - 企业级稳定 (熔断/降级/重试/隔离)
  - 事件驱动 (发布/订阅事件总线)
  - 完整配置 (YAML 配置 + 热重载)
2026-04-06 09:57:10 +08:00

253 lines
7.8 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.
/**
* OSS Community Editor JS
* Markdown 编辑器、实时预览、表单提交
*/
document.addEventListener('DOMContentLoaded', () => {
initEditor();
initToolbar();
initTags();
initForm();
});
// 初始化编辑器
function initEditor() {
const textarea = document.getElementById('postContent');
const titleInput = document.getElementById('postTitle');
// Tab 键支持
if (textarea) {
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + 2;
}
});
}
}
// 初始化工具栏
function initToolbar() {
const buttons = document.querySelectorAll('.md-btn');
const textarea = document.getElementById('postContent');
if (!textarea) return;
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.md;
insertMarkdown(textarea, action);
});
});
}
function insertMarkdown(textarea, action) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selected = textarea.value.substring(start, end);
let insertion = '';
switch (action) {
case 'bold':
insertion = `**${selected || '粗体文本'}**`;
break;
case 'italic':
insertion = `*${selected || '斜体文本'}*`;
break;
case 'heading':
insertion = `\n## ${selected || '标题'}\n`;
break;
case 'quote':
insertion = `\n> ${selected || '引用文本'}\n`;
break;
case 'code':
insertion = selected.includes('\n') ? `\n\`\`\`\n${selected || '代码块'}\n\`\`\`\n` : `\`${selected || '行内代码'}\``;
break;
case 'link':
insertion = `[${selected || '链接文本'}](url)`;
break;
case 'list':
insertion = `\n- ${selected || '列表项'}\n`;
break;
}
textarea.value = textarea.value.substring(0, start) + insertion + textarea.value.substring(end);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + insertion.length;
}
// 初始化标签
function initTags() {
const tagInput = document.getElementById('tagInput');
const tagsContainer = document.getElementById('tagsContainer');
if (!tagInput || !tagsContainer) return;
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const tagName = tagInput.value.trim();
if (tagName) {
addTag(tagName);
tagInput.value = '';
}
}
});
}
function addTag(name) {
const tagsContainer = document.getElementById('tagsContainer');
if (!tagsContainer) return;
// 检查是否已存在
const existing = tagsContainer.querySelectorAll('.tag-item');
for (const tag of existing) {
if (tag.textContent.trim().replace('×', '').trim() === name) {
return;
}
}
const tagEl = document.createElement('span');
tagEl.className = 'tag-item';
tagEl.innerHTML = `
${name}
<button type="button" class="tag-remove" onclick="this.parentElement.remove()">&times;</button>
`;
tagsContainer.appendChild(tagEl);
}
// 初始化表单
function initForm() {
const form = document.getElementById('postEditorForm');
const saveBtn = document.getElementById('savePostBtn');
if (!form || !saveBtn) return;
saveBtn.addEventListener('click', async (e) => {
e.preventDefault();
const postId = document.getElementById('editPostId').value;
const title = document.getElementById('postTitle').value.trim();
const content = document.getElementById('postContent').value.trim();
const categoryId = document.getElementById('postCategory').value;
// 收集标签
const tags = [];
document.querySelectorAll('#tagsContainer .tag-item').forEach(tag => {
const name = tag.textContent.replace('×', '').trim();
if (name) tags.push(name);
});
// 验证
if (!title) {
showError('请输入帖子标题');
return;
}
if (title.length < 5) {
showError('标题至少 5 个字符');
return;
}
if (!content) {
showError('请输入帖子内容');
return;
}
if (content.length < 10) {
showError('内容至少 10 个字符');
return;
}
if (!categoryId) {
showError('请选择分类');
return;
}
// 提交
setSaveButtonLoading(true);
const formData = {
title: title,
content: content,
category_id: categoryId,
tags: tags
};
if (postId) {
formData.id = parseInt(postId);
}
const action = postId ? 'update' : 'create';
const url = `api/posts.php?action=${action}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showSuccess(postId ? '更新成功!' : '发布成功!正在跳转...');
setTimeout(() => {
window.location.href = `post.php?id=${result.post_id || postId}`;
}, 1000);
} else {
showError(result.message || '操作失败');
}
} catch (error) {
showError('网络错误,请稍后重试');
} finally {
setSaveButtonLoading(false);
}
});
}
function showSuccess(message) {
const toast = document.getElementById('successToast');
const msgEl = document.getElementById('successMessage');
if (toast && msgEl) {
msgEl.textContent = message;
toast.style.display = 'flex';
setTimeout(() => { toast.style.display = 'none'; }, 3000);
}
}
function showError(message) {
const toast = document.getElementById('errorToast');
const msgEl = document.getElementById('errorMessage');
if (toast && msgEl) {
msgEl.textContent = message;
toast.style.display = 'flex';
setTimeout(() => { toast.style.display = 'none'; }, 4000);
}
}
function setSaveButtonLoading(loading) {
const btn = document.getElementById('savePostBtn');
if (!btn) return;
if (loading) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.innerHTML = `
<svg class="btn-spinner" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.25"/>
<path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" fill="currentColor" opacity="0.75"/>
</svg>
处理中...
`;
} else {
btn.disabled = false;
btn.style.opacity = '1';
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
${document.getElementById('editPostId').value ? '保存修改' : '发布帖子'}
`;
}
}