Files
hsa/Html/apps/static/js/lesson_markdown.js
2025-11-03 16:14:57 +08:00

199 lines
5.9 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.

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