use markdown to edit
This commit is contained in:
198
Html/apps/static/js/lesson_markdown.js
Normal file
198
Html/apps/static/js/lesson_markdown.js
Normal 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, ``);
|
||||
}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 对齐预览内容。
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user