课程-章节-课时顺序调整完成
This commit is contained in:
40
Html/apps/static/css/teacher_course_setting.css
Normal file
40
Html/apps/static/css/teacher_course_setting.css
Normal file
@@ -0,0 +1,40 @@
|
||||
/* 开启排序模式时,统一卡片样式 */
|
||||
#chapter-list.reorder-mode .chapter-item,
|
||||
#chapter-list.reorder-mode .lesson-item {
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: grab;
|
||||
transition: box-shadow .2s, border-color .2s;
|
||||
}
|
||||
|
||||
/* 间距与内边距 */
|
||||
.chapter-item { padding: 8px 10px; margin: 8px 0; }
|
||||
.lesson-item { padding: 6px 10px; margin: 6px 0; }
|
||||
|
||||
/* hover/active 反馈 */
|
||||
#chapter-list.reorder-mode .chapter-item:hover,
|
||||
#chapter-list.reorder-mode .lesson-item:hover {
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.1);
|
||||
}
|
||||
#chapter-list.reorder-mode .chapter-item:active,
|
||||
#chapter-list.reorder-mode .lesson-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.dragging { opacity: .6; }
|
||||
|
||||
/* 放置位置提示线(在元素顶部/底部) */
|
||||
.drop-indicator-top { box-shadow: inset 0 3px 0 0 #4CAF50; }
|
||||
.drop-indicator-bottom { box-shadow: inset 0 -3px 0 0 #4CAF50; }
|
||||
|
||||
/* 把 lesson 拖到 chapter 标题上时,高亮标题,表示会追加到该章节末尾 */
|
||||
.chapter-header.drop-target {
|
||||
outline: 2px dashed #4CAF50;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.chapter-header {
|
||||
display: block; /* 使元素成为块级元素,占满整行 */
|
||||
/* 文字默认左对齐,无需额外设置text-align: left */
|
||||
}
|
||||
11
Html/apps/static/js/course_current.js
Normal file
11
Html/apps/static/js/course_current.js
Normal 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: {}
|
||||
};
|
||||
|
||||
314
Html/apps/static/js/teacher_course_setting.js
Normal file
314
Html/apps/static/js/teacher_course_setting.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user