From f9d8b3d0923c2bdc7732412ad16f072ff16094dc Mon Sep 17 00:00:00 2001 From: CakeCN Date: Fri, 29 Aug 2025 21:41:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0chapter=E5=92=8Clesson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- Html/apps/auth/decorators.py | 6 +- Html/apps/config.py | 10 + Html/apps/extensions.py | 20 +- Html/apps/models/material.py | 24 ++ Html/apps/services/auth_service.py | 4 +- Html/apps/services/cos_service.py | 10 + Html/apps/services/course_service.py | 84 +++++++ Html/apps/static/css/teacherboard.css | 240 +++++++++++++++++++- Html/apps/static/js/teacherboard.js | 182 ++++++++++++--- Html/apps/templates/navbar.html | 13 +- Html/apps/templates/teacherboard.html | 72 +++++- Html/apps/views/__init__.py | 4 +- Html/apps/views/auth.py | 4 - Html/apps/views/markdown.py | 2 + Html/apps/views/material.py | 100 ++++++++ README.md | 7 + requirements.txt | 4 +- study/TCake/algorithm/binary_search/.config | 1 + 19 files changed, 736 insertions(+), 53 deletions(-) create mode 100644 Html/apps/models/material.py create mode 100644 Html/apps/services/cos_service.py create mode 100644 Html/apps/views/material.py create mode 100644 study/TCake/algorithm/binary_search/.config diff --git a/.gitignore b/.gitignore index 4c142ef..96c1e56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ Html/runs/* Html/config.ini - +Html/runs/* **.pyc # Python bytecode files diff --git a/Html/apps/auth/decorators.py b/Html/apps/auth/decorators.py index 85e85e8..3d8db74 100644 --- a/Html/apps/auth/decorators.py +++ b/Html/apps/auth/decorators.py @@ -52,15 +52,15 @@ def require_role( def wrapper(*args, **kwargs): uuid2username, users_list, injected_checker = _get_deps() - user_id = session.get("user_id") - if not user_id or uuid2username is None or user_id not in uuid2username: + user_uuid = session.get("user_uuid") + if not user_uuid or uuid2username is None or user_uuid not in uuid2username: return redirect(url_for(login_endpoint)) # 仅需登录 if not roles: return func(*args, **kwargs) - username = uuid2username[user_id] + username = uuid2username[user_uuid] # 选择校验器优先级:参数 checker > app 注入的 role_checker > 内置 users_list 适配 effective_checker = checker or injected_checker diff --git a/Html/apps/config.py b/Html/apps/config.py index bb285e1..f34d5b8 100644 --- a/Html/apps/config.py +++ b/Html/apps/config.py @@ -35,3 +35,13 @@ class Config: "windows_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "windows_path", fallback=""), "wsl_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "wsl_path", fallback=""), } + + + MONGO_URI = GLOBAL_CONFIG['MONGO']['uri'] + + TENCENT_COS = { + "secret_id": GLOBAL_CONFIG['TENCENT_COS']['secret_id'], + "secret_key": GLOBAL_CONFIG['TENCENT_COS']['secret_key'], + "bucket": GLOBAL_CONFIG['TENCENT_COS']['bucket'], + "region": GLOBAL_CONFIG['TENCENT_COS']['region'], + } \ No newline at end of file diff --git a/Html/apps/extensions.py b/Html/apps/extensions.py index a98cfdc..5303f4d 100644 --- a/Html/apps/extensions.py +++ b/Html/apps/extensions.py @@ -5,8 +5,13 @@ from flask_cors import CORS from db.user_list import UserList from db.course_list import CourseList from .services.my_function import MyFunction +from flask_pymongo import PyMongo +from qcloud_cos import CosConfig +from qcloud_cos import CosS3Client + + +mongo = PyMongo() -db = SQLAlchemy() socketio = SocketIO(cors_allowed_origins="*") # 不直接传 app cors = CORS() @@ -26,7 +31,6 @@ user_id2UserClass = {} from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager agent_manager = AgentManager() def init_extensions(app): - db.init_app(app) # 第三方 socketio.init_app( app, @@ -51,6 +55,18 @@ def init_extensions(app): app.extensions["user_id2UserClass"] = user_id2UserClass app.extensions["agent_manager"] = agent_manager app.extensions["my_function"] = MyFunction() + + mongo.init_app(app,uri=app.config["MONGO_URI"]) + app.extensions["mongo"] = mongo + + cos_config = CosConfig( + Region=app.config["TENCENT_COS"]["region"], + SecretId=app.config["TENCENT_COS"]["secret_id"], + SecretKey=app.config["TENCENT_COS"]["secret_key"], + ) + cos_client = CosS3Client(cos_config) + app.extensions["cos_client"] = cos_client + def register_namespaces(app): """把所有 Socket.IO namespaces 注册到 socketio""" # 延迟导入以避免循环 diff --git a/Html/apps/models/material.py b/Html/apps/models/material.py new file mode 100644 index 0000000..0c325ea --- /dev/null +++ b/Html/apps/models/material.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime +from bson import ObjectId + +class Lesson(BaseModel): + lesson_name: str + markdown_lesson_file_link: str + markdown_prompt_file_name: str + markdown_score_prompt_file_link: str + +class Chapter(BaseModel): + chapter_name: str + lessons: List[Lesson] + +class Material(BaseModel): + _id: ObjectId + name: str + description: Optional[str] = None + teacher_id: str + created_at: datetime + updated_at: datetime + image_url: Optional[str] = None # 封面图片的url + chapters: List[Chapter] diff --git a/Html/apps/services/auth_service.py b/Html/apps/services/auth_service.py index 823d4dd..d9e8935 100644 --- a/Html/apps/services/auth_service.py +++ b/Html/apps/services/auth_service.py @@ -33,7 +33,7 @@ def login_user(username: str, password: str, *, require_teacher: bool=False): # 设置会话 + 关联映射 user_uuid = "user_" + str(uuid.uuid4()) - session["user_id"] = user_uuid + session["user_uuid"] = user_uuid username2uuid = current_app.extensions["username2uuid"] uuid2username = current_app.extensions["uuid2username"] @@ -43,4 +43,4 @@ def login_user(username: str, password: str, *, require_teacher: bool=False): return True, "登录成功" def logout_user(): - session.pop("user_id", None) + session.pop("user_uuid", None) diff --git a/Html/apps/services/cos_service.py b/Html/apps/services/cos_service.py new file mode 100644 index 0000000..b90b4cb --- /dev/null +++ b/Html/apps/services/cos_service.py @@ -0,0 +1,10 @@ +from flask import current_app + +def upload_file(rb_file, file_name): + cos_client = current_app.extensions["cos_client"] + cos_client.put_object( + Bucket=current_app.config["TENCENT_COS"]["bucket"], + Body=rb_file, + Key=file_name, + ) + return f"https://{current_app.config['TENCENT_COS']['bucket']}.cos.{current_app.config['TENCENT_COS']['region']}.myqcloud.com/{file_name}" \ No newline at end of file diff --git a/Html/apps/services/course_service.py b/Html/apps/services/course_service.py index fb4a179..f49a0a8 100644 --- a/Html/apps/services/course_service.py +++ b/Html/apps/services/course_service.py @@ -1,5 +1,10 @@ # myapp/services/course_service.py +from typing import Dict, List from flask import current_app +from datetime import datetime +from bson import ObjectId + +from ..models.material import Material # 假设你已有这两个 loader from db.course import load_course_from_json @@ -18,3 +23,82 @@ def user_selected_course_briefs(user_obj): brief = course_list.get_course_brief_info(cid, course_obj) briefs.append(brief) return briefs + +def create_material(teacher_id, material_name, description, chapters, image_url): + material = Material( + name=material_name, + description=description, + teacher_id=teacher_id, + created_at=datetime.now(), + updated_at=datetime.now(), + chapters=chapters, + image_url=image_url + ) + mongo = current_app.extensions["mongo"] + material_id = mongo.db.materials.insert_one(material.model_dump()).inserted_id + return str(material_id) + +def load_material(material_id): + mongo = current_app.extensions["mongo"] + material = mongo.db.materials.find_one({"_id": ObjectId(material_id)}) + return Material(**material) + +def update_material(material_id, chapters): + mongo = current_app.extensions["mongo"] + material = mongo.db.materials.find_one({'_id': ObjectId(material_id)}) + if material: + mongo.db.materials.update_one( + {'_id': ObjectId(material_id)}, + {'$set': {'chapters': chapters, 'updated_at': datetime.now()}} + ) + return True + return False + +def add_material_chapter(material_id, chapter_name, teacher_id): + mongo = current_app.extensions["mongo"] + material = mongo.db.materials.find_one({'_id': ObjectId(material_id)}) + if material['teacher_id'] != teacher_id: + return False + if material: + mongo.db.materials.update_one( + {'_id': ObjectId(material_id)}, + {'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}}) + return True + +def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id): + mongo = current_app.extensions["mongo"] + + lesson_doc = { + "lesson_name": lesson_name or "", + "markdown_lesson_file_link": "", + "markdown_prompt_file_name": "", + "markdown_score_prompt_file_link": "", + } + + res = mongo.db.materials.update_one( + { + '_id': ObjectId(material_id), + 'teacher_id': teacher_id, + }, + { + '$push': {'chapters.$[c].lessons': lesson_doc}, + '$set': {'updated_at': datetime.now()}, + }, + array_filters=[{'c.chapter_name': chapter_name}] # 命名占位符 c 指向命中的 chapter + ) + return res.modified_count == 1 + + +def get_materials_by_teacher(teacher_id)->List[Material]: + mongo = current_app.extensions["mongo"] + materials = mongo.db.materials.find({'teacher_id': teacher_id}) + return [Material(**material) for material in materials] + +def get_materials_by_teacher_dict(teacher_id)->List[Dict]: + mongo = current_app.extensions["mongo"] + materials = mongo.db.materials.find({'teacher_id': teacher_id}) + result = [] + for material in materials: + material['_id'] = str(material['_id']) + result.append(material) + return result \ No newline at end of file diff --git a/Html/apps/static/css/teacherboard.css b/Html/apps/static/css/teacherboard.css index c375397..ccc1a35 100644 --- a/Html/apps/static/css/teacherboard.css +++ b/Html/apps/static/css/teacherboard.css @@ -1,4 +1,8 @@ + /* Teacherboard 样式 */ +/* 按钮样式 */ + + .teacherboard { padding: 20px; font-family: Arial, sans-serif; @@ -62,6 +66,206 @@ z-index: 9999; border-radius: 15px; /* 添加圆角 */ } +/* 基本的课程卡片样式 */ +.course-card { + width: 150px; + height: 200px; + margin: 10px; + border: 1px solid #ddd; + border-radius: 8px; + position: relative; + overflow: hidden; + cursor: pointer; +} + +.course-image { + width: 100%; + height: 100px; + object-fit: cover; +} + +.course-info { + padding: 10px; +} + +.course-name { + font-size: 16px; + font-weight: bold; +} + +.course-description { + font-size: 14px; + color: #777; +} + +/* 空的添加课程按钮卡片 */ +.add-course-card { + display: flex; + justify-content: center; + align-items: center; + background-color: #f0f0f0; + border: 1px dashed #bbb; +} +/* 模态框背景 */ +.modal { + display: none; /* 初始时隐藏 */ + position: fixed; + z-index: 1; /* 保证浮窗在顶部 */ + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* 半透明的黑色背景 */ +} + +/* 浮窗内容 */ +.modal-content { + background-color: #fff; + margin: 15% auto; + padding: 20px; + border-radius: 10px; + width: 40%; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* 按钮样式 */ +.modal-buttons { + display: flex; + justify-content: space-between; +} +/* 基本样式,保证输入框和下拉框占满整行 */ +input[type="text"], select { + width: 100%; /* 占满一行 */ + padding: 12px; /* 增加内边距 */ + font-size: 16px; /* 调整字体大小 */ + margin: 10px 0; /* 上下留间距 */ + border: 2px solid #ccc; /* 边框 */ + border-radius: 8px; /* 圆角边框 */ + background-color: #f9f9f9; /* 背景色 */ + transition: border 0.3s ease, box-shadow 0.3s ease; /* 添加过渡效果 */ +} + +/* 鼠标悬停时输入框的效果 */ +input[type="text"]:focus, select:focus { + border-color: #4CAF50; /* 聚焦时的边框颜色 */ + box-shadow: 0 0 10px rgba(76, 175, 80, 0.5); /* 聚焦时的阴影效果 */ + outline: none; /* 去掉默认的聚焦框 */ +} + +/* 给表单标签添加样式 */ +label { + font-size: 18px; /* 增加字体大小 */ + font-weight: bold; /* 加粗字体 */ + margin-bottom: 8px; /* 下方间距 */ + display: block; /* 使标签占一行 */ + color: #333; /* 标签字体颜色 */ +} + +/* 按钮部分 */ +.modal-buttons { + display: flex; + justify-content: space-between; + margin-top: 20px; /* 上方间距 */ +} + + + +/* 添加课程按钮样式 */ +.add-course-button { + display: flex; + justify-content: center; + align-items: center; + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #4CAF50; + color: white; + font-size: 30px; + font-weight: bold; + cursor: pointer; +} + +.add-course-button:hover { + background-color: #45a049; +} +.cover-image-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.cover-image-placeholder { + width: 150px; + height: 150px; + border: 1px solid #ddd; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.modal-buttons button { + width: 48%; + padding: 12px 0; + font-size: 16px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.modal-buttons button:hover { + background-color: #45a049; +} + +.modal-buttons button[type="button"] { + background-color: #f44336; +} + +.modal-buttons button[type="button"]:hover { + background-color: #e53935; +} +.plus-sign { + font-size: 30px; + color: #888; +} + +#cover-preview { + width: 150px; + height: 150px; + margin-top: 10px; +} +/* 课程描述文本框样式 */ +#course-description { + width: 100%; /* 让 textarea 占据整个父容器的宽度 */ + height: 150px; /* 设置适当的高度 */ + padding: 10px; /* 内边距,让文本与边框之间有一定的间距 */ + font-size: 16px; /* 设置字体大小 */ + font-family: Arial, sans-serif; /* 字体样式 */ + border: 1px solid #ddd; /* 边框颜色 */ + border-radius: 8px; /* 边框圆角,使其更加圆润 */ + resize: vertical; /* 允许用户垂直调整大小 */ + box-sizing: border-box; /* 包含内边距和边框在内的总宽高 */ + transition: border-color 0.3s, box-shadow 0.3s; /* 鼠标悬浮和聚焦时的动画效果 */ +} + +/* 课程描述文本框的聚焦样式 */ +#course-description:focus { + border-color: #4CAF50; /* 聚焦时改变边框颜色 */ + box-shadow: 0 0 8px rgba(76, 175, 80, 0.4); /* 聚焦时加上绿色的阴影 */ + outline: none; /* 去掉聚焦时的默认外轮廓 */ +} + +/* 课程描述标签样式 */ +label[for="course-description"] { + font-size: 18px; /* 标签字体大小 */ + font-weight: bold; /* 加粗字体 */ + margin-bottom: 8px; /* 标签与文本框之间的间距 */ + display: block; /* 将标签设置为块级元素,保证换行 */ + color: #333; /* 标签颜色 */ +} .course-details.open { right: 0; /* 打开时显示 */ @@ -71,7 +275,8 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + font-size: 18px; + font-weight: bold; } .details-header button { @@ -81,6 +286,39 @@ cursor: pointer; } +.close-btn { + background: transparent; + border: none; + font-size: 20px; + cursor: pointer; +} + +.add-chapter-btn { + margin-top: 20px; + padding: 10px 15px; + background-color: #4CAF50; + color: white; + border: none; + cursor: pointer; +} + +.add-chapter-btn:hover { + background-color: #45a049; +} + +.course-card { + border: 1px solid #ddd; + padding: 15px; + cursor: pointer; +} + +.course-card:hover { + background-color: #f1f1f1; +} + +#courseDetails.open { + right: 0; /* 打开侧边栏 */ +} .details-content { padding-right: 20px; } diff --git a/Html/apps/static/js/teacherboard.js b/Html/apps/static/js/teacherboard.js index bca619b..72bb6c2 100644 --- a/Html/apps/static/js/teacherboard.js +++ b/Html/apps/static/js/teacherboard.js @@ -1,12 +1,14 @@ -function lessonTemplate(lesson) { + +let material_id = null; +function lessonTemplate(lesson_name) { return `
  • - ${lesson} - -
  • @@ -77,13 +79,26 @@ function checkAndAddChapter(input) { - `; chapterList.appendChild(chapterItem); // 恢复新增按钮的显示 document.querySelector('.add-chapter-btn').style.display = 'block'; input.remove(); isAddingChapter = false; + fetch(`/materials/addchapter/${material_id}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({chapter_name: chapterName}) + }) + .then(response => response.json()) + .then(data => { + alert(data.message); + }) + .catch(error => { + console.error('Error adding chapter:', error); + }); } function openCourseDetails(courseId) { // 打开课程目录 @@ -104,44 +119,61 @@ function deleteChapter(deleteButton) { function renderChapters(courseId) { const chapterList = document.getElementById('chapter-list'); chapterList.innerHTML = ""; // 清空章节列表 - - // 假设从服务器获取课程章节数据 - const chapters = [ - { title: "第一章:算法基础", lessons: ["算法简介", "算法设计方法"] }, - { title: "第二章:数据结构", lessons: ["数组", "链表"] } - ]; - - chapters.forEach(chapter => { - const chapterItem = document.createElement('li'); + material_id = courseId; + fetch(`/materials/${courseId}`) + .then(response => response.json()) + .then(data => { + document.getElementById('course-title').innerText = data.name; + const detailsContent = document.getElementById('details-content'); + // 填充课程描述 + const description = data.description || '暂无描述'; + detailsContent.innerHTML = `

    课程描述:

    ${description}

    `; - // 渲染章节标题 - let lessonsHtml = chapter.lessons.map(lesson => lessonTemplate(lesson)).join(''); - - chapterItem.innerHTML = ` - ${chapter.title} - - - `; + // 填充章节列表 + const chapterList = document.getElementById('chapter-list'); + chapterList.innerHTML = ''; // 清空之前的章节列表 + data.chapters.forEach(chapter => { + const chapterItem = document.createElement('li'); + chapterItem.innerHTML = `${chapter.chapter_name}`; + + const lessonList = document.createElement('ul'); + chapter.lessons.forEach(lesson => { + const lessonItem = document.createElement('li'); + lessonItem.innerHTML = lessonTemplate(lesson.lesson_name); + lessonList.appendChild(lessonItem); + }); + chapterItem.appendChild(lessonList); + const addBtn = document.createElement('button'); + addBtn.className = 'add-lesson-btn'; + addBtn.onclick = () => showInputForNewLesson(chapter.chapter_name, addBtn); + addBtn.textContent = '新增课时'; + chapterItem.appendChild(addBtn); - chapterList.appendChild(chapterItem); + chapterList.appendChild(chapterItem); + }); + + // 使侧边栏滑出 + document.getElementById('courseDetails').classList.add('open'); + }) + .catch(error => { + const detailsContent = document.getElementById('details-content'); + detailsContent.innerHTML = '

    加载课程详情失败,请稍后再试。

    '; + console.error('Error loading course details:', error); }); } + function showInputForNewLesson(chapterTitle, button) { // 防止重复点击 const addButton = button; addButton.style.display = 'none'; // 隐藏按钮 - // 创建一个输入框 const input = document.createElement('input'); input.type = 'text'; input.placeholder = '请输入课时名称...'; input.className = 'new-lesson-input'; input.onblur = () => checkAndAddLesson(input, chapterTitle, addButton); // 失去焦点时检查输入 - // 将输入框插入到章节列表中 const chapterItem = addButton.closest('li'); // 获取到点击按钮的父元素 chapterItem.appendChild(input); @@ -168,6 +200,13 @@ function checkAndAddLesson(input, chapterTitle, addButton) { lessonsList.appendChild(lessonItem); // 将新课时添加到章节下 input.remove(); // 删除输入框 addButton.style.display = 'block'; // 重新显示新增课时按钮 + fetch(`/materials/addlesson/${material_id}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({chapter_name: chapterTitle, lesson_name: lessonName}) + }) } function deleteLesson(lesson) { const lessonItems = document.querySelectorAll('.lesson-item'); @@ -186,4 +225,93 @@ function editLesson(lesson) { function closeCourseDetails() { document.getElementById('courseDetails').classList.remove('open'); + material_id = null; } + + + +// 打开新建课程浮窗 +function openAddCourseModal() { + document.getElementById('add-course-modal').style.display = 'block'; +} + +// 关闭新建课程浮窗 +function closeAddCourseModal() { + document.getElementById('add-course-modal').style.display = 'none'; +} + +// 提交表单处理 +document.getElementById('add-course-form').addEventListener('submit', function(event) { + event.preventDefault(); + + const courseName = document.getElementById('course-name').value; + const courseSelection = document.getElementById('course-selection').value; + const coverImage = document.getElementById('cover-preview').src; + const courseDescription = document.getElementById('course-description').value; + // 构建发送的数据 + const data = { + material_name: courseName, + description: courseDescription, + chapters: [], // 这里可以根据需要添加章节信息,如果有的话 + image_url: coverImage + }; + + // 如果选择了已有课程,可以通过 courseSelection 传递课程ID(根据需要修改) + if (courseSelection !== 'new') { + // 如果是修改已有课程,设置相关的章节信息或其他数据 + data.chapters = ["Chapter 1", "Chapter 2"]; // 示例章节信息 + } + + // 发送 POST 请求到 /create_material + fetch('/create_material', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) // 将数据转为 JSON 格式 + }) + .then(response => response.json()) + .then(data => { + console.log("返回消息:", data.message); // 打印返回消息 + alert(data.message); // 显示一个警告框来显示返回的消息 + courseName.value = ""; + closeAddCourseModal(); // 关闭浮窗 + }) + .catch(error => { + console.error("请求失败:", error); // 如果请求失败,打印错误 + alert("创建课程失败,请稍后再试!"); + }); +}); + +document.getElementById("cover-image").addEventListener("click", function() { + document.getElementById("cover-image-input").click(); +}); + +document.getElementById("cover-image-input").addEventListener("change", function(event) { + const file = event.target.files[0]; + if (file) { + const formData = new FormData(); + formData.append("file", file); + + // 上传图片到服务器 + fetch('/teacher/upload_image', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.image_url) { + // 显示上传后的图片预览 + document.getElementById("cover-preview").src = data.image_url; + document.getElementById("cover-preview").style.display = 'block'; + document.getElementById("cover-image").style.display = 'none'; // 隐藏占位符 + } else { + alert('图片上传失败'); + } + }) + .catch(error => { + alert('上传出错'); + console.error(error); + }); + } +}); diff --git a/Html/apps/templates/navbar.html b/Html/apps/templates/navbar.html index 227ace5..aa4a46c 100644 --- a/Html/apps/templates/navbar.html +++ b/Html/apps/templates/navbar.html @@ -84,11 +84,18 @@ - + 成绩 + {% endif %} + - + + {% for course in materials %} +
    + 课程封面 +
    +

    {{ course.name }}

    +

    {{ course.description }}

    +
    +
    +

    创建时间:{{ course.created_at.strftime('%Y-%m-%d %H:%M:%S') }}

    +

    更新时间:{{ course.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}

    +
    +
    + {% endfor %} +
    +
    + + +
    +
    @@ -43,13 +61,15 @@ -
    -

    课程章节列表:

    - - -
    +
    +

    课程章节列表:

    + + + @@ -66,6 +86,42 @@ + + +