199 lines
5.9 KiB
JavaScript
199 lines
5.9 KiB
JavaScript
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, ``);
|
||
}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, ``);
|
||
}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, ``);
|
||
}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 对齐预览内容。
|
||
})();
|
||
|
||
|
||
|
||
|
||
|