rename chapter/lesson
This commit is contained in:
@@ -92,6 +92,44 @@ def add_material_chapter(material_id, chapter_name, teacher_id):
|
||||
{'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}})
|
||||
return True
|
||||
|
||||
def rename_material_structure(material_id, is_chapter, chapter_name, lesson_name, new_name):
|
||||
mongo = current_app.extensions["mongo"]
|
||||
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
|
||||
# 先检查是否存在
|
||||
if is_chapter:
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == chapter_name:
|
||||
break
|
||||
return False
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == new_name:
|
||||
return False
|
||||
else:
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == chapter_name:
|
||||
break
|
||||
return False
|
||||
for lesson in chapter['lessons']:
|
||||
if lesson['lesson_name'] == lesson_name:
|
||||
break
|
||||
for lesson in chapter['lessons']:
|
||||
if lesson['lesson_name'] == new_name:
|
||||
return False
|
||||
|
||||
if is_chapter:
|
||||
mongo.db.materials.update_one(
|
||||
{'_id': ObjectId(material_id)},
|
||||
{'$set': {'chapters.$[c].chapter_name': new_name}},
|
||||
array_filters=[{'c.chapter_name': chapter_name}]
|
||||
)
|
||||
else:
|
||||
mongo.db.materials.update_one(
|
||||
{'_id': ObjectId(material_id)},
|
||||
{'$set': {'chapters.$[c].lessons.$[l].lesson_name': new_name}},
|
||||
array_filters=[{'c.chapter_name': chapter_name}, {'l.lesson_name': lesson_name}]
|
||||
)
|
||||
return True
|
||||
|
||||
def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
|
||||
"""
|
||||
删除指定课程的课时。
|
||||
|
||||
@@ -23,6 +23,21 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 改名模式下两类元素的高亮与交互 */
|
||||
#chapter-list.chapter-rename-mode .chapter-item,
|
||||
#chapter-list.chapter-rename-mode .lesson-item {
|
||||
border: 1px solid #dc2626;
|
||||
border-radius: 6px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
#chapter-list.chapter-rename-mode .chapter-item:hover,
|
||||
#chapter-list.chapter-rename-mode .lesson-item:hover {
|
||||
background: rgba(220,38,38,.06);
|
||||
box-shadow: 0 0 0 2px rgba(220,38,38,.15) inset;
|
||||
}
|
||||
|
||||
.dragging { opacity: .6; }
|
||||
|
||||
/* 放置位置提示线(在元素顶部/底部) */
|
||||
@@ -37,4 +52,14 @@
|
||||
.chapter-header {
|
||||
display: block; /* 使元素成为块级元素,占满整行 */
|
||||
/* 文字默认左对齐,无需额外设置text-align: left */
|
||||
}
|
||||
}
|
||||
|
||||
/* 改名按钮的激活态 */
|
||||
.rename-btn[aria-pressed="true"] {
|
||||
background: rgba(220,38,38,.12);
|
||||
outline: 2px solid #dc2626;
|
||||
}
|
||||
.reorder-btn[aria-pressed="true"] {
|
||||
background: rgba(220,38,38,.12);
|
||||
outline: 2px solid #dc2626;
|
||||
}
|
||||
@@ -110,6 +110,8 @@
|
||||
background-color: #f0f0f0;
|
||||
border: 1px dashed #bbb;
|
||||
}
|
||||
|
||||
|
||||
/* 模态框背景 */
|
||||
.modal {
|
||||
display: none; /* 初始时隐藏 */
|
||||
|
||||
@@ -45,24 +45,43 @@
|
||||
const cancelBtn = title_change_modal.querySelector('#cancel-btn');
|
||||
const errBox = title_change_modal.querySelector('#title-error');
|
||||
const prevFocus = document.activeElement;
|
||||
|
||||
const setLoading = (on) => {
|
||||
okBtn.disabled = on;
|
||||
cancelBtn.disabled = on;
|
||||
okBtn.textContent = on ? '保存中…' : '确定';
|
||||
};
|
||||
const close = () => {
|
||||
document.body.removeChild(backdrop);
|
||||
if (prevFocus && prevFocus.focus) prevFocus.focus();
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const raw = input.value;
|
||||
const result = onSubmit ? onSubmit(raw) : { ok: true };
|
||||
if (result && result.ok) {
|
||||
close();
|
||||
} else if (result && result.message) {
|
||||
errBox.textContent = result.message;
|
||||
input.setAttribute('aria-invalid', 'true');
|
||||
input.focus();
|
||||
}
|
||||
errBox.textContent = '';
|
||||
setLoading(true);
|
||||
|
||||
const maybePromise = onSubmit ? onSubmit(input.value) : { ok: true };
|
||||
|
||||
Promise
|
||||
.resolve(maybePromise)
|
||||
.then(result => {
|
||||
if (result && result.ok) {
|
||||
close();
|
||||
} else {
|
||||
// 显示错误但不关闭
|
||||
const msg = result && result.message ? result.message : '提交失败';
|
||||
errBox.textContent = msg;
|
||||
input.setAttribute('aria-invalid', 'true');
|
||||
input.focus();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Submit error:', err);
|
||||
errBox.textContent = '提交异常,请稍后再试';
|
||||
input.setAttribute('aria-invalid', 'true');
|
||||
input.focus();
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
okBtn.addEventListener('click', submit);
|
||||
cancelBtn.addEventListener('click', close);
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ toggleBtn.addEventListener('click', () => {
|
||||
});
|
||||
|
||||
function enableReorderMode() {
|
||||
setRenameMode(false);
|
||||
toggleBtn.setAttribute('aria-pressed', 'true');
|
||||
chapterListEl.classList.add('reorder-mode');
|
||||
makeChaptersDraggable(true);
|
||||
@@ -32,6 +33,147 @@ function disableReorderMode() {
|
||||
btn.style.display = 'block';
|
||||
});
|
||||
}
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
const btnReorder = document.getElementById('toggle-reorder');
|
||||
const btnRename = document.getElementById('toggle-rename');
|
||||
|
||||
function setReorderMode(enabled) {
|
||||
if (enabled) {
|
||||
enableReorderMode();
|
||||
} else {
|
||||
disableReorderMode();
|
||||
}
|
||||
}
|
||||
function setRenameMode(enabled) {
|
||||
btnRename?.setAttribute('aria-pressed', String(enabled));
|
||||
chapterList.classList.toggle('chapter-rename-mode', enabled);
|
||||
// const chapterContainers = document.querySelectorAll('li.chapter-item');
|
||||
// const lessonContainers = document.querySelectorAll('li.lesson-item');
|
||||
|
||||
// chapterContainers.forEach(container => {
|
||||
// container.classList.toggle('chapter-rename-mode', enabled);
|
||||
// });
|
||||
|
||||
// lessonContainers.forEach(container => {
|
||||
// container.classList.toggle('chapter-rename-mode', enabled);
|
||||
// });
|
||||
if (enabled) {
|
||||
document.querySelectorAll('.add-lesson-btn').forEach(btn => {
|
||||
btn.style.display = 'none';
|
||||
});
|
||||
document.querySelectorAll('.add-chapter-btn').forEach(btn => {
|
||||
btn.style.display = 'none';
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.add-lesson-btn').forEach(btn => {
|
||||
btn.style.display = 'block';
|
||||
});
|
||||
document.querySelectorAll('.add-chapter-btn').forEach(btn => {
|
||||
btn.style.display = 'block';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// —— 点击“改名模式”按钮 —— //
|
||||
btnRename?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const willEnable = btnRename.getAttribute('aria-pressed') !== 'true';
|
||||
if (willEnable && btnReorder?.getAttribute('aria-pressed') === 'true') {
|
||||
setReorderMode(false);
|
||||
}
|
||||
setRenameMode(willEnable);
|
||||
});
|
||||
|
||||
// 获取展示文字:chapter-item 用 <strong>,lesson-item 用 <span>
|
||||
function getDisplayText(el) {
|
||||
if (el.classList.contains('chapter-item')) {
|
||||
return (el.querySelector('strong')?.textContent ?? '').trim();
|
||||
}
|
||||
if (el.classList.contains('lesson-item')) {
|
||||
return (el.querySelector('span')?.textContent ?? '').trim();
|
||||
}
|
||||
// 兜底
|
||||
return '';
|
||||
}
|
||||
|
||||
// 设置展示文字:chapter-item 用 <strong>,lesson-item 用 <span>
|
||||
function setDisplayText(el, text) {
|
||||
if (el.classList.contains('chapter-item')) {
|
||||
let node = el.querySelector('strong');
|
||||
if (!node) {
|
||||
node = document.createElement('strong');
|
||||
el.prepend(node);
|
||||
}
|
||||
node.textContent = text;
|
||||
return;
|
||||
}
|
||||
if (el.classList.contains('lesson-item')) {
|
||||
let node = el.querySelector('span');
|
||||
if (!node) {
|
||||
node = document.createElement('span');
|
||||
el.prepend(node);
|
||||
}
|
||||
node.textContent = text;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// —— 事件代理:改名模式下点击 li 触发重命名 —— //
|
||||
chapterList.addEventListener('click', (e) => {
|
||||
if (btnRename?.getAttribute('aria-pressed') !== 'true') return; // 非改名模式
|
||||
|
||||
const li = e.target.closest?.('li.chapter-item, li.lesson-item');
|
||||
if (!li) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation(); // 若你担心同层还有其他捕获监听,也可用这个更狠的
|
||||
|
||||
const isChapter = li.classList.contains('chapter-item');
|
||||
const isLesson = li.classList.contains('lesson-item');
|
||||
|
||||
// 读取旧名(章节:显示文字;课时:优先 data-lesson-name)
|
||||
const oldName = isLesson
|
||||
? (li.dataset.lessonName || getDisplayText(li))
|
||||
: getDisplayText(li);
|
||||
|
||||
openTitleEditor({
|
||||
title: isChapter ? '修改章节名称' : '修改课时名称',
|
||||
initialValue: oldName,
|
||||
onSubmit: async (value) => {
|
||||
const res = validateTitle(value);
|
||||
// 先持久化,成功后在本地同步
|
||||
const resSave = await fetch(`/materials/structure/rename/${encodeURIComponent(EDIT_COURSE_CURRENT.material_id)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_chapter: isChapter, chapter_name: isChapter ? oldName : li.dataset.chapterName, lesson_name: isChapter ? "" : li.dataset.lessonName, new_name: value })
|
||||
})
|
||||
const data = await resSave.json();
|
||||
|
||||
if (!data.ok) return { ok: false, message: data.message };
|
||||
const newName = res.value;
|
||||
if (isChapter) { // 将章节改名同步到本地
|
||||
setDisplayText(li, newName);
|
||||
if (li.dataset.chapterName !== undefined) {
|
||||
li.dataset.chapterName = newName;
|
||||
}
|
||||
const scope = li.querySelectorAll?.('li.lesson-item');
|
||||
if (scope && scope.length) {
|
||||
scope.forEach(item => {
|
||||
item.dataset.chapterName = newName;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isLesson) { // 将课时改名同步到本地
|
||||
setDisplayText(li, newName);
|
||||
li.dataset.lessonName = newName;
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* 工具选择器 */
|
||||
function getChapterItems() {
|
||||
|
||||
@@ -3,12 +3,12 @@ let material_id = null;
|
||||
function lessonTemplate(material_id,chapter_name,lesson_name) {
|
||||
return `
|
||||
<li class="lesson-item" data-material-id="${material_id}" data-chapter-name="${chapter_name}" data-lesson-name="${lesson_name}">
|
||||
<span class="lesson-text" onclick="editLesson('${material_id}','${chapter_name}','${lesson_name}')">${lesson_name}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson('${material_id}','${chapter_name}','${lesson_name}')">
|
||||
<span class="lesson-text" onclick="editLesson(this)">${lesson_name}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson(this)">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
<div class="delete-btn-container" onclick="deleteLesson('${material_id}','${chapter_name}','${lesson_name}')">
|
||||
<div class="delete-btn-container" onclick="deleteLesson(this)">
|
||||
<button class="icon-btn delete-btn">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
@@ -216,7 +216,20 @@ function checkAndAddLesson(input, chapterTitle, addButton) {
|
||||
})
|
||||
}
|
||||
|
||||
function deleteLesson(material_id,chapter_name,lesson_name) {
|
||||
function deleteLesson(elemOrEvent) {
|
||||
const el = elemOrEvent?.target ? elemOrEvent.target : elemOrEvent;
|
||||
const li = el?.closest?.('li.lesson-item');
|
||||
if (!li) {
|
||||
console.warn('未找到父级 lesson-item');
|
||||
return;
|
||||
}
|
||||
const { materialId, chapterName, lessonName } = li.dataset;
|
||||
const material_id = materialId;
|
||||
const chapter_name = chapterName;
|
||||
const lesson_name = lessonName;
|
||||
if (!window.confirm(`确认删除课时「${lesson_name}」吗?`)){
|
||||
return;
|
||||
}
|
||||
const lessonItems = document.querySelectorAll('.lesson-item');
|
||||
lessonItems.forEach(item => {
|
||||
if (item.dataset.lessonName === lesson_name && item.dataset.chapterName === chapter_name && item.dataset.materialId === material_id) {
|
||||
@@ -242,7 +255,21 @@ function deleteLesson(material_id,chapter_name,lesson_name) {
|
||||
}
|
||||
|
||||
|
||||
function editLesson(material_id,chapter_name,lesson_name) {
|
||||
function editLesson(elemOrEvent) {
|
||||
const el = elemOrEvent?.target ? elemOrEvent.target : elemOrEvent;
|
||||
const li = el?.closest?.('li.lesson-item');
|
||||
if (!li) {
|
||||
console.warn('未找到父级 lesson-item');
|
||||
return;
|
||||
}
|
||||
const { materialId, chapterName, lessonName } = li.dataset;
|
||||
const material_id = materialId;
|
||||
const chapter_name = chapterName;
|
||||
const lesson_name = lessonName;
|
||||
const renameMode = document.getElementById('toggle-rename').getAttribute('aria-pressed') === 'true';
|
||||
if (renameMode) {
|
||||
return;
|
||||
}
|
||||
// 编辑课时的逻辑
|
||||
alert("编辑课时: " + lesson_name);
|
||||
window.location.href = `/api/materials/${material_id}/chapters/${chapter_name}/lessons/${lesson_name}`;
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/modelinput.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<script src="/static/js/modelinput.js"></script>
|
||||
</head>
|
||||
<body onload="onTeacherboardPageLoad()">
|
||||
|
||||
@@ -18,22 +21,6 @@
|
||||
<div class="teacherboard">
|
||||
<h2 class="teacherboard-title">—— 我的课程 ——</h2>
|
||||
<div class="course-cards" id="course-cards">
|
||||
<!-- 每个课程的卡片 -->
|
||||
<div class="course-card" onclick="openCourseDetails('algorithm')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">算法分析与设计</h3>
|
||||
<p class="course-description">学习基本的算法知识概念,与算法实现。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 继续添加其他课程卡片 -->
|
||||
<div class="course-card" onclick="openCourseDetails('data-structures')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">数据结构与算法</h3>
|
||||
<p class="course-description">深入学习数据结构和算法设计。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 动态加载课程卡片 -->
|
||||
{% for course in materials %}
|
||||
<div class="course-card" onclick="openCourseDetails('{{ course._id }}')">
|
||||
@@ -67,6 +54,9 @@
|
||||
<button id="toggle-reorder" class="reorder-btn" title="设置章节顺序" aria-pressed="false">
|
||||
<i class="fa-solid fa-arrow-up-short-wide"></i>
|
||||
</button>
|
||||
<button id="toggle-rename" class="rename-btn" title="改名模式" aria-pressed="false">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
</h3>
|
||||
<ul id="chapter-list">
|
||||
<div id="loading-message">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify, current_app, session, render_template, session
|
||||
from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson, delete_material_chapter_lesson, reorder_material_structure
|
||||
from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson, delete_material_chapter_lesson, reorder_material_structure, rename_material_structure
|
||||
from ..auth.decorators import require_role
|
||||
from ..services.cos_service import upload_file
|
||||
bp = Blueprint('material', __name__)
|
||||
@@ -79,6 +79,19 @@ def add_lesson(material_id):
|
||||
return jsonify({"message": "课时添加成功"}), 200
|
||||
return jsonify({"message": "课时添加失败"}), 404
|
||||
|
||||
@bp.route('/materials/structure/rename/<material_id>', methods=['POST'])
|
||||
@require_role(roles="teacher")
|
||||
def rename_material(material_id):
|
||||
data = request.get_json()
|
||||
is_chapter = data.get('is_chapter')
|
||||
chapter_name = data.get('chapter_name')
|
||||
lesson_name = data.get('lesson_name')
|
||||
new_name = data.get('new_name')
|
||||
print(f"rename_material: {is_chapter}, {chapter_name}, {lesson_name}, {new_name}")
|
||||
if rename_material_structure(material_id, is_chapter, chapter_name, lesson_name, new_name):
|
||||
return jsonify({"message": "教材重命名成功"}), 200
|
||||
return jsonify({"message": "教材重命名失败"}), 404
|
||||
|
||||
@bp.route('/materials/deletelesson/<material_id>', methods=['DELETE'])
|
||||
@require_role(roles="teacher")
|
||||
def delete_lesson(material_id):
|
||||
|
||||
Reference in New Issue
Block a user