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 对齐预览内容。 })();