lesson的步骤、阶段顺序调整ok
This commit is contained in:
26
Html/apps/static/css/lesson_setting.css
Normal file
26
Html/apps/static/css/lesson_setting.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/* 开启排序模式时的卡片态与反馈 */
|
||||||
|
#outline-tree.reorder-mode li.lvl2,
|
||||||
|
#outline-tree.reorder-mode li.lvl3 {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin: 6px 0;
|
||||||
|
cursor: grab;
|
||||||
|
transition: box-shadow .2s, border-color .2s;
|
||||||
|
}
|
||||||
|
#outline-tree.reorder-mode li.lvl2:hover,
|
||||||
|
#outline-tree.reorder-mode li.lvl3:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
#outline-tree.reorder-mode li.lvl2:active,
|
||||||
|
#outline-tree.reorder-mode li.lvl3:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
#outline-tree li.dragging { opacity: .6; }
|
||||||
|
|
||||||
|
/* 放置提示线:在目标元素顶部/底部出现内阴影 */
|
||||||
|
#outline-tree li.drop-indicator-top { box-shadow: inset 0 3px 0 0 #4CAF50; }
|
||||||
|
#outline-tree li.drop-indicator-bottom { box-shadow: inset 0 -3px 0 0 #4CAF50; }
|
||||||
@@ -610,3 +610,445 @@ function loadStepToEditors(phaseText, stepText) {
|
|||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 假设已存在 parseHeadings(md) => {lines, tokens([{level,text,start,end}])}
|
||||||
|
|
||||||
|
function sliceByRange(lines, start, end) {
|
||||||
|
return lines.slice(start, end + 1).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取整个文档的“主题(#)”区块与其后所有 ## 阶段 token
|
||||||
|
function splitDoc(md) {
|
||||||
|
const parsed = parseHeadings(md);
|
||||||
|
const { lines, tokens } = parsed;
|
||||||
|
const h1 = tokens.find(t => t.level === 1);
|
||||||
|
// 文首到第一个##之前(含 # 与其正文)保留为 prelude
|
||||||
|
const firstPhase = tokens.find(t => t.level === 2);
|
||||||
|
const preludeEnd = firstPhase ? firstPhase.start - 1 : lines.length - 1;
|
||||||
|
const prelude = sliceByRange(lines, 0, Math.max(0, preludeEnd));
|
||||||
|
const phases = tokens.filter(t => t.level === 2);
|
||||||
|
return { lines, tokens, prelude, phases };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取某个“## 阶段”的完整文本(到下一个 ## 之前)
|
||||||
|
function getPhaseBlock(lines, phaseToken) {
|
||||||
|
return sliceByRange(lines, phaseToken.start, phaseToken.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析阶段块内的 ### 步骤;返回 {intro, steps: [{name, start,end,text}], order}
|
||||||
|
function analyzePhaseBlock(phaseBlock) {
|
||||||
|
const p = parseHeadings(phaseBlock);
|
||||||
|
const { lines, tokens } = p;
|
||||||
|
const h2 = tokens.find(t => t.level === 2); // 本块第一行
|
||||||
|
const steps = tokens.filter(t => t.level === 3);
|
||||||
|
// intro: 从 h2 之后到第一个 ### 之前的正文
|
||||||
|
const introStart = (h2 ? h2.start + 1 : 0);
|
||||||
|
const introEnd = steps.length ? steps[0].start - 1 : lines.length - 1;
|
||||||
|
const intro = introStart <= introEnd ? sliceByRange(lines, introStart, introEnd) : '';
|
||||||
|
|
||||||
|
const stepObjs = steps.map(st => ({
|
||||||
|
name: st.text,
|
||||||
|
start: st.start,
|
||||||
|
end: st.end,
|
||||||
|
text: sliceByRange(lines, st.start, st.end)
|
||||||
|
}));
|
||||||
|
return { intro, steps: stepObjs };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对“整篇文档”建立一个全局的 step 索引(phaseName::stepName -> text)
|
||||||
|
function buildGlobalStepIndex(lines, tokens) {
|
||||||
|
const res = new Map();
|
||||||
|
const phases = tokens.filter(t => t.level === 2);
|
||||||
|
for (let i = 0; i < phases.length; i++) {
|
||||||
|
const ph = phases[i];
|
||||||
|
const phNextStart = (i + 1 < phases.length) ? phases[i+1].start : lines.length;
|
||||||
|
// 该阶段内的所有 ###:
|
||||||
|
const steps = tokens.filter(t => t.level === 3 && t.start > ph.start && t.start < phNextStart);
|
||||||
|
for (const st of steps) {
|
||||||
|
const key = `${ph.text}||${st.text}`;
|
||||||
|
res.set(key, sliceByRange(lines, st.start, st.end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== 选择器与小工具 =====
|
||||||
|
const treeEl = document.getElementById('outline-tree');
|
||||||
|
const toggleOutlineBtn = document.getElementById('toggle-outline-reorder');
|
||||||
|
|
||||||
|
const isLvl1 = el => el.classList?.contains('lvl1');
|
||||||
|
const isLvl2 = el => el.classList?.contains('lvl2');
|
||||||
|
const isLvl3 = el => el.classList?.contains('lvl3');
|
||||||
|
const isBtn = el => el.tagName === 'BUTTON'; // 新增阶段/步骤的按钮
|
||||||
|
// 取一个阶段块(从 phaseLi 开始,到下一个 lvl2 之前的所有节点)
|
||||||
|
function blockOfPhase(phaseLi) {
|
||||||
|
const block = [phaseLi];
|
||||||
|
let n = phaseLi.nextElementSibling;
|
||||||
|
while (n && !isLvl2(n)) {
|
||||||
|
block.push(n);
|
||||||
|
n = n.nextElementSibling;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取阶段块最后一个节点(可能是最后一个 lvl3;若无 lvl3 则是 phaseLi 自己)
|
||||||
|
function lastNodeOfPhase(phaseLi) {
|
||||||
|
let last = phaseLi;
|
||||||
|
let n = phaseLi.nextElementSibling;
|
||||||
|
while (n && !isLvl2(n)) {
|
||||||
|
last = n;
|
||||||
|
n = n.nextElementSibling;
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一组 nodes(一个完整阶段块)移动到 targetPhaseLi 的前/后
|
||||||
|
* place: 'before' | 'after'
|
||||||
|
* 用占位符避免 DOM 移动时锚点错位
|
||||||
|
*/
|
||||||
|
function movePhaseBlock(nodes, targetPhaseLi, place = 'before') {
|
||||||
|
const parent = targetPhaseLi.parentNode;
|
||||||
|
if (place === 'before') {
|
||||||
|
const marker = document.createComment('phase-insert-before');
|
||||||
|
parent.insertBefore(marker, targetPhaseLi); // 在目标前放标记
|
||||||
|
nodes.forEach(node => parent.insertBefore(node, marker));
|
||||||
|
parent.removeChild(marker);
|
||||||
|
} else {
|
||||||
|
// after:要放在“目标阶段块最后一个节点”之后
|
||||||
|
const last = lastNodeOfPhase(targetPhaseLi);
|
||||||
|
const marker = document.createComment('phase-insert-after');
|
||||||
|
parent.insertBefore(marker, last.nextSibling); // 在目标块之后放标记
|
||||||
|
nodes.forEach(node => parent.insertBefore(node, marker));
|
||||||
|
parent.removeChild(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getAllItems() {
|
||||||
|
// 只取直接子节点里的 LI(忽略按钮等)
|
||||||
|
return Array.from(treeEl.children).filter(el => el.tagName === 'LI');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevLvl2Of(el) {
|
||||||
|
// 从当前节点向上找前一个 lvl2(步骤所属的阶段)
|
||||||
|
let p = el.previousElementSibling;
|
||||||
|
while (p) {
|
||||||
|
if (isLvl2(p)) return p;
|
||||||
|
p = p.previousElementSibling;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockOfPhase(phaseLi) {
|
||||||
|
// 返回从 phaseLi 开始直到下一个 lvl2(不含)的所有“块内元素”
|
||||||
|
const block = [phaseLi];
|
||||||
|
let n = phaseLi.nextElementSibling;
|
||||||
|
while (n && !isLvl2(n)) {
|
||||||
|
// 包括 lvl3(步骤)以及紧跟其后的“新增步骤按钮”(若你也插在 ul 里)
|
||||||
|
block.push(n);
|
||||||
|
n = n.nextElementSibling;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearIndicators() {
|
||||||
|
treeEl.querySelectorAll('.drop-indicator-top, .drop-indicator-bottom')
|
||||||
|
.forEach(el => el.classList.remove('drop-indicator-top', 'drop-indicator-bottom'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDragging() {
|
||||||
|
treeEl.querySelectorAll('.dragging').forEach(el => el.classList.remove('dragging'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 排序模式开关 =====
|
||||||
|
toggleOutlineBtn.addEventListener('click', () => {
|
||||||
|
const active = toggleOutlineBtn.getAttribute('aria-pressed') === 'true';
|
||||||
|
active ? disableOutlineReorder() : enableOutlineReorder();
|
||||||
|
});
|
||||||
|
|
||||||
|
function enableOutlineReorder() {
|
||||||
|
toggleOutlineBtn.setAttribute('aria-pressed', 'true');
|
||||||
|
treeEl.classList.add('reorder-mode');
|
||||||
|
|
||||||
|
// lvl2/lvl3 可拖拽;lvl1(主题)不可拖
|
||||||
|
getAllItems().forEach(li => {
|
||||||
|
if (isLvl1(li) || isBtn(li)) return;
|
||||||
|
li.draggable = true;
|
||||||
|
li.addEventListener('dragstart', onDragStart);
|
||||||
|
li.addEventListener('dragend', onDragEnd);
|
||||||
|
li.addEventListener('dragover', onDragOver);
|
||||||
|
li.addEventListener('dragleave', onDragLeave);
|
||||||
|
li.addEventListener('drop', onDrop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableOutlineReorder() {
|
||||||
|
toggleOutlineBtn.setAttribute('aria-pressed', 'false');
|
||||||
|
treeEl.classList.remove('reorder-mode');
|
||||||
|
|
||||||
|
getAllItems().forEach(li => {
|
||||||
|
li.draggable = false;
|
||||||
|
li.removeEventListener('dragstart', onDragStart);
|
||||||
|
li.removeEventListener('dragend', onDragEnd);
|
||||||
|
li.removeEventListener('dragover', onDragOver);
|
||||||
|
li.removeEventListener('dragleave', onDragLeave);
|
||||||
|
li.removeEventListener('drop', onDrop);
|
||||||
|
li.classList.remove('dragging', 'drop-indicator-top', 'drop-indicator-bottom');
|
||||||
|
});
|
||||||
|
|
||||||
|
// (可选)退出时持久化顺序
|
||||||
|
// persistOutlineOrderFromDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== DnD 状态 =====
|
||||||
|
let lessen_draggingEl = null; // 实际被拖的 “单个 li”(lvl2 或 lvl3)
|
||||||
|
let lessen_draggingType = null; // 'phase' | 'step'
|
||||||
|
let lessen_draggingBlock = null; // 若拖的是 lvl2,这里是整个“阶段块”的节点数组
|
||||||
|
|
||||||
|
function onDragStart(e) {
|
||||||
|
// 不让按钮参与拖拽
|
||||||
|
if (isBtn(e.target)) { e.preventDefault(); return; }
|
||||||
|
|
||||||
|
lessen_draggingEl = this;
|
||||||
|
lessen_draggingType = isLvl2(this) ? 'phase' : 'step';
|
||||||
|
this.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', this.textContent.trim());
|
||||||
|
|
||||||
|
if (lessen_draggingType === 'phase') {
|
||||||
|
lessen_draggingBlock = blockOfPhase(this); // 包含 phase + 其后的所有 lvl3/按钮
|
||||||
|
} else {
|
||||||
|
lessen_draggingBlock = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
removeDragging();
|
||||||
|
clearIndicators();
|
||||||
|
lessen_draggingEl = null;
|
||||||
|
lessen_draggingType = null;
|
||||||
|
lessen_draggingBlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!lessen_draggingEl || this === lessen_draggingEl) return;
|
||||||
|
|
||||||
|
clearIndicators();
|
||||||
|
|
||||||
|
// 仅在合法组合时显示指示
|
||||||
|
if (lessen_draggingType === 'phase') {
|
||||||
|
if (!isLvl2(this)) return; // 关键:phase 只对 lvl2 生效
|
||||||
|
} else if (lessen_draggingType === 'step') {
|
||||||
|
if (!isLvl3(this)) return; // 关键:step 只对 lvl3 生效
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const before = e.clientY < rect.top + rect.height / 2;
|
||||||
|
this.classList.add(before ? 'drop-indicator-top' : 'drop-indicator-bottom');
|
||||||
|
}
|
||||||
|
function onDragLeave() {
|
||||||
|
this.classList.remove('drop-indicator-top', 'drop-indicator-bottom');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!lessen_draggingEl || this === lessen_draggingEl) return;
|
||||||
|
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const before = e.clientY < rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
if (lessen_draggingType === 'phase') {
|
||||||
|
// ⭐ 仅允许丢在 lvl2 上;丢在 lvl3 直接忽略
|
||||||
|
if (!isLvl2(this)) return;
|
||||||
|
const nodes = lessen_draggingBlock || [lessen_draggingEl];
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
// 插到目标 lvl2 前
|
||||||
|
movePhaseBlock(nodes, this, 'before');
|
||||||
|
} else {
|
||||||
|
// 插到目标“阶段块的最后一个节点”之后
|
||||||
|
movePhaseBlock(nodes, this, 'after');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (lessen_draggingType === 'step') {
|
||||||
|
// ⭐ 仅允许丢在 lvl3 上;丢在 lvl2(阶段)不触发步骤重排
|
||||||
|
if (!isLvl3(this)) return;
|
||||||
|
|
||||||
|
const parent = treeEl;
|
||||||
|
if (before) parent.insertBefore(lessen_draggingEl, this);
|
||||||
|
else parent.insertBefore(lessen_draggingEl, this.nextSibling);
|
||||||
|
}
|
||||||
|
// ⭐ 确保 add-phase-btn 永远在最底部
|
||||||
|
const btn = treeEl.querySelector('.add-phase-btn');
|
||||||
|
if (btn) treeEl.appendChild(btn);
|
||||||
|
clearIndicators();
|
||||||
|
removeDragging();
|
||||||
|
syncOutlineFromDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== DOM → 内存:回写 CURRENT.outline =====
|
||||||
|
function syncOutlineFromDOM() {
|
||||||
|
const lis = getAllItems();
|
||||||
|
const phases = [];
|
||||||
|
const stepsByPhase = {};
|
||||||
|
|
||||||
|
let currentPhase = null;
|
||||||
|
|
||||||
|
for (const el of lis) {
|
||||||
|
if (isLvl1(el)) continue; // 忽略主题
|
||||||
|
if (isBtn(el)) continue; // 忽略新增按钮(如果按钮是 li 以外的元素)
|
||||||
|
|
||||||
|
if (isLvl2(el)) {
|
||||||
|
currentPhase = el.textContent.trim();
|
||||||
|
phases.push({ text: currentPhase });
|
||||||
|
stepsByPhase[currentPhase] = [];
|
||||||
|
} else if (isLvl3(el)) {
|
||||||
|
if (!currentPhase) continue; // 安全防御
|
||||||
|
stepsByPhase[currentPhase].push(el.textContent.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CURRENT.outline.phases = phases;
|
||||||
|
CURRENT.outline.stepsByPhase = stepsByPhase;
|
||||||
|
|
||||||
|
// (可选)立即保存结构到后端
|
||||||
|
persistOutlineOrderByRewriting();
|
||||||
|
}
|
||||||
|
// 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 },
|
||||||
|
// active:{ phase:null, step:null },
|
||||||
|
// outline:null
|
||||||
|
// };
|
||||||
|
async function persistOutlineOrderByRewriting() {
|
||||||
|
// 1) 按 outline 重写三份原文
|
||||||
|
const newLesson = rewriteMarkdownByOutline(CURRENT.raw.lesson, CURRENT.outline);
|
||||||
|
const newPrompt = rewriteMarkdownByOutline(CURRENT.raw.prompt, CURRENT.outline);
|
||||||
|
const newScore = rewriteMarkdownByOutline(CURRENT.raw.score, CURRENT.outline);
|
||||||
|
|
||||||
|
// 2) 更新本地内存(可选:重新解析 & 重绘)
|
||||||
|
CURRENT.raw.lesson = newLesson;
|
||||||
|
CURRENT.raw.prompt = newPrompt;
|
||||||
|
CURRENT.raw.score = newScore;
|
||||||
|
|
||||||
|
CURRENT.parsed.lesson = parseHeadings(newLesson);
|
||||||
|
CURRENT.parsed.prompt = parseHeadings(newPrompt);
|
||||||
|
CURRENT.parsed.score = parseHeadings(newScore);
|
||||||
|
// 可选:renderOutline(CURRENT.outline); // outline 顺序已是最新
|
||||||
|
console.log(CURRENT.outline);
|
||||||
|
console.log(CURRENT.raw.lesson);
|
||||||
|
await saveToOrigin({
|
||||||
|
lesson: { cdn_link: CURRENT.links.lesson, content: newLesson },
|
||||||
|
prompt: { cdn_link: CURRENT.links.prompt, content: newPrompt },
|
||||||
|
score: { cdn_link: CURRENT.links.score, content: newScore },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据 outline 重排整篇 md:
|
||||||
|
* - phases: [{text}]
|
||||||
|
* - stepsByPhase: { [phaseName]: [stepName, ...] }
|
||||||
|
* 规则:
|
||||||
|
* 1) 一级标题/前导(prelude)保持不变;
|
||||||
|
* 2) 按 outline.phases 的顺序重排各“## 阶段块”;
|
||||||
|
* 3) 阶段块内部:保持原 intro 文本;按 stepsByPhase 顺序重排所有 ###;
|
||||||
|
* 4) 被移动到其它阶段的步骤会从原处“搬走”;未在新顺序中出现的旧步骤会作为“剩余”追加在末尾;
|
||||||
|
* 5) 缺少的步骤会生成占位骨架。
|
||||||
|
*/
|
||||||
|
function rewriteMarkdownByOutline(md, outline) {
|
||||||
|
const { lines, tokens, prelude, phases: oldPhaseTokens } = splitDoc(md);
|
||||||
|
|
||||||
|
// 索引:phaseName -> 原始阶段块文本 & 解析
|
||||||
|
const oldPhaseMap = new Map();
|
||||||
|
oldPhaseTokens.forEach(pt => {
|
||||||
|
oldPhaseMap.set(pt.text, {
|
||||||
|
token: pt,
|
||||||
|
block: getPhaseBlock(lines, pt)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全局步骤索引(带原所属阶段前缀,避免重名冲突)
|
||||||
|
const globalStepIndex = buildGlobalStepIndex(lines, tokens);
|
||||||
|
const usedSteps = new Set();
|
||||||
|
|
||||||
|
function takeStepTextPreferAny(stepName) {
|
||||||
|
// 任意相同名(跨阶段搬运)
|
||||||
|
for (const [k, v] of globalStepIndex.entries()) {
|
||||||
|
const name = k.split('||')[1];
|
||||||
|
if (name === stepName && !usedSteps.has(k)) {
|
||||||
|
usedSteps.add(k);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不存在则返回骨架
|
||||||
|
return `### ${stepName}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按新顺序构建各阶段块
|
||||||
|
const newPhaseBlocks = [];
|
||||||
|
const desiredPhases = outline.phases.map(p => p.text);
|
||||||
|
for (const phName of desiredPhases) {
|
||||||
|
const old = oldPhaseMap.get(phName);
|
||||||
|
const h2Line = `## ${phName}`;
|
||||||
|
let intro = '';
|
||||||
|
let leftovers = []; // 本阶段原有但未被使用/未在新顺序中列出的步骤
|
||||||
|
|
||||||
|
if (old) {
|
||||||
|
const analyzed = analyzePhaseBlock(old.block);
|
||||||
|
intro = analyzed.intro;
|
||||||
|
// 先标记本阶段原有的步骤(用于“剩余”)
|
||||||
|
const oldStepNames = analyzed.steps.map(s => s.name);
|
||||||
|
|
||||||
|
// 目标顺序
|
||||||
|
const wantSteps = outline.stepsByPhase[phName] || [];
|
||||||
|
const orderedParts = wantSteps.map(stepName => takeStepTextPreferAny(stepName));
|
||||||
|
|
||||||
|
// 追加旧阶段中未被“使用”的步骤(保持内容不丢)
|
||||||
|
for (const st of analyzed.steps) {
|
||||||
|
// 若相同名的某个实例已被 usedSteps 吃掉,说明已搬走或已按需放置
|
||||||
|
// 需要判断具体 key(本阶段::该名)
|
||||||
|
const key = `${phName}||${st.name}`;
|
||||||
|
if (!usedSteps.has(key) && !wantSteps.includes(st.name)) {
|
||||||
|
orderedParts.push(st.text);
|
||||||
|
usedSteps.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = [h2Line, '', intro].join('\n').trimEnd() + '\n\n'
|
||||||
|
+ orderedParts.join('\n\n').trimEnd() + '\n\n';
|
||||||
|
newPhaseBlocks.push(body);
|
||||||
|
} else {
|
||||||
|
// 新增的阶段(原文里不存在):生成一个空阶段骨架 + 目标步骤骨架
|
||||||
|
const wantSteps = outline.stepsByPhase[phName] || [];
|
||||||
|
const orderedParts = wantSteps.map(st => `### ${st}\n\n`);
|
||||||
|
const body = [h2Line, '', /* intro */ ''].join('\n').trimEnd() + '\n\n'
|
||||||
|
+ orderedParts.join('\n\n').trimEnd() + '\n\n';
|
||||||
|
newPhaseBlocks.push(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把未列入 outline 的“遗留阶段”也接到末尾,避免内容丢失
|
||||||
|
for (const pt of oldPhaseTokens) {
|
||||||
|
if (!desiredPhases.includes(pt.text)) {
|
||||||
|
const blk = getPhaseBlock(lines, pt);
|
||||||
|
newPhaseBlocks.push(blk.trimEnd() + '\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装全文
|
||||||
|
const rebuilt = (prelude.trimEnd() + '\n\n' + newPhaseBlocks.join('')).replace(/\n{3,}/g, '\n\n');
|
||||||
|
return rebuilt.trimEnd() + '\n';
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="/static/css/lesson.css">
|
<link rel="stylesheet" href="/static/css/lesson.css">
|
||||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/lesson_setting.css">
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
@@ -39,6 +40,10 @@
|
|||||||
<!-- 课时编辑页主体:左侧大纲 + 右侧编辑 -->
|
<!-- 课时编辑页主体:左侧大纲 + 右侧编辑 -->
|
||||||
<div class="lesson-editor">
|
<div class="lesson-editor">
|
||||||
<aside class="outline">
|
<aside class="outline">
|
||||||
|
<button id="toggle-outline-reorder" class="reorder-btn" title="排序模式" aria-pressed="false">
|
||||||
|
排序
|
||||||
|
</button>
|
||||||
|
|
||||||
<ul id="outline-tree"></ul>
|
<ul id="outline-tree"></ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user