const chapterListEl = document.getElementById('chapter-list'); const toggleBtn = document.getElementById('toggle-reorder'); toggleBtn.addEventListener('click', () => { const active = toggleBtn.getAttribute('aria-pressed') === 'true'; active ? disableReorderMode() : enableReorderMode(); }); function enableReorderMode() { setRenameMode(false); toggleBtn.setAttribute('aria-pressed', 'true'); chapterListEl.classList.add('reorder-mode'); makeChaptersDraggable(true); makeLessonsDraggable(true); document.querySelectorAll('.add-lesson-btn').forEach(btn => { btn.style.display = 'none'; }); document.querySelectorAll('.add-chapter-btn').forEach(btn => { btn.style.display = 'none'; }); } function disableReorderMode() { toggleBtn.setAttribute('aria-pressed', 'false'); chapterListEl.classList.remove('reorder-mode'); makeChaptersDraggable(false); makeLessonsDraggable(false); document.querySelectorAll('.add-lesson-btn').forEach(btn => { btn.style.display = 'block'; }); document.querySelectorAll('.add-chapter-btn').forEach(btn => { btn.style.display = 'block'; }); } const chapterList = document.getElementById('chapter-list'); const btnReorder = document.getElementById('toggle-reorder'); const btnRename = document.getElementById('toggle-rename'); function setReorderMode(enabled) { if (enabled) { enableReorderMode(); } else { disableReorderMode(); } } function setRenameMode(enabled) { btnRename?.setAttribute('aria-pressed', String(enabled)); chapterList.classList.toggle('chapter-rename-mode', enabled); // const chapterContainers = document.querySelectorAll('li.chapter-item'); // const lessonContainers = document.querySelectorAll('li.lesson-item'); // chapterContainers.forEach(container => { // container.classList.toggle('chapter-rename-mode', enabled); // }); // lessonContainers.forEach(container => { // container.classList.toggle('chapter-rename-mode', enabled); // }); if (enabled) { document.querySelectorAll('.add-lesson-btn').forEach(btn => { btn.style.display = 'none'; }); document.querySelectorAll('.add-chapter-btn').forEach(btn => { btn.style.display = 'none'; }); } else { document.querySelectorAll('.add-lesson-btn').forEach(btn => { btn.style.display = 'block'; }); document.querySelectorAll('.add-chapter-btn').forEach(btn => { btn.style.display = 'block'; }); } } // —— 点击“改名模式”按钮 —— // btnRename?.addEventListener('click', (e) => { e.preventDefault(); const willEnable = btnRename.getAttribute('aria-pressed') !== 'true'; if (willEnable && btnReorder?.getAttribute('aria-pressed') === 'true') { setReorderMode(false); } setRenameMode(willEnable); }); // 获取展示文字:chapter-item 用 ,lesson-item 用 function getDisplayText(el) { if (el.classList.contains('chapter-item')) { return (el.querySelector('strong')?.textContent ?? '').trim(); } if (el.classList.contains('lesson-item')) { return (el.querySelector('span')?.textContent ?? '').trim(); } // 兜底 return ''; } // 设置展示文字:chapter-item 用 ,lesson-item 用 function setDisplayText(el, text) { if (el.classList.contains('chapter-item')) { let node = el.querySelector('strong'); if (!node) { node = document.createElement('strong'); el.prepend(node); } node.textContent = text; return; } if (el.classList.contains('lesson-item')) { let node = el.querySelector('span'); if (!node) { node = document.createElement('span'); el.prepend(node); } node.textContent = text; return; } } // —— 事件代理:改名模式下点击 li 触发重命名 —— // chapterList.addEventListener('click', (e) => { if (btnRename?.getAttribute('aria-pressed') !== 'true') return; // 非改名模式 const li = e.target.closest?.('li.chapter-item, li.lesson-item'); if (!li) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); // 若你担心同层还有其他捕获监听,也可用这个更狠的 const isChapter = li.classList.contains('chapter-item'); const isLesson = li.classList.contains('lesson-item'); // 读取旧名(章节:显示文字;课时:优先 data-lesson-name) const oldName = isLesson ? (li.dataset.lessonName || getDisplayText(li)) : getDisplayText(li); openTitleEditor({ title: isChapter ? '修改章节名称' : '修改课时名称', initialValue: oldName, onSubmit: async (value) => { const res = validateTitle(value); // 先持久化,成功后在本地同步 const resSave = await fetch(`/materials/structure/rename/${encodeURIComponent(EDIT_COURSE_CURRENT.material_id)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_chapter: isChapter, chapter_name: isChapter ? oldName : li.dataset.chapterName, lesson_name: isChapter ? "" : li.dataset.lessonName, new_name: value }) }) const data = await resSave.json(); if (!data.ok) return { ok: false, message: data.message }; const newName = res.value; if (isChapter) { // 将章节改名同步到本地 setDisplayText(li, newName); if (li.dataset.chapterName !== undefined) { li.dataset.chapterName = newName; } const scope = li.querySelectorAll?.('li.lesson-item'); if (scope && scope.length) { scope.forEach(item => { item.dataset.chapterName = newName; }); } } if (isLesson) { // 将课时改名同步到本地 setDisplayText(li, newName); li.dataset.lessonName = newName; } return { ok: true }; } }); }); /* 工具选择器 */ function getChapterItems() { return Array.from(chapterListEl.querySelectorAll(':scope > li.chapter-item')); } function getLessonItems(chapterLi) { const list = chapterLi.querySelector(':scope > ul.lessons-list'); return list ? Array.from(list.children).filter(el => el.classList.contains('lesson-item')) : []; } /* —— 章节拖拽 —— */ let draggingEl = null; // 当前拖拽的元素(chapter or lesson) let draggingType = null; // 'chapter' | 'lesson' function makeChaptersDraggable(enable) { getChapterItems().forEach(chLi => { const header = chLi.querySelector(':scope > .chapter-header'); if (!header) return; header.draggable = enable; // 只让 header 可拖 if (enable) { header.addEventListener('dragstart', e => onChapterHeaderDragStart(e, chLi)); header.addEventListener('dragend', onDragEndCommon); } else { header.removeEventListener('dragstart', e => onChapterHeaderDragStart(e, chLi)); header.removeEventListener('dragend', onDragEndCommon); } // 章节 li 仍作为“放置目标” chLi.addEventListener('dragover', onChapterDragOver); chLi.addEventListener('dragleave', onDragLeaveCommon); chLi.addEventListener('drop', onChapterDrop); }); } function onChapterHeaderDragStart(e, chLi) { // 若从 lesson 起手,直接拦截(双保险) if (e.target.closest('.lesson-item')) { e.preventDefault(); return; } draggingEl = chLi; // 拖动的是整个章节 li draggingType = 'chapter'; chLi.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', chLi.dataset.chapterName || ''); } function onChapterDragStart(e) { draggingEl = this; draggingType = 'chapter'; this.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.dataset.chapterName || ''); } function onChapterDragOver(e) { e.preventDefault(); if (draggingType !== 'chapter' || draggingEl === this) return; const rect = this.getBoundingClientRect(); const before = e.clientY < rect.top + rect.height / 2; this.classList.toggle('drop-indicator-top', before); this.classList.toggle('drop-indicator-bottom', !before); } function onChapterDrop(e) { e.preventDefault(); this.classList.remove('dragging') if (draggingType !== 'chapter' || draggingEl === this) return; const rect = this.getBoundingClientRect(); const before = e.clientY < rect.top + rect.height / 2; const parent = chapterListEl; if (before) parent.insertBefore(draggingEl, this); else parent.insertBefore(draggingEl, this.nextSibling); clearIndicators(); syncOutlineFromDOM(); } /* —— 课时拖拽(支持跨章节)—— */ function makeLessonsDraggable(enable) { getChapterItems().forEach(chLi => { // 子列表作为 drop 容器 let ul = chLi.querySelector(':scope > ul.lessons-list'); if (!ul) { ul = document.createElement('ul'); ul.className = 'lessons-list'; chLi.appendChild(ul); } // lessons 本身可拖拽 getLessonItems(chLi).forEach(li => { li.draggable = enable; if (enable) { li.addEventListener('dragstart', onLessonDragStart); li.addEventListener('dragend', onDragEndCommon); li.addEventListener('dragover', onLessonDragOver); li.addEventListener('dragleave', onDragLeaveCommon); li.addEventListener('drop', onLessonDrop); } else { li.removeEventListener('dragstart', onLessonDragStart); li.removeEventListener('dragend', onDragEndCommon); li.removeEventListener('dragover', onLessonDragOver); li.removeEventListener('dragleave', onDragLeaveCommon); li.removeEventListener('drop', onLessonDrop); li.classList.remove('dragging','drop-indicator-top','drop-indicator-bottom'); } }); // 容器允许作为 drop 目标:把 lesson 拖入空列表时可追加 if (enable) { ul.addEventListener('dragover', onLessonsListDragOver); ul.addEventListener('drop', onLessonsListDrop); } else { ul.removeEventListener('dragover', onLessonsListDragOver); ul.removeEventListener('drop', onLessonsListDrop); } }); } function onLessonDragStart(e) { draggingEl = this; draggingType = 'lesson'; this.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.dataset.lessonName || ''); } function onLessonDragOver(e) { e.preventDefault(); if (draggingType !== 'lesson' || draggingEl === this) return; const rect = this.getBoundingClientRect(); const before = e.clientY < rect.top + rect.height / 2; this.classList.toggle('drop-indicator-top', before); this.classList.toggle('drop-indicator-bottom', !before); } function onLessonDrop(e) { e.preventDefault(); if (draggingType !== 'lesson' || draggingEl === this) return; const rect = this.getBoundingClientRect(); const before = e.clientY < rect.top + rect.height / 2; const parent = this.parentElement; // lessons-list if (before) parent.insertBefore(draggingEl, this); else parent.insertBefore(draggingEl, this.nextSibling); clearIndicators(); syncOutlineFromDOM(); } /* 把 lesson 拖到某章节“标题”上:表示追加到该章节末尾 */ function onChapterHeaderDragOver(e) { if (draggingType !== 'lesson') return; e.preventDefault(); this.classList.add('drop-target'); } function onChapterHeaderDragLeave() { this.classList.remove('drop-target'); } function onChapterHeaderDrop(e) { if (draggingType !== 'lesson') return; e.preventDefault(); const chapterLi = this.closest('li.chapter-item'); const ul = chapterLi.querySelector(':scope > ul.lessons-list'); ul.appendChild(draggingEl); // 追加到末尾 this.classList.remove('drop-target'); clearIndicators(); syncOutlineFromDOM(); } /* 拖到空 lessons-list 上:也允许追加 */ function onLessonsListDragOver(e) { if (draggingType !== 'lesson') return; e.preventDefault(); } function onLessonsListDrop(e) { if (draggingType !== 'lesson') return; e.preventDefault(); this.appendChild(draggingEl); clearIndicators(); syncOutlineFromDOM(); } /* 通用 */ function onDragEndCommon() { // 确保结束时移除 'dragging' 状态 if (draggingEl) { draggingEl.classList.remove('dragging'); // 移除 'dragging' 状态 draggingEl = null; draggingType = null; } getChapterItems().forEach(chLi => { const ul = chLi.querySelector(':scope > ul.lessons-list'); if (!ul) return; console.log(getLessonItems(chLi).length); // 如果课时数量大于 2,移除虚拟的课时元素 if (getLessonItems(chLi).length >= 1) { const placeholder = ul.querySelector('.placeholder'); if (placeholder) { placeholder.remove(); } } // 如果章节为空,添加一个虚拟的课时 if (getLessonItems(chLi).length == 0) { const placeholderLesson = document.createElement('li'); placeholderLesson.classList.add('lesson-item', 'placeholder'); placeholderLesson.textContent = '拖拽课时到这里'; ul.appendChild(placeholderLesson); } }); } function onDragLeaveCommon() { this.classList.remove('drop-indicator-top','drop-indicator-bottom'); } function clearIndicators() { chapterListEl.querySelectorAll('.drop-indicator-top, .drop-indicator-bottom') .forEach(el => el.classList.remove('drop-indicator-top','drop-indicator-bottom')); chapterListEl.querySelectorAll('.chapter-header.drop-target') .forEach(el => el.classList.remove('drop-target')); } function syncOutlineFromDOM() { const chapters = getChapterItems().map(chLi => { const name = chLi.dataset.chapterName || chLi.querySelector(':scope > .chapter-header')?.textContent.trim(); const lessons = getLessonItems(chLi) .filter(li => !li.classList.contains('placeholder')) .map(li => li.dataset.lessonName || li.textContent.trim()); return { chapter_name: name, lessons }; }); // 写入 CURRENT if (window.EDIT_COURSE_CURRENT) { EDIT_COURSE_CURRENT.outline.phases = chapters.map(c => ({ text: c.chapter_name })); EDIT_COURSE_CURRENT.outline.stepsByPhase = {}; chapters.forEach(c => { EDIT_COURSE_CURRENT.outline.stepsByPhase[c.chapter_name] = c.lessons; }); } // 立即持久化 schedulePersistOrder(chapters); } // 定义一个全局/模块级变量,用来存放定时器 id let persistOrderTimer = null; function schedulePersistOrder(chapters) { // 如果已有定时器,先清掉 if (persistOrderTimer) { clearTimeout(persistOrderTimer); } // 设置新的定时器,1 秒后真正执行 persistOrderTimer = setTimeout(() => { persistOrderNow(chapters); persistOrderTimer = null; // 清理引用 }, 1000); } // 真正的持久化逻辑 async function persistOrderNow(chapters) { try { const payload = chapters || getChapterItems().map(chLi => ({ chapter_name: chLi.dataset.chapterName || chLi.querySelector(':scope > .chapter-header')?.textContent.trim(), lessons: getLessonItems(chLi).map( li => li.dataset.lessonName || li.textContent.trim() ) })); const res = await fetch(`/materials/structure/reorder/${encodeURIComponent(EDIT_COURSE_CURRENT.material_id)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ order: payload }) }); if (!res.ok) throw new Error('保存新顺序失败'); console.log('已保存顺序') } catch (err) { console.error(err); } }