use markdown to edit

This commit is contained in:
Cai
2025-11-03 16:14:57 +08:00
parent ce88ae645e
commit 33844786a1
9 changed files with 1126 additions and 53 deletions

View File

@@ -0,0 +1,198 @@
const editors = {}; // id -> EasyMDE
(function(){
// ====== 配置:图片上传接口 ======
const UPLOAD_URL = '/api/upload'; // 返回 { url: 'https://...' }
// ====== Markdown 渲染器 ======
const md = window.markdownit({
html: false, // 禁止原生HTML安全些如需支持可设为 true
linkify: true,
typographer: true,
breaks: true
});
const $preview = document.getElementById('md-preview');
// ====== 三个 textarea 的 EasyMDE 实例 ======
const textareas = [
{ id: 'editor-lesson', tabBtnId: 'tab-btn-lesson' },
{ id: 'editor-prompt', tabBtnId: 'tab-btn-prompt' },
{ id: 'editor-score', tabBtnId: 'tab-btn-score' },
];
let activeId = 'editor-lesson'; // 默认激活
// 工具:将 Markdown 渲染到右侧预览
const renderToPreview = (markdownStr) => {
const rawHtml = md.render(markdownStr || '');
const safe = DOMPurify.sanitize(rawHtml);
$preview.innerHTML = safe;
};
// 工具:节流/防抖
function debounce(fn, delay){
let t;
return function(...args){
clearTimeout(t);
t = setTimeout(()=>fn.apply(this,args), delay);
}
}
const debouncedPreview = debounce(renderToPreview, 120);
// 工具:插入文本到当前光标
function insertAtCursor(cm, text){
const doc = cm.getDoc();
const cursor = doc.getCursor();
doc.replaceRange(text, cursor);
}
// 图片上传:给粘贴/拖拽/按钮选择共用
async function uploadFile(file){
const form = new FormData();
form.append('file', file);
const { data } = await axios.post(UPLOAD_URL, form, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if(!data || !data.url){
throw new Error('上传返回缺少 url 字段');
}
return data.url;
}
// 为一个 EasyMDE 实例注入图片粘贴/拖拽/按钮上传
function bindImageHandlers(easy){
const cm = easy.codemirror;
// 1) 粘贴图片
cm.on('paste', async (cmInst, e)=>{
const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
for(const it of items){
if(it.kind === 'file'){
const file = it.getAsFile();
if(file && file.type.startsWith('image/')){
e.preventDefault();
try{
const url = await uploadFile(file);
insertAtCursor(cmInst, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}
}
}
}
});
// 2) 拖拽图片
easy.codemirror.getWrapperElement().addEventListener('drop', async (e)=>{
const files = e.dataTransfer?.files || [];
if(!files.length) return;
const img = Array.from(files).find(f=>f.type.startsWith('image/'));
if(!img) return;
e.preventDefault();
try{
const url = await uploadFile(img);
insertAtCursor(cm, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}
});
// 3) 工具栏“上传图片”按钮:使用隐藏 input[type=file]
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
fileInput.addEventListener('change', async ()=>{
const file = fileInput.files?.[0];
if(!file) return;
try{
const url = await uploadFile(file);
insertAtCursor(cm, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}finally{
fileInput.value = '';
}
});
document.body.appendChild(fileInput);
// 将 input 触发器挂到 toolbar 按钮上(下面初始化里有 name: 'upload-image'
easy.toolbar?.forEach(btn=>{
if(btn?.name === 'upload-image'){
btn.action = () => fileInput.click();
}
});
}
// 创建并初始化一个 EasyMDE
function createMDEFor(textareaId){
const el = document.getElementById(textareaId);
if(!el) return null;
const mde = new EasyMDE({
element: el,
autoDownloadFontAwesome: false,
spellChecker: false,
status: false,
autofocus: textareaId === activeId,
minHeight: '100%',
renderingConfig: { singleLineBreaks: false },
uploadImage: false, // 我们自定义上传
toolbar: [
'bold','italic','heading','|',
'quote','unordered-list','ordered-list','table','|',
'code','link',
{ name:'upload-image', action:null, className:'fa fa-image', title:'插入图片(上传)' },
'|','preview','side-by-side','fullscreen',
'|','guide'
],
// 保证 textarea 同步(默认就会同步 value保存时你的原逻辑仍可读取
forceSync: true
});
// 内容变化 -> 刷新预览(仅激活编辑器触发)
mde.codemirror.on('change', ()=>{
if(textareaId === activeId){
debouncedPreview(mde.value());
}
});
bindImageHandlers(mde);
return mde;
}
// 初始化三个编辑器
for(const t of textareas){
editors[t.id] = createMDEFor(t.id);
}
// 初次渲染预览
debouncedPreview(editors[activeId]?.value() || '');
// 监听 Tab 切换按钮,让预览跟随当前激活编辑器
function setActiveEditor(id){
activeId = id;
// 确保 EasyMDE 的编辑区获得焦点(可选)
const ed = editors[id];
if(ed){
ed.codemirror.refresh();
ed.codemirror.focus();
renderToPreview(ed.value());
}
}
document.addEventListener('tab:change', (e) => {
const { editorId } = e.detail || {};
if (editorId) setActiveEditor(editorId);
});
// 如果你已有 tab 切换逻辑(例如给 .tab-btn 切换 active 类),保留原逻辑即可;
// 我们只需在切换时调用 setActiveEditor 对齐预览内容。
})();