Files
hsa/Html/apps/static/js/lesson.js
2025-08-31 00:30:53 +08:00

205 lines
8.6 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.

window.addEventListener('load', ()=>{
console.log(window.material);
console.log(window.lesson);
editLesson(window.material.material_id, window.lesson.chapter_name, window.lesson.lesson_name);
});
function parseHeadings(mdText) {
const lines = mdText.split('\n');
const tokens = [];
for (let i=0;i<lines.length;i++) {
const m = lines[i].match(/^(#{1,3})\s+(.*)$/);
if (m) tokens.push({ level: m[1].length, text: m[2].trim(), line: i });
}
// 确定每个heading的区间到下一个同级或上级前一行
for (let i=0;i<tokens.length;i++) {
let j = i+1;
while (j<tokens.length && tokens[j].level > tokens[i].level) j++;
// 找到第一个 >= 当前level 的 heading 或到文末
let k = i+1;
while (k<tokens.length && tokens[k].level > tokens[i].level) k++;
const nextSameOrHigher = k<tokens.length ? tokens[k].line : lines.length;
tokens[i].start = tokens[i].line;
tokens[i].end = nextSameOrHigher - 1;
}
return { lines, tokens };
}
// 从解析结果中抽取一个 heading 段落文本
function sliceSection(parsed, headingText, level) {
const t = parsed.tokens.find(x => x.level===level && x.text===headingText);
if (!t) return '';
return parsed.lines.slice(t.start, t.end+1).join('\n');
}
// 根据三个 md构造统一大纲树一级唯一“主题”其下多个阶段##,其下多个步骤###
function buildUnifiedOutline(parsedLesson, parsedPrompt, parsedScore) {
// 取 lesson 的结构为主(也可做三者交集/并集策略,这里简单以 lesson 为基准)
const root1 = parsedLesson.tokens.find(t => t.level===1);
const phases = parsedLesson.tokens.filter(t => t.level===2);
const stepsByPhase = {};
phases.forEach(ph => {
stepsByPhase[ph.text] = parsedLesson.tokens.filter(t => t.level===3 && t.start>ph.start && t.end<=ph.end)
.map(s => s.text);
});
return { theme: root1 ? root1.text : '主题', phases, stepsByPhase };
}
let CURRENT = {
material_id: null, chapter_name: null, lesson_name: null,
links: { lesson:'', prompt:'', score:'' },
raw: { lesson:'', prompt:'', score:'' },
parsed:{ lesson:null, prompt:null, score:null },
outline:null
};
// 入口:从你的列表调用
async function editLesson(material_id, chapter_name, lesson_name) {
CURRENT.material_id = material_id;
CURRENT.chapter_name = chapter_name;
CURRENT.lesson_name = lesson_name;
// 打开右侧课程详情先占位
const details = document.getElementById('courseDetails');
const detailsContent = document.getElementById('details-content');
detailsContent.innerHTML = '<p>...</p>';
details.classList.add('open');
// 拉课时详情
metaRes = window.lesson;
const meta = metaRes;
document.getElementById('course-title').textContent = meta.name || meta.material_name || lesson_name;
// 右侧面板描述/目录(简化)
detailsContent.innerHTML = `<h4>描述</h4><p>${meta.description || meta.material_description || '暂无描述'}</p>`;
const chapterList = document.getElementById('chapter-list');
chapterList.innerHTML = '';
(meta.chapters||[]).forEach(ch=>{
const li = document.createElement('li');
li.textContent = ch.chapter_name;
chapterList.appendChild(li);
});
// 保存三份链接
CURRENT.links.lesson = meta.markdown_lesson_file_link;
CURRENT.links.prompt = meta.markdown_prompt_file_name;
CURRENT.links.score = meta.markdown_score_prompt_file_link;
console.log(CURRENT.links);
// 拉三份 Markdown
const [t1, t2, t3] = await Promise.all([ fetch(CURRENT.links.lesson).then(r=>r.text()), fetch(CURRENT.links.prompt).then(r=>r.text()), fetch(CURRENT.links.score ).then(r=>r.text()) ]);
CURRENT.raw.lesson = t1; CURRENT.raw.prompt = t2; CURRENT.raw.score = t3;
CURRENT.parsed.lesson = parseHeadings(t1);
CURRENT.parsed.prompt = parseHeadings(t2);
CURRENT.parsed.score = parseHeadings(t3);
// 构建大纲并渲染
CURRENT.outline = buildUnifiedOutline(CURRENT.parsed.lesson, CURRENT.parsed.prompt, CURRENT.parsed.score);
renderOutline(CURRENT.outline);
// 默认选中第一个“步骤”(若无步骤则整个文档)
const firstPhase = CURRENT.outline.phases?.[0]?.text;
const firstStep = firstPhase && CURRENT.outline.stepsByPhase[firstPhase]?.[0];
if (firstPhase && firstStep) {
loadStepToEditors(firstPhase, firstStep);
highlightActive(firstPhase, firstStep);
} else {
// 无###时,直接把全文放到 Lesson 标签
document.getElementById('editor-lesson').value = CURRENT.raw.lesson;
document.getElementById('editor-prompt').value = CURRENT.raw.prompt;
document.getElementById('editor-score').value = CURRENT.raw.score;
}
}
// 渲染左侧大纲
function renderOutline(outline) {
const tree = document.getElementById('outline-tree');
tree.innerHTML = '';
if (outline.theme) {
const li = document.createElement('li'); li.className='lvl1'; li.textContent = `# ${outline.theme}`; tree.appendChild(li);
}
outline.phases.forEach(ph=>{
const li2 = document.createElement('li'); li2.className='lvl2'; li2.textContent = `## ${ph.text}`;
tree.appendChild(li2);
const steps = outline.stepsByPhase[ph.text] || [];
steps.forEach(st=>{
const li3 = document.createElement('li');
li3.className='lvl3';
li3.textContent = `### ${st}`;
li3.dataset.phase = ph.text;
li3.dataset.step = st;
li3.onclick = () => {
loadStepToEditors(ph.text, st);
highlightActive(ph.text, st);
};
tree.appendChild(li3);
});
});
}
function highlightActive(phase, step) {
document.querySelectorAll('#outline-tree li').forEach(li=>li.classList.remove('active'));
const target = Array.from(document.querySelectorAll('#outline-tree li.lvl3'))
.find(li => li.dataset.phase===phase && li.dataset.step===step);
if (target) target.classList.add('active');
}
// 把“某个步骤”对应的三个片段载入到三个编辑器
function loadStepToEditors(phaseText, stepText) {
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
const L = CURRENT.parsed.lesson, P = CURRENT.parsed.prompt, S = CURRENT.parsed.score;
const lessonPhase = sliceSection(L, phaseText, 2) || CURRENT.raw.lesson;
const promptPhase = sliceSection(P, phaseText, 2) || CURRENT.raw.prompt;
const scorePhase = sliceSection(S, phaseText, 2) || CURRENT.raw.score;
// 在各 phase 片段里再找 ### step
const subL = parseHeadings(lessonPhase);
const subP = parseHeadings(promptPhase);
const subS = parseHeadings(scorePhase);
const lText = sliceSection(subL, stepText, 3) || lessonPhase;
const pText = sliceSection(subP, stepText, 3) || promptPhase;
const sText = sliceSection(subS, stepText, 3) || scorePhase;
document.getElementById('editor-lesson').value = lText;
document.getElementById('editor-prompt').value = pText;
document.getElementById('editor-score').value = sText;
}
// Tab 切换
document.addEventListener('click', (e)=>{
if (e.target.classList.contains('tab-btn')) {
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
e.target.classList.add('active');
const id = e.target.dataset.target;
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
});
// 保存全部(把当前三个编辑器的文本整体 PUT 回各自文件)
document.getElementById('save-all-btn')?.addEventListener('click', async ()=>{
const l = document.getElementById('editor-lesson').value;
const p = document.getElementById('editor-prompt').value;
const s = document.getElementById('editor-score').value;
try {
const [r1, r2, r3] = await Promise.all([
fetch(CURRENT.links.lesson, {method:'PUT', body:l}),
fetch(CURRENT.links.prompt, {method:'PUT', body:p}),
fetch(CURRENT.links.score , {method:'PUT', body:s}),
]);
if (r1.ok && r2.ok && r3.ok) alert('已保存');
else alert('保存失败,请重试');
} catch (err) {
console.error(err); alert('保存出错');
}
});
// 右侧详情开/关
function closeCourseDetails(){ document.getElementById('courseDetails').classList.remove('open'); }
function showInputForNewChapter(){ /* 省略:你的新增逻辑 */ }