课程-章节-课时顺序调整完成

This commit is contained in:
CakeCN
2025-09-03 20:33:23 +08:00
parent a0dd8cd234
commit 9de817d416
8 changed files with 443 additions and 8 deletions

View File

@@ -0,0 +1,11 @@
window.EDIT_COURSE_CURRENT = window.EDIT_COURSE_CURRENT || {
material_id: null,
outline: {
// phases: [{ text: chapterName }, ...]
phases: [],
// stepsByPhase: { [chapterName]: [lessonName, ...] }
stepsByPhase: {}
},
meta: {}
};

View File

@@ -0,0 +1,314 @@
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);
}
}

View File

@@ -122,6 +122,11 @@ function renderChapters(courseId) {
const chapterList = document.getElementById('chapter-list');
chapterList.innerHTML = ""; // 清空章节列表
material_id = courseId;
window.material_id = courseId; // 兼容你原来的全局变量
window.EDIT_COURSE_CURRENT = window.EDIT_COURSE_CURRENT || {};
EDIT_COURSE_CURRENT.material_id = courseId; // 新的统一来源
fetch(`/materials/${courseId}`)
.then(response => response.json())
.then(data => {
@@ -137,13 +142,13 @@ function renderChapters(courseId) {
data.chapters.forEach(chapter => {
const chapterItem = document.createElement('li');
chapterItem.innerHTML = `<strong>${chapter.chapter_name}</strong>`;
chapterItem.classList.add('chapter-item');
chapterItem.innerHTML = `<strong class="chapter-header">${chapter.chapter_name}</strong>`;
const lessonList = document.createElement('ul');
lessonList.classList.add('lessons-list');
chapter.lessons.forEach(lesson => {
const lessonItem = document.createElement('li');
lessonItem.innerHTML = lessonTemplate(material_id,chapter.chapter_name,lesson.lesson_name);
lessonList.appendChild(lessonItem);
lessonList.innerHTML += lessonTemplate(material_id,chapter.chapter_name,lesson.lesson_name);
});
chapterItem.appendChild(lessonList);
const addBtn = document.createElement('button');