添加chapter和lesson
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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"""
|
||||
# 延迟导入以避免循环
|
||||
|
||||
24
Html/apps/models/material.py
Normal file
24
Html/apps/models/material.py
Normal file
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
10
Html/apps/services/cos_service.py
Normal file
10
Html/apps/services/cos_service.py
Normal file
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
function lessonTemplate(lesson) {
|
||||
|
||||
let material_id = null;
|
||||
function lessonTemplate(lesson_name) {
|
||||
return `
|
||||
<li class="lesson-item">
|
||||
<span class="lesson-text" onclick="editLesson('${lesson}')">${lesson}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson('${lesson}')">
|
||||
<span class="lesson-text" onclick="editLesson('${lesson_name}')">${lesson_name}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson('${lesson_name}')">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson}')">
|
||||
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson_name}')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</li>
|
||||
@@ -77,13 +79,26 @@ function checkAndAddChapter(input) {
|
||||
<ul>
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapterName}', this)">新增课时</button>
|
||||
|
||||
`;
|
||||
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 = `<h3>课程描述:</h3><p>${description}</p>`;
|
||||
|
||||
// 渲染章节标题
|
||||
let lessonsHtml = chapter.lessons.map(lesson => lessonTemplate(lesson)).join('');
|
||||
|
||||
chapterItem.innerHTML = `
|
||||
<strong>${chapter.title}</strong>
|
||||
<ul>
|
||||
${lessonsHtml}
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapter.title}', this)">新增课时</button>
|
||||
`;
|
||||
// 填充章节列表
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
chapterList.innerHTML = ''; // 清空之前的章节列表
|
||||
|
||||
data.chapters.forEach(chapter => {
|
||||
const chapterItem = document.createElement('li');
|
||||
chapterItem.innerHTML = `<strong>${chapter.chapter_name}</strong>`;
|
||||
|
||||
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 = '<p>加载课程详情失败,请稍后再试。</p>';
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84,11 +84,18 @@
|
||||
<div class="logo">
|
||||
<h1>华实君:教学练评一体的虚拟助教</h1>
|
||||
</div>
|
||||
<nav class="navbar-links">
|
||||
{% if role == 'teacher' %}
|
||||
<!-- 如果是教师角色 -->
|
||||
<a href="/teacherindex">首页</a>
|
||||
<a href="/teacherdashboard">课程</a>
|
||||
<a href="/teachergrades">成绩</a>
|
||||
{% else %}
|
||||
<!-- 如果是学生角色 -->
|
||||
<a href="/">首页</a>
|
||||
<a href="/dashboard">课程</a>
|
||||
<a href="/">成绩</a>
|
||||
</nav>
|
||||
<a href="/grades">成绩</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="avatar dropdown">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="个人头像" class="avatar-img">
|
||||
<div class="dropdown-content">
|
||||
|
||||
@@ -33,7 +33,25 @@
|
||||
<p class="course-description">深入学习数据结构和算法设计。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可以继续添加课程卡片 -->
|
||||
<!-- 动态加载课程卡片 -->
|
||||
{% for course in materials %}
|
||||
<div class="course-card" onclick="openCourseDetails('{{ course._id }}')">
|
||||
<img src="{{ course.image_url }}" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">{{ course.name }}</h3>
|
||||
<p class="course-description">{{ course.description }}</p>
|
||||
</div>
|
||||
<div class="course-date">
|
||||
<p class="course-date-text">创建时间:{{ course.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<p class="course-date-text">更新时间:{{ course.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="course-card add-course-card" onclick="openAddCourseModal()">
|
||||
<div class="add-course-button">
|
||||
<span>+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,13 +61,15 @@
|
||||
<span id="course-title"></span>
|
||||
<button class="close-btn" onclick="closeCourseDetails()">×</button>
|
||||
</div>
|
||||
<div class="details-content">
|
||||
<h3>课程章节列表:</h3>
|
||||
<ul id="chapter-list">
|
||||
<!-- 章节列表会在点击课程时动态生成 -->
|
||||
</ul>
|
||||
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
|
||||
</div>
|
||||
<div class="details-content" id="details-content"></div>
|
||||
<h3>课程章节列表:</h3>
|
||||
<ul id="chapter-list">
|
||||
<div id="loading-message">
|
||||
<p>加载中...</p> <!-- 加载中效果 -->
|
||||
</div>
|
||||
</ul>
|
||||
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 编辑章节或课时弹窗 -->
|
||||
@@ -66,6 +86,42 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建课程的浮窗 -->
|
||||
<div id="add-course-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>新建课程</h2>
|
||||
<div class="cover-image-container">
|
||||
<div id="cover-image" class="cover-image-placeholder">
|
||||
<span class="plus-sign">+</span>
|
||||
<input type="file" id="cover-image-input" style="display: none;" accept="image/*" />
|
||||
</div>
|
||||
<img id="cover-preview" src="" alt="封面预览" style="display: none;" />
|
||||
</div>
|
||||
|
||||
<form id="add-course-form">
|
||||
<!-- 选择课程或新建课程的下拉框 -->
|
||||
<label for="course-selection">从课程创建或新建课程:</label>
|
||||
<select id="course-selection">
|
||||
<option value="new">新建课程</option>
|
||||
{% for course in courses %}
|
||||
<option value="{{ course.id }}">{{ course.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<br><br>
|
||||
|
||||
<!-- 课程名称输入框 -->
|
||||
<label for="course-name">课程名称:</label>
|
||||
<input type="text" id="course-name" name="course-name" required><br><br>
|
||||
<label for="course-description">课程描述:</label>
|
||||
<textarea id="course-description" name="course-description"></textarea><br><br>
|
||||
<div class="modal-buttons">
|
||||
<button type="button" onclick="closeAddCourseModal()">取消</button>
|
||||
<button type="submit">确认</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/teacherboard.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -3,8 +3,10 @@ from .markdown import bp as markdown_bp
|
||||
from .vscode import bp as vscode_bp
|
||||
from .auth import bp as auth_bp
|
||||
from .dashboard import bp as main_bp
|
||||
from .material import bp as material_bp
|
||||
def register_blueprints(app: Flask):
|
||||
app.register_blueprint(markdown_bp) # 默认就是挂在根路径
|
||||
app.register_blueprint(vscode_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(material_bp)
|
||||
@@ -42,10 +42,6 @@ def login_teacher_post():
|
||||
ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=True)
|
||||
return jsonify({"success": ok, "message": msg})
|
||||
|
||||
@bp.get("/teacherboard")
|
||||
@require_role(roles="teacher")
|
||||
def teacherboard():
|
||||
return render_template("teacherboard.html")
|
||||
|
||||
@bp.get("/logout")
|
||||
def logout():
|
||||
|
||||
@@ -33,3 +33,5 @@ def convert_md(course_id, filename):
|
||||
@bp.route("/static/<path:filename>")
|
||||
def serve_static(filename):
|
||||
return send_from_directory(current_app.config["STATIC_DIR"], filename)
|
||||
|
||||
|
||||
|
||||
100
Html/apps/views/material.py
Normal file
100
Html/apps/views/material.py
Normal file
@@ -0,0 +1,100 @@
|
||||
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
|
||||
from ..auth.decorators import require_role
|
||||
from ..services.cos_service import upload_file
|
||||
bp = Blueprint('material', __name__)
|
||||
|
||||
@bp.get("/teacherboard")
|
||||
@require_role(roles="teacher")
|
||||
def teacherboard():
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
return render_template("teacherboard.html",role="teacher", materials=get_materials_by_teacher_dict(teacher_id))
|
||||
|
||||
@bp.route('/teacher/upload_image', methods=['POST'])
|
||||
@require_role(roles="teacher")
|
||||
def upload_image():
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
file = request.files.get('file')
|
||||
if not file:
|
||||
return jsonify({"message": "未上传文件"}), 400
|
||||
image_url = upload_file(file, teacher_id + '_' + file.filename)
|
||||
return jsonify({"message": "图片上传成功", "image_url": image_url}), 200
|
||||
|
||||
@bp.route('/create_material', methods=['POST'])
|
||||
@require_role(roles="teacher")
|
||||
def create_new_material():
|
||||
data = request.get_json()
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
image_url = data.get('image_url',None)
|
||||
material_name = data.get('material_name',None)
|
||||
description = data.get('description','')
|
||||
chapters = data.get('chapters',[]) # 如果从其他课程为例进一步修改,则非空
|
||||
if material_name is None:
|
||||
return jsonify({"message": "教材名称不能为空"}), 400
|
||||
material_id = create_material(teacher_id, material_name, description, chapters, image_url)
|
||||
return jsonify({"message": "教材创建成功", "material_id": material_id}), 201
|
||||
|
||||
@bp.route('/materials/<material_id>', methods=['GET'])
|
||||
@require_role(roles="teacher")
|
||||
def get_material(material_id):
|
||||
if material_id is None:
|
||||
return jsonify({"message": "教材名称不能为空"}), 400
|
||||
material = load_material(material_id)
|
||||
print(material)
|
||||
return jsonify( material.model_dump()), 200
|
||||
|
||||
|
||||
@bp.route('/materials/<material_id>', methods=['PUT'])
|
||||
@require_role(roles="teacher")
|
||||
def update_existing_material(material_id):
|
||||
data = request.get_json()
|
||||
chapters = data.get('chapters')
|
||||
if update_material(material_id, chapters):
|
||||
return jsonify({"message": "教材更新成功"}), 200
|
||||
return jsonify({"message": "教材未找到"}), 404
|
||||
|
||||
@bp.route('/materials/addchapter/<material_id>', methods=['POST'])
|
||||
@require_role(roles="teacher")
|
||||
def add_chapter(material_id):
|
||||
data = request.get_json()
|
||||
chapter_name = data.get('chapter_name')
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
if add_material_chapter(material_id, chapter_name, teacher_id):
|
||||
return jsonify({"message": "章节添加成功"}), 200
|
||||
return jsonify({"message": "章节添加失败"}), 404
|
||||
|
||||
@bp.route('/materials/addlesson/<material_id>', methods=['POST'])
|
||||
@require_role(roles="teacher")
|
||||
def add_lesson(material_id):
|
||||
data = request.get_json()
|
||||
chapter_name = data.get('chapter_name')
|
||||
lesson_name = data.get('lesson_name')
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
if add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
|
||||
return jsonify({"message": "课时添加成功"}), 200
|
||||
return jsonify({"message": "课时添加失败"}), 404
|
||||
|
||||
@bp.route('/teacher/materials', methods=['GET'])
|
||||
@require_role(roles="teacher")
|
||||
def get_materials_by_teacher_view():
|
||||
teacher_uuid = session.get("user_uuid")
|
||||
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
|
||||
materials = get_materials_by_teacher(teacher_id)
|
||||
result = []
|
||||
for material in materials:
|
||||
result.append({
|
||||
'_id': str(material._id),
|
||||
'name': material.name,
|
||||
'description': material.description,
|
||||
'created_at': material.created_at,
|
||||
'updated_at': material.updated_at,
|
||||
#'chapters': material.chapters,
|
||||
'image_url': material.image_url
|
||||
})
|
||||
return jsonify(result), 200
|
||||
|
||||
Reference in New Issue
Block a user