314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
|
||
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);
|
||
}
|
||
} |