| (function () { |
| const bootstrap = window.__ADMIN_BOOTSTRAP__ || {}; |
| const state = { |
| adminPage: bootstrap.adminPage || "schedule", |
| courses: bootstrap.courses || [], |
| categories: bootstrap.categories || [], |
| scheduleSettings: bootstrap.scheduleSettings || {}, |
| defaultTimeSlots: bootstrap.defaultTimeSlots || [], |
| scheduleEditor: { |
| segments: [], |
| interaction: null, |
| dragKind: null, |
| pixelsPerMinute: 2.25, |
| }, |
| }; |
|
|
| const toastStack = document.getElementById("toastStack"); |
| const scheduleSettingsForm = document.getElementById("scheduleSettingsForm"); |
| const scheduleEditorAxis = document.getElementById("scheduleEditorAxis"); |
| const scheduleEditorTrack = document.getElementById("scheduleEditorTrack"); |
| const scheduleEditorDropzone = document.getElementById("scheduleEditorDropzone"); |
| const scheduleSegmentCount = document.getElementById("scheduleSegmentCount"); |
| const resetTimelineButton = document.getElementById("resetTimelineButton"); |
| const schedulePalette = document.getElementById("schedulePalette"); |
| const createCategoryForm = document.getElementById("createCategoryForm"); |
| const adminGrid = document.getElementById("adminGrid"); |
| const courseGrid = document.getElementById("courseGrid"); |
| const courseForm = document.getElementById("courseForm"); |
| const resetCourseEditorButton = document.getElementById("resetCourseEditorButton"); |
| const courseEditorHeading = document.getElementById("courseEditorHeading"); |
|
|
| if (!toastStack) { |
| return; |
| } |
|
|
| function showToast(message, kind = "success") { |
| const toast = document.createElement("div"); |
| toast.className = `toast ${kind}`; |
| toast.textContent = message; |
| toastStack.appendChild(toast); |
| window.setTimeout(() => toast.remove(), 2600); |
| } |
|
|
| async function requestJSON(url, options = {}) { |
| const response = await fetch(url, { |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| ...options, |
| }); |
| const payload = await response.json().catch(() => ({ ok: false, error: "请求失败" })); |
| if (!response.ok || !payload.ok) { |
| throw new Error(payload.error || "请求失败"); |
| } |
| return payload; |
| } |
|
|
| function toMinutes(value) { |
| const [hour, minute] = String(value).split(":").map(Number); |
| return (hour * 60) + minute; |
| } |
|
|
| function minutesToTime(totalMinutes) { |
| const hour = Math.floor(totalMinutes / 60); |
| const minute = totalMinutes % 60; |
| return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`; |
| } |
|
|
| function snapMinutes(value, step = 5) { |
| return Math.round(value / step) * step; |
| } |
|
|
| function formatWeekPattern(pattern) { |
| if (pattern === "odd") { |
| return "单周"; |
| } |
| if (pattern === "even") { |
| return "双周"; |
| } |
| return "每周"; |
| } |
|
|
| function weekdayLabel(value) { |
| return ["一", "二", "三", "四", "五", "六", "日"][Number(value) - 1] || ""; |
| } |
|
|
| function buildSlotLabel(index) { |
| return `第${String(index + 1).padStart(2, "0")}节课`; |
| } |
|
|
| function getConfiguredTimeSlots() { |
| const slots = Array.isArray(state.scheduleSettings.time_slots) && state.scheduleSettings.time_slots.length |
| ? state.scheduleSettings.time_slots |
| : state.defaultTimeSlots; |
| return slots.map((slot, index) => ({ |
| label: buildSlotLabel(index), |
| start: slot.start, |
| end: slot.end, |
| })); |
| } |
|
|
| function buildScheduleSegmentsFromSlots(timeSlots) { |
| const slots = (timeSlots || []).map((slot, index) => ({ |
| kind: "class", |
| label: buildSlotLabel(index), |
| durationMinutes: Math.max(25, toMinutes(slot.end) - toMinutes(slot.start)), |
| })); |
|
|
| const segments = []; |
| slots.forEach((slot, index) => { |
| if (index > 0) { |
| const previous = timeSlots[index - 1]; |
| const current = timeSlots[index]; |
| const gap = Math.max(5, toMinutes(current.start) - toMinutes(previous.end)); |
| segments.push({ |
| id: `break-${index}`, |
| kind: "break", |
| label: `课间 ${index}`, |
| durationMinutes: gap, |
| }); |
| } |
| segments.push({ |
| id: `class-${index + 1}`, |
| kind: "class", |
| label: slot.label, |
| durationMinutes: slot.durationMinutes, |
| }); |
| }); |
| return segments; |
| } |
|
|
| function renumberSegments(segments) { |
| let classIndex = 1; |
| let breakIndex = 1; |
| segments.forEach((segment) => { |
| if (segment.kind === "class") { |
| segment.label = buildSlotLabel(classIndex - 1); |
| classIndex += 1; |
| } else { |
| segment.label = `课间 ${breakIndex}`; |
| breakIndex += 1; |
| } |
| }); |
| } |
|
|
| function reflowSegments() { |
| const dayStartInput = document.getElementById("dayStartInput"); |
| if (!dayStartInput) { |
| return; |
| } |
| const baseStart = toMinutes(dayStartInput.value); |
| let cursor = baseStart; |
| renumberSegments(state.scheduleEditor.segments); |
| state.scheduleEditor.segments.forEach((segment) => { |
| segment.startMinutes = cursor; |
| segment.endMinutes = cursor + segment.durationMinutes; |
| cursor = segment.endMinutes; |
| }); |
| } |
|
|
| function getEditorPixelsPerMinute() { |
| return state.scheduleEditor.pixelsPerMinute || 1.65; |
| } |
|
|
| function segmentMinDuration(segment) { |
| return segment.kind === "break" ? 5 : 25; |
| } |
|
|
| function validateSegmentOrder(segments) { |
| if (!segments.length) { |
| return "请至少保留一节课"; |
| } |
| if (segments[0].kind !== "class") { |
| return "时间表必须从课程开始"; |
| } |
| if (segments[segments.length - 1].kind !== "class") { |
| return "时间表必须以课程结束"; |
| } |
| for (let index = 1; index < segments.length; index += 1) { |
| if (segments[index].kind === segments[index - 1].kind) { |
| return "课程后面必须是休息,休息后面必须是课程"; |
| } |
| } |
| return null; |
| } |
|
|
| function buildScheduleAxisMarks() { |
| const segments = state.scheduleEditor.segments; |
| if (!segments.length) { |
| return []; |
| } |
| const marks = new Set(); |
| marks.add(segments[0].startMinutes); |
| marks.add(segments[segments.length - 1].endMinutes); |
| segments.forEach((segment) => { |
| marks.add(segment.startMinutes); |
| marks.add(segment.endMinutes); |
| }); |
| let hour = Math.floor(segments[0].startMinutes / 60) * 60; |
| while (hour <= segments[segments.length - 1].endMinutes) { |
| marks.add(hour); |
| hour += 60; |
| } |
| return Array.from(marks).sort((left, right) => left - right); |
| } |
|
|
| function renderScheduleEditor() { |
| if (!scheduleEditorAxis || !scheduleEditorTrack || !scheduleEditorDropzone) { |
| return; |
| } |
|
|
| reflowSegments(); |
| const segments = state.scheduleEditor.segments; |
|
|
| if (!segments.length) { |
| scheduleEditorAxis.innerHTML = ""; |
| scheduleEditorTrack.innerHTML = ""; |
| scheduleEditorTrack.appendChild(scheduleEditorDropzone); |
| if (scheduleSegmentCount) { |
| scheduleSegmentCount.textContent = "0 段"; |
| } |
| return; |
| } |
|
|
| const startMinutes = segments[0].startMinutes; |
| const endMinutes = segments[segments.length - 1].endMinutes; |
| const totalMinutes = Math.max(endMinutes - startMinutes, 1); |
| const contentHeight = Math.max(Math.round(totalMinutes * getEditorPixelsPerMinute()), 980); |
| const dropzoneTop = contentHeight + 20; |
| const height = dropzoneTop + 96; |
|
|
| scheduleEditorAxis.innerHTML = ""; |
| scheduleEditorTrack.innerHTML = ""; |
| scheduleEditorAxis.style.height = `${height}px`; |
| scheduleEditorTrack.style.height = `${height}px`; |
|
|
| const marks = buildScheduleAxisMarks(); |
| const visibleMarks = []; |
|
|
| marks.forEach((minute) => { |
| const top = Math.round((minute - startMinutes) * getEditorPixelsPerMinute()); |
| const line = document.createElement("div"); |
| line.className = "schedule-editor-line"; |
| line.style.top = `${top}px`; |
| scheduleEditorTrack.appendChild(line); |
| }); |
|
|
| marks.forEach((minute, index) => { |
| const top = Math.round((minute - startMinutes) * getEditorPixelsPerMinute()); |
| const isEdge = index === 0 || index === marks.length - 1; |
| const previous = visibleMarks[visibleMarks.length - 1]; |
|
|
| if (isEdge && previous && top - previous.top < 42) { |
| visibleMarks.pop(); |
| } |
|
|
| if (!previous || isEdge || top - (visibleMarks[visibleMarks.length - 1]?.top ?? -Infinity) >= 42) { |
| visibleMarks.push({ minute, top, index }); |
| } |
| }); |
|
|
| visibleMarks.forEach(({ minute, top, index }) => { |
| const tick = document.createElement("div"); |
| tick.className = "schedule-editor-tick"; |
| if (index === 0) { |
| tick.classList.add("is-leading"); |
| } else if (index === marks.length - 1) { |
| tick.classList.add("is-terminal"); |
| } |
| tick.style.top = `${top}px`; |
| tick.textContent = minutesToTime(minute); |
| scheduleEditorAxis.appendChild(tick); |
| }); |
|
|
| segments.forEach((segment, index) => { |
| const minimumHeight = segment.kind === "break" ? 16 : 42; |
| const rawHeight = Math.max(Math.round(segment.durationMinutes * getEditorPixelsPerMinute()), minimumHeight); |
| const inset = rawHeight > 80 ? 4 : rawHeight > 56 ? 3 : 2; |
| const segmentHeight = Math.max(rawHeight - (inset * 2), 16); |
| const block = document.createElement("article"); |
| block.className = `schedule-editor-segment ${segment.kind}`; |
| block.style.top = `${Math.round((segment.startMinutes - startMinutes) * getEditorPixelsPerMinute()) + inset}px`; |
| block.style.height = `${segmentHeight}px`; |
| block.classList.toggle("is-compact", segmentHeight < 88); |
| block.classList.toggle("is-tight", segmentHeight < 56); |
| block.innerHTML = ` |
| <div class="schedule-editor-segment-copy"> |
| <strong>${segment.label}</strong> |
| <span>${minutesToTime(segment.startMinutes)} - ${minutesToTime(segment.endMinutes)}</span> |
| </div> |
| <div class="schedule-editor-segment-kind">${segment.kind === "class" ? "上课" : "课间"}</div> |
| ${segment.kind === "class" ? `<button class="schedule-editor-delete" type="button" data-delete-segment="${index}">删除</button>` : ""} |
| <button class="schedule-editor-resize" type="button" data-resize-segment="${index}" aria-label="调整时长"></button> |
| `; |
| scheduleEditorTrack.appendChild(block); |
| }); |
|
|
| scheduleEditorDropzone.style.top = `${dropzoneTop}px`; |
| scheduleEditorDropzone.style.bottom = "auto"; |
| scheduleEditorTrack.appendChild(scheduleEditorDropzone); |
|
|
| if (scheduleSegmentCount) { |
| scheduleSegmentCount.textContent = `${segments.length} 段`; |
| } |
| } |
|
|
| function appendSegment(kind) { |
| const segments = state.scheduleEditor.segments; |
| if (!segments.length && kind !== "class") { |
| showToast("时间表必须从课程开始", "error"); |
| return; |
| } |
| if (segments.length && segments[segments.length - 1].kind === kind) { |
| showToast("课程后面必须是休息,休息后面必须是课程", "error"); |
| return; |
| } |
|
|
| segments.push({ |
| id: `${kind}-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, |
| kind, |
| label: kind === "class" ? "新课程" : "新课间", |
| durationMinutes: kind === "class" ? 45 : 10, |
| }); |
| renderScheduleEditor(); |
| } |
|
|
| function deleteClassSegment(index) { |
| const segments = [...state.scheduleEditor.segments]; |
| const target = segments[index]; |
| if (!target || target.kind !== "class") { |
| return; |
| } |
|
|
| if (index === 0) { |
| segments.splice(index, segments[index + 1]?.kind === "break" ? 2 : 1); |
| } else if (index === segments.length - 1) { |
| segments.splice(index - 1, segments[index - 1]?.kind === "break" ? 2 : 1); |
| } else { |
| segments.splice(index, 1); |
| if (segments[index - 1] && segments[index] && segments[index - 1].kind === "break" && segments[index].kind === "break") { |
| segments[index - 1].durationMinutes += segments[index].durationMinutes; |
| segments.splice(index, 1); |
| } |
| } |
|
|
| state.scheduleEditor.segments = segments.filter(Boolean); |
| renderScheduleEditor(); |
| } |
|
|
| function beginScheduleResize(event, index) { |
| const segment = state.scheduleEditor.segments[index]; |
| if (!segment) { |
| return; |
| } |
| event.preventDefault(); |
| state.scheduleEditor.interaction = { |
| index, |
| pointerId: event.pointerId, |
| startY: event.clientY, |
| snapshot: state.scheduleEditor.segments.map((item) => ({ ...item })), |
| }; |
| } |
|
|
| function updateScheduleResize(clientY) { |
| const interaction = state.scheduleEditor.interaction; |
| if (!interaction) { |
| return; |
| } |
| const segments = interaction.snapshot.map((item) => ({ ...item })); |
| const target = segments[interaction.index]; |
| const deltaMinutes = snapMinutes((clientY - interaction.startY) / getEditorPixelsPerMinute()); |
| target.durationMinutes = Math.max(segmentMinDuration(target), target.durationMinutes + deltaMinutes); |
| state.scheduleEditor.segments = segments; |
| renderScheduleEditor(); |
| } |
|
|
| function endScheduleResize() { |
| state.scheduleEditor.interaction = null; |
| } |
|
|
| function collectSchedulePayload() { |
| const error = validateSegmentOrder(state.scheduleEditor.segments); |
| if (error) { |
| throw new Error(error); |
| } |
|
|
| reflowSegments(); |
| const classSegments = state.scheduleEditor.segments.filter((segment) => segment.kind === "class"); |
| const dayStart = document.getElementById("dayStartInput").value; |
| const dayEnd = minutesToTime(classSegments[classSegments.length - 1].endMinutes); |
| document.getElementById("dayEndInput").value = dayEnd; |
|
|
| return { |
| semester_start: document.getElementById("semesterStartInput").value, |
| day_start: dayStart, |
| day_end: dayEnd, |
| default_task_duration_minutes: Number(document.getElementById("defaultDurationInput").value), |
| time_slots: classSegments.map((segment, index) => ({ |
| label: buildSlotLabel(index), |
| start: minutesToTime(segment.startMinutes), |
| end: minutesToTime(segment.endMinutes), |
| })), |
| }; |
| } |
|
|
| function resetScheduleEditor(useDefault) { |
| const source = useDefault ? state.defaultTimeSlots : getConfiguredTimeSlots(); |
| state.scheduleEditor.segments = buildScheduleSegmentsFromSlots(source); |
| renderScheduleEditor(); |
| } |
|
|
| function buildPeriodOptions() { |
| return getConfiguredTimeSlots().map((slot, index) => ({ |
| value: String(index + 1), |
| label: `第${String(index + 1).padStart(2, "0")}节 · ${slot.start}-${slot.end}`, |
| })); |
| } |
|
|
| function findPeriodByBoundary(boundary, edge) { |
| const slots = getConfiguredTimeSlots(); |
| const index = slots.findIndex((slot) => slot[edge] === boundary); |
| return index >= 0 ? String(index + 1) : "1"; |
| } |
|
|
| function populateCoursePeriodOptions() { |
| const startSelect = document.getElementById("courseStartPeriodInput"); |
| const endSelect = document.getElementById("courseEndPeriodInput"); |
| if (!startSelect || !endSelect) { |
| return; |
| } |
| const options = buildPeriodOptions(); |
| const markup = options.map((option) => `<option value="${option.value}">${option.label}</option>`).join(""); |
| startSelect.innerHTML = markup; |
| endSelect.innerHTML = markup; |
| } |
|
|
| function courseCard(course) { |
| return ` |
| <article class="admin-card admin-row-card course-card" data-course-id="${course.id}"> |
| <div class="admin-card-head course-row-head"> |
| <div> |
| <p class="column-label">Course</p> |
| <h2>${course.title}</h2> |
| </div> |
| <div class="course-row-actions"> |
| <span class="task-count">周${weekdayLabel(course.day_of_week)}</span> |
| <button class="secondary-button" type="button" data-edit-course="${course.id}">编辑课程</button> |
| <button class="danger-button" type="button" data-delete-course="${course.id}">删除课程</button> |
| </div> |
| </div> |
| <p class="admin-card-copy">${course.start_time} - ${course.end_time} · 第 ${course.start_week}-${course.end_week} 周 · ${formatWeekPattern(course.week_pattern)}</p> |
| <p class="admin-card-copy">${course.location || "未填写地点"}</p> |
| </article> |
| `; |
| } |
|
|
| function renderCourses() { |
| if (!courseGrid) { |
| return; |
| } |
| if (!state.courses.length) { |
| courseGrid.innerHTML = ` |
| <article class="admin-card empty-admin-card"> |
| <h2>还没有固定课程</h2> |
| <p class="admin-card-copy">在左侧填写课程信息并保存后,周课表会自动按周显示。</p> |
| </article> |
| `; |
| return; |
| } |
| courseGrid.innerHTML = state.courses.map(courseCard).join(""); |
| } |
|
|
| function resetCourseForm(course = null) { |
| const slots = getConfiguredTimeSlots(); |
| document.getElementById("courseIdInput").value = course ? course.id : ""; |
| document.getElementById("courseTitleInput").value = course ? course.title : ""; |
| document.getElementById("courseLocationInput").value = course ? (course.location || "") : ""; |
| document.getElementById("courseWeekdayInput").value = course ? String(course.day_of_week) : "1"; |
| document.getElementById("courseStartPeriodInput").value = course ? findPeriodByBoundary(course.start_time, "start") : "1"; |
| document.getElementById("courseEndPeriodInput").value = course ? findPeriodByBoundary(course.end_time, "end") : String(Math.min(2, slots.length || 1)); |
| document.getElementById("courseStartWeekInput").value = course ? String(course.start_week) : "1"; |
| document.getElementById("courseEndWeekInput").value = course ? String(course.end_week) : "16"; |
| document.getElementById("courseWeekPatternInput").value = course ? course.week_pattern : "all"; |
| if (courseEditorHeading) { |
| courseEditorHeading.textContent = course ? "编辑课程" : "新增课程"; |
| } |
| } |
|
|
| function collectCoursePayload() { |
| const slots = getConfiguredTimeSlots(); |
| const startPeriod = Number(document.getElementById("courseStartPeriodInput").value); |
| const endPeriod = Number(document.getElementById("courseEndPeriodInput").value); |
| if (endPeriod < startPeriod) { |
| throw new Error("结束节次不能早于开始节次"); |
| } |
| const startSlot = slots[startPeriod - 1]; |
| const endSlot = slots[endPeriod - 1]; |
| if (!startSlot || !endSlot) { |
| throw new Error("请选择有效的节次"); |
| } |
|
|
| return { |
| title: document.getElementById("courseTitleInput").value.trim(), |
| location: document.getElementById("courseLocationInput").value.trim(), |
| day_of_week: Number(document.getElementById("courseWeekdayInput").value), |
| start_time: startSlot.start, |
| end_time: endSlot.end, |
| start_week: Number(document.getElementById("courseStartWeekInput").value), |
| end_week: Number(document.getElementById("courseEndWeekInput").value), |
| week_pattern: document.getElementById("courseWeekPatternInput").value, |
| }; |
| } |
|
|
| function categoryCard(category) { |
| const taskCount = Array.isArray(category.tasks) ? category.tasks.length : Number(category.task_count || 0); |
| return ` |
| <article class="admin-card admin-row-card list-row-card" data-category-id="${category.id}"> |
| <div class="admin-row-main"> |
| <div> |
| <p class="column-label">Category</p> |
| <h2>${category.name}</h2> |
| <p class="admin-card-copy">删除分类会同时移除其下全部任务,请谨慎操作。</p> |
| </div> |
| <div class="admin-row-side"> |
| <span class="task-count">${taskCount} 项任务</span> |
| <button class="danger-button" type="button" data-delete-category="${category.id}">删除此清单</button> |
| </div> |
| </div> |
| </article> |
| `; |
| } |
|
|
| function renderCategories() { |
| if (!adminGrid) { |
| return; |
| } |
| adminGrid.innerHTML = state.categories.map(categoryCard).join(""); |
| } |
|
|
| function initSchedulePage() { |
| if (!scheduleSettingsForm || !scheduleEditorAxis || !scheduleEditorTrack || !scheduleEditorDropzone) { |
| return; |
| } |
|
|
| resetScheduleEditor(false); |
|
|
| if (schedulePalette) { |
| schedulePalette.addEventListener("dragstart", (event) => { |
| const card = event.target.closest("[data-palette-kind]"); |
| if (!card) { |
| return; |
| } |
| state.scheduleEditor.dragKind = card.dataset.paletteKind; |
| event.dataTransfer.effectAllowed = "copy"; |
| event.dataTransfer.setData("text/plain", state.scheduleEditor.dragKind); |
| }); |
|
|
| schedulePalette.addEventListener("dragend", () => { |
| state.scheduleEditor.dragKind = null; |
| }); |
| } |
|
|
| scheduleEditorTrack.addEventListener("dragover", (event) => { |
| if (!state.scheduleEditor.dragKind) { |
| return; |
| } |
| event.preventDefault(); |
| scheduleEditorDropzone.classList.add("is-active"); |
| }); |
|
|
| scheduleEditorTrack.addEventListener("dragleave", (event) => { |
| if (!scheduleEditorTrack.contains(event.relatedTarget)) { |
| scheduleEditorDropzone.classList.remove("is-active"); |
| } |
| }); |
|
|
| scheduleEditorTrack.addEventListener("drop", (event) => { |
| if (!state.scheduleEditor.dragKind) { |
| return; |
| } |
| event.preventDefault(); |
| scheduleEditorDropzone.classList.remove("is-active"); |
| appendSegment(state.scheduleEditor.dragKind); |
| state.scheduleEditor.dragKind = null; |
| }); |
|
|
| scheduleEditorTrack.addEventListener("click", (event) => { |
| const deleteButton = event.target.closest("[data-delete-segment]"); |
| if (!deleteButton) { |
| return; |
| } |
| deleteClassSegment(Number(deleteButton.dataset.deleteSegment)); |
| }); |
|
|
| scheduleEditorTrack.addEventListener("pointerdown", (event) => { |
| const resizeHandle = event.target.closest("[data-resize-segment]"); |
| if (!resizeHandle) { |
| return; |
| } |
| beginScheduleResize(event, Number(resizeHandle.dataset.resizeSegment)); |
| }); |
|
|
| document.addEventListener("pointermove", (event) => { |
| if (!state.scheduleEditor.interaction || event.pointerId !== state.scheduleEditor.interaction.pointerId) { |
| return; |
| } |
| event.preventDefault(); |
| updateScheduleResize(event.clientY); |
| }); |
|
|
| document.addEventListener("pointerup", (event) => { |
| if (!state.scheduleEditor.interaction || event.pointerId !== state.scheduleEditor.interaction.pointerId) { |
| return; |
| } |
| endScheduleResize(); |
| }); |
|
|
| document.addEventListener("pointercancel", endScheduleResize); |
|
|
| if (resetTimelineButton) { |
| resetTimelineButton.addEventListener("click", () => { |
| resetScheduleEditor(true); |
| showToast("已恢复默认节次,记得保存"); |
| }); |
| } |
|
|
| document.getElementById("dayStartInput").addEventListener("change", renderScheduleEditor); |
|
|
| scheduleSettingsForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| try { |
| const payload = collectSchedulePayload(); |
| const result = await requestJSON("/api/settings/schedule", { |
| method: "PATCH", |
| body: JSON.stringify(payload), |
| }); |
| state.scheduleSettings = result.settings; |
| resetScheduleEditor(false); |
| populateCoursePeriodOptions(); |
| showToast("时间表设置已保存"); |
| } catch (error) { |
| showToast(error.message, "error"); |
| } |
| }); |
| } |
|
|
| function initListsPage() { |
| if (!createCategoryForm || !adminGrid) { |
| return; |
| } |
|
|
| renderCategories(); |
|
|
| createCategoryForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| const nameInput = document.getElementById("newCategoryName"); |
| const name = nameInput.value.trim(); |
| try { |
| const payload = await requestJSON("/api/categories", { |
| method: "POST", |
| body: JSON.stringify({ name }), |
| }); |
| state.categories = [...state.categories, payload.category]; |
| renderCategories(); |
| nameInput.value = ""; |
| showToast("新清单已创建"); |
| } catch (error) { |
| showToast(error.message, "error"); |
| } |
| }); |
|
|
| adminGrid.addEventListener("click", async (event) => { |
| const button = event.target.closest("[data-delete-category]"); |
| if (!button) { |
| return; |
| } |
| try { |
| await requestJSON(`/api/categories/${button.dataset.deleteCategory}`, { |
| method: "DELETE", |
| body: JSON.stringify({}), |
| }); |
| state.categories = state.categories.filter((category) => category.id !== button.dataset.deleteCategory); |
| renderCategories(); |
| showToast("清单已删除"); |
| } catch (error) { |
| showToast(error.message, "error"); |
| } |
| }); |
| } |
|
|
| function initCoursesPage() { |
| if (!courseGrid || !courseForm) { |
| return; |
| } |
|
|
| populateCoursePeriodOptions(); |
| resetCourseForm(); |
| renderCourses(); |
|
|
| if (resetCourseEditorButton) { |
| resetCourseEditorButton.addEventListener("click", () => resetCourseForm()); |
| } |
|
|
| courseForm.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| const courseId = document.getElementById("courseIdInput").value; |
| try { |
| const payload = collectCoursePayload(); |
| const result = await requestJSON(courseId ? `/api/courses/${courseId}` : "/api/courses", { |
| method: courseId ? "PATCH" : "POST", |
| body: JSON.stringify(payload), |
| }); |
| if (courseId) { |
| state.courses = state.courses.map((course) => (course.id === courseId ? result.course : course)); |
| showToast("课程已更新"); |
| } else { |
| state.courses = [result.course, ...state.courses]; |
| showToast("课程已创建"); |
| } |
| renderCourses(); |
| resetCourseForm(); |
| } catch (error) { |
| showToast(error.message, "error"); |
| } |
| }); |
|
|
| courseGrid.addEventListener("click", async (event) => { |
| const editButton = event.target.closest("[data-edit-course]"); |
| if (editButton) { |
| const course = state.courses.find((item) => item.id === editButton.dataset.editCourse); |
| if (course) { |
| resetCourseForm(course); |
| } |
| return; |
| } |
|
|
| const deleteButton = event.target.closest("[data-delete-course]"); |
| if (!deleteButton) { |
| return; |
| } |
|
|
| try { |
| await requestJSON(`/api/courses/${deleteButton.dataset.deleteCourse}`, { |
| method: "DELETE", |
| body: JSON.stringify({}), |
| }); |
| state.courses = state.courses.filter((course) => course.id !== deleteButton.dataset.deleteCourse); |
| renderCourses(); |
| resetCourseForm(); |
| showToast("课程已删除"); |
| } catch (error) { |
| showToast(error.message, "error"); |
| } |
| }); |
| } |
|
|
| initSchedulePage(); |
| initListsPage(); |
| initCoursesPage(); |
| })(); |
|
|