rename chapter/lesson

This commit is contained in:
Cai
2025-09-19 09:06:37 +00:00
parent 6575ee80bc
commit 24f191d5a5
9 changed files with 709 additions and 430 deletions

View File

@@ -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;
}

View File

@@ -110,6 +110,8 @@
background-color: #f0f0f0;
border: 1px dashed #bbb;
}
/* 模态框背景 */
.modal {
display: none; /* 初始时隐藏 */

View File

@@ -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) => {

View File

@@ -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() {

View File

@@ -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}`;