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() { 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'; }); } /* 工具选择器 */ 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); } }