diff --git a/Html/apps/services/course_service.py b/Html/apps/services/course_service.py index df1fbcc..5ed2445 100644 --- a/Html/apps/services/course_service.py +++ b/Html/apps/services/course_service.py @@ -112,6 +112,49 @@ def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teach # 如果删除操作成功,res.modified_count 应为 1 return res.modified_count == 1 +def reorder_material_structure(material_id, order): + mongo = current_app.extensions["mongo"] + material = mongo.db.materials.find_one({'_id': ObjectId(material_id)}) + if not material: + return False + chapter_map = {ch["chapter_name"]: ch for ch in material["chapters"]} + new_chapters = [] + for chapter_order in order: + ch_name = chapter_order['chapter_name'] + if ch_name not in chapter_map: + continue # 如果前端传了不存在的章节名,可以选择跳过或报错 + original_ch = chapter_map[ch_name] + + # 建立 lesson_name -> lesson dict + lesson_map = {ls["lesson_name"]: ls for ls in original_ch["lessons"]} + + # 按顺序重排 lessons + new_lessons = [] + for ls_name in chapter_order['lessons']: + if ls_name in lesson_map: + new_lessons.append(lesson_map[ls_name]) + + # 如果还有遗漏(前端没传的),可以追加到最后,避免丢数据 + for ls_name, ls in lesson_map.items(): + if ls not in new_lessons: + new_lessons.append(ls) + + # 构造新的 chapter + new_chapter = {**original_ch, "lessons": new_lessons} + new_chapters.append(new_chapter) + + # 更新 material + mongo.db.materials.update_one( + {"_id": ObjectId(material_id)}, + { + "$set": { + "chapters": new_chapters, + "updated_at": datetime.now() + } + } + ) + return True + def save_materials_markdown_cos( material_id: str, chapter_name: str, diff --git a/Html/apps/static/css/teacher_course_setting.css b/Html/apps/static/css/teacher_course_setting.css new file mode 100644 index 0000000..bc08950 --- /dev/null +++ b/Html/apps/static/css/teacher_course_setting.css @@ -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 */ + } \ No newline at end of file diff --git a/Html/apps/static/js/course_current.js b/Html/apps/static/js/course_current.js new file mode 100644 index 0000000..a97e09a --- /dev/null +++ b/Html/apps/static/js/course_current.js @@ -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: {} + }; + \ No newline at end of file diff --git a/Html/apps/static/js/teacher_course_setting.js b/Html/apps/static/js/teacher_course_setting.js new file mode 100644 index 0000000..53304c9 --- /dev/null +++ b/Html/apps/static/js/teacher_course_setting.js @@ -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); + } +} \ No newline at end of file diff --git a/Html/apps/static/js/teacherboard.js b/Html/apps/static/js/teacherboard.js index bf97673..5256aa9 100644 --- a/Html/apps/static/js/teacherboard.js +++ b/Html/apps/static/js/teacherboard.js @@ -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 = `${chapter.chapter_name}`; + chapterItem.classList.add('chapter-item'); + chapterItem.innerHTML = `${chapter.chapter_name}`; 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'); diff --git a/Html/apps/templates/lesson.html b/Html/apps/templates/lesson.html index a70397d..95c1755 100644 --- a/Html/apps/templates/lesson.html +++ b/Html/apps/templates/lesson.html @@ -7,6 +7,7 @@ + @@ -24,7 +25,11 @@
...
加载中...